mirror of
https://github.com/LukeHagar/toasty.git
synced 2025-12-06 04:21:49 +00:00
Initial commit
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "base-sveltekit-app",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/kit": "^2.39.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"bits-ui": "latest",
|
||||
"clsx": "latest",
|
||||
"lucide-svelte": "latest",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"src": "latest",
|
||||
"svelte": "^5.38.10",
|
||||
"svelte-check": "^4.3.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "latest",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "5.9.2",
|
||||
"vite": "^7.1.5"
|
||||
}
|
||||
}
|
||||
1677
pnpm-lock.yaml
generated
Normal file
1677
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
76
src/app.css
Normal file
76
src/app.css
Normal file
@@ -0,0 +1,76 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* Updated design tokens to match the blue-themed design brief */
|
||||
--background: hsl(210 40% 98%);
|
||||
--foreground: hsl(222.2 84% 4.9%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(222.2 84% 4.9%);
|
||||
--popover: hsl(220 14% 11%);
|
||||
--popover-foreground: hsl(210 40% 98%);
|
||||
--primary: hsl(217.2 91.2% 59.8%);
|
||||
--primary-foreground: hsl(222.2 84% 4.9%);
|
||||
--secondary: hsl(210 40% 96%);
|
||||
--secondary-foreground: hsl(222.2 84% 4.9%);
|
||||
--muted: hsl(210 40% 96%);
|
||||
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||
--accent: hsl(217.2 91.2% 59.8%);
|
||||
--accent-foreground: hsl(222.2 84% 4.9%);
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
--border: hsl(214.3 31.8% 91.4%);
|
||||
--input: hsl(214.3 31.8% 91.4%);
|
||||
--ring: hsl(217.2 91.2% 59.8%);
|
||||
--chart-1: hsl(217.2 91.2% 59.8%);
|
||||
--chart-2: hsl(220 14% 11%);
|
||||
--chart-3: hsl(262.1 83.3% 57.8%);
|
||||
--chart-4: hsl(221.2 83.2% 53.3%);
|
||||
--chart-5: hsl(212 95% 68%);
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: hsl(210 40% 98%);
|
||||
--sidebar-foreground: hsl(215.4 16.3% 46.9%);
|
||||
--sidebar-primary: hsl(220 14% 11%);
|
||||
--sidebar-primary-foreground: hsl(210 40% 98%);
|
||||
--sidebar-accent: hsl(210 40% 96%);
|
||||
--sidebar-accent-foreground: hsl(222.2 84% 4.9%);
|
||||
--sidebar-border: hsl(214.3 31.8% 91.4%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Updated dark mode tokens with blue undertones as primary theme */
|
||||
--background: hsl(220 14% 11%);
|
||||
--foreground: hsl(210 40% 98%);
|
||||
--card: hsl(222.2 84% 4.9%);
|
||||
--card-foreground: hsl(210 40% 98%);
|
||||
--popover: hsl(220 14% 11%);
|
||||
--popover-foreground: hsl(210 40% 98%);
|
||||
--primary: hsl(217.2 91.2% 59.8%);
|
||||
--primary-foreground: hsl(222.2 84% 4.9%);
|
||||
--secondary: hsl(217.2 32.6% 17.5%);
|
||||
--secondary-foreground: hsl(210 40% 98%);
|
||||
--muted: hsl(217.2 32.6% 17.5%);
|
||||
--muted-foreground: hsl(215 20.2% 65.1%);
|
||||
--accent: hsl(217.2 91.2% 59.8%);
|
||||
--accent-foreground: hsl(210 40% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
--border: hsl(217.2 32.6% 17.5%);
|
||||
--input: hsl(217.2 32.6% 17.5%);
|
||||
--ring: hsl(217.2 91.2% 59.8%);
|
||||
--chart-1: hsl(217.2 91.2% 59.8%);
|
||||
--chart-2: hsl(262.1 83.3% 57.8%);
|
||||
--chart-3: hsl(221.2 83.2% 53.3%);
|
||||
--chart-4: hsl(212 95% 68%);
|
||||
--chart-5: hsl(193 95% 68%);
|
||||
--sidebar-background: hsl(220 14% 11%);
|
||||
--sidebar-foreground: hsl(215 20.2% 65.1%);
|
||||
--sidebar-primary: hsl(217.2 91.2% 59.8%);
|
||||
--sidebar-primary-foreground: hsl(222.2 84% 4.9%);
|
||||
--sidebar-accent: hsl(217.2 32.6% 17.5%);
|
||||
--sidebar-accent-foreground: hsl(210 40% 98%);
|
||||
--sidebar-border: hsl(217.2 32.6% 17.5%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
}
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
111
src/lib/components/schema/APIComponents.svelte
Normal file
111
src/lib/components/schema/APIComponents.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Globe,
|
||||
Send,
|
||||
Download,
|
||||
Upload,
|
||||
Trash,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
Key,
|
||||
Shield,
|
||||
FileText
|
||||
} from 'lucide-svelte';
|
||||
|
||||
const httpMethods = [
|
||||
{ method: 'GET', color: 'bg-green-500 text-white', icon: Download },
|
||||
{ method: 'POST', color: 'bg-blue-500 text-white', icon: Upload },
|
||||
{ method: 'PUT', color: 'bg-orange-500 text-white', icon: Send },
|
||||
{ method: 'PATCH', color: 'bg-yellow-500 text-white', icon: Settings },
|
||||
{ method: 'DELETE', color: 'bg-red-500 text-white', icon: Trash }
|
||||
];
|
||||
|
||||
const apiComponents = [
|
||||
{
|
||||
type: 'parameter',
|
||||
icon: Key,
|
||||
label: 'Parameter',
|
||||
description: 'Query, path, or header parameter',
|
||||
color: 'bg-indigo-500/10 text-indigo-600 border-indigo-500/20'
|
||||
},
|
||||
{
|
||||
type: 'response',
|
||||
icon: ArrowRight,
|
||||
label: 'Response',
|
||||
description: 'HTTP response definition',
|
||||
color: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
||||
},
|
||||
{
|
||||
type: 'security',
|
||||
icon: Shield,
|
||||
label: 'Security',
|
||||
description: 'Authentication scheme',
|
||||
color: 'bg-rose-500/10 text-rose-600 border-rose-500/20'
|
||||
},
|
||||
{
|
||||
type: 'schema',
|
||||
icon: FileText,
|
||||
label: 'Schema',
|
||||
description: 'Reusable schema definition',
|
||||
color: 'bg-violet-500/10 text-violet-600 border-violet-500/20'
|
||||
}
|
||||
];
|
||||
|
||||
function handleMethodDragStart(event: DragEvent, method: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('api-method', method.method);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
|
||||
function handleComponentDragStart(event: DragEvent, component: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('api-component', component.type);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">HTTP Methods</h3>
|
||||
<div class="space-y-2">
|
||||
{#each httpMethods as method}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleMethodDragStart(e, method)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge class="text-xs {method.color} font-mono">{method.method}</Badge>
|
||||
<method.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">Endpoint</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">/{method.method.toLowerCase()}/resource</p>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">API Components</h3>
|
||||
<div class="space-y-2">
|
||||
{#each apiComponents as component}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleComponentDragStart(e, component)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<component.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{component.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{component.description}</p>
|
||||
<Badge class="text-xs mt-1 {component.color}">{component.type}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
145
src/lib/components/schema/DragDropCanvas.svelte
Normal file
145
src/lib/components/schema/DragDropCanvas.svelte
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts">
|
||||
import { schemaStore, addSchema, addEndpoint, selectItem, type SchemaItem, type APIEndpoint } from '$lib/stores/schema';
|
||||
import SchemaNode from './SchemaNode.svelte';
|
||||
import EndpointNode from './EndpointNode.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
let canvasElement: HTMLDivElement;
|
||||
let isDragOver = $state(false);
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
isDragOver = true;
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
isDragOver = false;
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
isDragOver = false;
|
||||
|
||||
const rect = canvasElement.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Get drag data
|
||||
const schemaType = event.dataTransfer?.getData('schema-type');
|
||||
const schemaFormat = event.dataTransfer?.getData('schema-format');
|
||||
const apiMethod = event.dataTransfer?.getData('api-method');
|
||||
const apiComponent = event.dataTransfer?.getData('api-component');
|
||||
|
||||
if (schemaType) {
|
||||
// Create new schema item
|
||||
const newSchema: Omit<SchemaItem, 'id'> = {
|
||||
type: schemaType as any,
|
||||
name: `new_${schemaType}`,
|
||||
required: false,
|
||||
description: `A ${schemaType} field`,
|
||||
format: schemaFormat || undefined,
|
||||
children: schemaType === 'object' || schemaType === 'array' ? [] : undefined,
|
||||
x,
|
||||
y
|
||||
};
|
||||
addSchema(newSchema);
|
||||
} else if (apiMethod) {
|
||||
// Create new endpoint
|
||||
const newEndpoint: Omit<APIEndpoint, 'id'> = {
|
||||
method: apiMethod as any,
|
||||
path: `/${apiMethod.toLowerCase()}/resource`,
|
||||
summary: `${apiMethod} operation`,
|
||||
description: `${apiMethod} endpoint description`,
|
||||
x,
|
||||
y
|
||||
};
|
||||
addEndpoint(newEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: SchemaItem | APIEndpoint) {
|
||||
selectItem(item);
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
// Trigger the button click event on Enter key press
|
||||
const button = event.target as HTMLButtonElement;
|
||||
button.click();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Created interactive drag and drop canvas -->
|
||||
<div
|
||||
bind:this={canvasElement}
|
||||
class="relative min-h-96 border-2 border-dashed rounded-lg transition-colors {isDragOver ? 'border-primary bg-primary/5' : 'border-border bg-muted/20'}"
|
||||
role="button"
|
||||
aria-label="Drop Components Here"
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
tabindex="0"
|
||||
>
|
||||
{#if $schemaStore.schemas.length === 0 && $schemaStore.endpoints.length === 0}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Plus class="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-foreground mb-2">Drop Components Here</h3>
|
||||
<p class="text-muted-foreground mb-4">Drag schema types and API components from the sidebar to build your OpenAPI specification</p>
|
||||
<Button onkeydown={handleKeyDown} tabindex="0">
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
Add Component
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Render dropped schemas -->
|
||||
{#each $schemaStore.schemas as schema}
|
||||
<div
|
||||
class="absolute cursor-pointer"
|
||||
style="left: {schema.x}px; top: {schema.y}px;"
|
||||
role="button"
|
||||
aria-label={`Click to select ${schema.name}`}
|
||||
onclick={() => handleItemClick(schema)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleItemClick(schema);
|
||||
}
|
||||
}}
|
||||
tabindex="0"
|
||||
>
|
||||
<SchemaNode
|
||||
{...schema}
|
||||
selected={$schemaStore.selectedItem?.id === schema.id}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Render dropped endpoints -->
|
||||
{#each $schemaStore.endpoints as endpoint}
|
||||
<div
|
||||
class="absolute cursor-pointer"
|
||||
style="left: {endpoint.x}px; top: {endpoint.y}px;"
|
||||
role="button"
|
||||
aria-label={`Click to select ${endpoint.path}`}
|
||||
onclick={() => handleItemClick(endpoint)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleItemClick(endpoint);
|
||||
}
|
||||
}}
|
||||
tabindex="0"
|
||||
>
|
||||
<EndpointNode
|
||||
{...endpoint}
|
||||
selected={$schemaStore.selectedItem?.id === endpoint.id}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
60
src/lib/components/schema/EndpointNode.svelte
Normal file
60
src/lib/components/schema/EndpointNode.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardContent, CardHeader } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Trash2, Settings } from 'lucide-svelte';
|
||||
import { removeSchema } from '$lib/stores/schema';
|
||||
|
||||
interface EndpointNodeProps {
|
||||
id: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
path: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
let { id, method, path, summary, description, selected = false }: EndpointNodeProps = $props();
|
||||
|
||||
const methodColors = {
|
||||
GET: 'bg-green-500 text-white',
|
||||
POST: 'bg-blue-500 text-white',
|
||||
PUT: 'bg-orange-500 text-white',
|
||||
PATCH: 'bg-yellow-500 text-white',
|
||||
DELETE: 'bg-red-500 text-white'
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Created endpoint node component for API operations -->
|
||||
<Card class="w-64 hover:bg-accent/50 transition-colors group {selected ? 'ring-2 ring-primary' : ''}">
|
||||
<CardHeader class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge class="text-xs font-mono {methodColors[method]}">{method}</Badge>
|
||||
<span class="font-medium text-sm">{path}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button variant="ghost" size="sm" class="w-6 h-6 p-0">
|
||||
<Settings class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-6 h-6 p-0 text-destructive"
|
||||
onclick={() => removeSchema(id)}
|
||||
>
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if summary}
|
||||
<p class="text-xs font-medium text-foreground mt-1">{summary}</p>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<p class="text-xs text-muted-foreground mt-1">{description}</p>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
289
src/lib/components/schema/ExportDialog.svelte
Normal file
289
src/lib/components/schema/ExportDialog.svelte
Normal file
@@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import {
|
||||
Download,
|
||||
FileText,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Eye,
|
||||
Settings
|
||||
} from 'lucide-svelte';
|
||||
import { schemaStore } from '$lib/stores/schema';
|
||||
import { generateOpenAPISpec, validateOpenAPISpec, downloadOpenAPISpec, type OpenAPISpec } from '$lib/utils/openapi-generator';
|
||||
|
||||
interface ExportDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { open, onClose }: ExportDialogProps = $props();
|
||||
|
||||
let apiTitle = $state('My API');
|
||||
let apiVersion = $state('1.0.0');
|
||||
let apiDescription = $state('API specification generated with OpenAPI Designer');
|
||||
let serverUrl = $state('https://api.example.com/v1');
|
||||
let showPreview = $state(false);
|
||||
let copied = $state(false);
|
||||
|
||||
let generatedSpec = $derived.by(() => {
|
||||
if (!open) return null;
|
||||
|
||||
const spec = generateOpenAPISpec($schemaStore);
|
||||
spec.info.title = apiTitle;
|
||||
spec.info.version = apiVersion;
|
||||
spec.info.description = apiDescription;
|
||||
|
||||
if (spec.servers && spec.servers.length > 0) {
|
||||
spec.servers[0].url = serverUrl;
|
||||
}
|
||||
|
||||
return spec;
|
||||
});
|
||||
|
||||
let validationIssues = $derived(generatedSpec ? validateOpenAPISpec(generatedSpec) : []);
|
||||
let hasErrors = $derived(validationIssues.some(issue => issue.type === 'error'));
|
||||
let hasWarnings = $derived(validationIssues.some(issue => issue.type === 'warning'));
|
||||
|
||||
function handleExport() {
|
||||
if (generatedSpec && !hasErrors) {
|
||||
const filename = `${apiTitle.toLowerCase().replace(/\s+/g, '-')}-openapi.json`;
|
||||
downloadOpenAPISpec(generatedSpec, filename);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
if (generatedSpec) {
|
||||
navigator.clipboard.writeText(JSON.stringify(generatedSpec, null, 2));
|
||||
copied = true;
|
||||
setTimeout(() => copied = false, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePreview() {
|
||||
showPreview = !showPreview;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- Created comprehensive export dialog with validation and preview -->
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-background border rounded-lg shadow-lg max-w-4xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Export OpenAPI Specification</h2>
|
||||
<p class="text-sm text-muted-foreground mt-1">Configure and download your API specification</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onclick={onClose}>
|
||||
<XCircle class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
|
||||
<!-- Configuration -->
|
||||
<div class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<Settings class="w-4 h-4" />
|
||||
API Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div>
|
||||
<Label for="api-title">Title</Label>
|
||||
<Input
|
||||
id="api-title"
|
||||
bind:value={apiTitle}
|
||||
placeholder="My API"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="api-version">Version</Label>
|
||||
<Input
|
||||
id="api-version"
|
||||
bind:value={apiVersion}
|
||||
placeholder="1.0.0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="api-description">Description</Label>
|
||||
<Textarea
|
||||
id="api-description"
|
||||
bind:value={apiDescription}
|
||||
placeholder="Describe your API..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if $schemaStore.openApiVersion !== '2.0'}
|
||||
<div>
|
||||
<Label for="server-url">Server URL</Label>
|
||||
<Input
|
||||
id="server-url"
|
||||
bind:value={serverUrl}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Validation Results -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
{#if hasErrors}
|
||||
<XCircle class="w-4 h-4 text-destructive" />
|
||||
{:else if hasWarnings}
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-500" />
|
||||
{:else}
|
||||
<CheckCircle class="w-4 h-4 text-green-500" />
|
||||
{/if}
|
||||
Validation Results
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if validationIssues.length === 0}
|
||||
<div class="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle class="w-4 h-4" />
|
||||
<span class="text-sm">No issues found</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each validationIssues as issue}
|
||||
<div class="flex items-start gap-2 text-sm">
|
||||
{#if issue.type === 'error'}
|
||||
<XCircle class="w-4 h-4 text-destructive mt-0.5 flex-shrink-0" />
|
||||
{:else}
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
{/if}
|
||||
<div>
|
||||
<div class="font-medium">{issue.message}</div>
|
||||
{#if issue.path}
|
||||
<div class="text-muted-foreground text-xs">{issue.path}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Statistics -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Specification Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<div class="font-medium">Schemas</div>
|
||||
<div class="text-muted-foreground">{$schemaStore.schemas.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">Endpoints</div>
|
||||
<div class="text-muted-foreground">{$schemaStore.endpoints.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">Version</div>
|
||||
<div class="text-muted-foreground">{$schemaStore.openApiVersion}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">Format</div>
|
||||
<div class="text-muted-foreground">JSON</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium">Specification Preview</h3>
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" variant="outline" onclick={togglePreview}>
|
||||
<Eye class="w-3 h-3 mr-1" />
|
||||
{showPreview ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onclick={copyToClipboard}>
|
||||
{#if copied}
|
||||
<CheckCircle class="w-3 h-3 mr-1" />
|
||||
Copied
|
||||
{:else}
|
||||
<Copy class="w-3 h-3 mr-1" />
|
||||
Copy
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showPreview && generatedSpec}
|
||||
<div class="border rounded-lg bg-muted/20 p-4 max-h-96 overflow-auto">
|
||||
<pre class="text-xs text-foreground whitespace-pre-wrap">{JSON.stringify(generatedSpec, null, 2)}</pre>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border-2 border-dashed border-border rounded-lg p-8 text-center">
|
||||
<FileText class="w-8 h-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p class="text-sm text-muted-foreground">Click "Show" to preview the generated specification</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-6 border-t bg-muted/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if hasErrors}
|
||||
<Badge variant="destructive">
|
||||
<XCircle class="w-3 h-3 mr-1" />
|
||||
{validationIssues.filter(i => i.type === 'error').length} errors
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if hasWarnings}
|
||||
<Badge variant="secondary">
|
||||
<AlertTriangle class="w-3 h-3 mr-1" />
|
||||
{validationIssues.filter(i => i.type === 'warning').length} warnings
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if !hasErrors && !hasWarnings}
|
||||
<Badge variant="secondary" class="text-green-600">
|
||||
<CheckCircle class="w-3 h-3 mr-1" />
|
||||
Ready to export
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" onclick={onClose}>Cancel</Button>
|
||||
<Button onclick={handleExport} disabled={hasErrors}>
|
||||
<Download class="w-4 h-4 mr-2" />
|
||||
Export JSON
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
378
src/lib/components/schema/PropertyEditor.svelte
Normal file
378
src/lib/components/schema/PropertyEditor.svelte
Normal file
@@ -0,0 +1,378 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Settings, Plus, Trash2, AlertTriangle } from 'lucide-svelte';
|
||||
import { schemaStore, updateSchema, type SchemaItem, type APIEndpoint } from '$lib/stores/schema';
|
||||
import { getVersionConfig, getAvailableFormats, isFeatureSupported } from '$lib/utils/openapi-versions';
|
||||
|
||||
let versionConfig = $derived(getVersionConfig($schemaStore.openApiVersion));
|
||||
let selectedItem = $derived($schemaStore.selectedItem);
|
||||
|
||||
// Form state for editing properties
|
||||
let editingName = $state('');
|
||||
let editingDescription = $state('');
|
||||
let editingRequired = $state(false);
|
||||
let editingNullable = $state(false);
|
||||
let editingFormat = $state('');
|
||||
let editingMinLength = $state('');
|
||||
let editingMaxLength = $state('');
|
||||
let editingMinimum = $state('');
|
||||
let editingMaximum = $state('');
|
||||
let editingPattern = $state('');
|
||||
let editingEnumValues = $state<string[]>([]);
|
||||
let editingExample = $state('');
|
||||
|
||||
// Update form when selection changes
|
||||
$effect(() => {
|
||||
if (selectedItem && 'type' in selectedItem) {
|
||||
const item = selectedItem as SchemaItem;
|
||||
editingName = item.name || '';
|
||||
editingDescription = item.description || '';
|
||||
editingRequired = item.required || false;
|
||||
editingNullable = item.properties?.nullable || false;
|
||||
editingFormat = item.format || '';
|
||||
editingMinLength = item.properties?.minLength?.toString() || '';
|
||||
editingMaxLength = item.properties?.maxLength?.toString() || '';
|
||||
editingMinimum = item.properties?.minimum?.toString() || '';
|
||||
editingMaximum = item.properties?.maximum?.toString() || '';
|
||||
editingPattern = item.properties?.pattern || '';
|
||||
editingEnumValues = item.properties?.enum || [];
|
||||
editingExample = item.properties?.example || '';
|
||||
}
|
||||
});
|
||||
|
||||
function saveChanges() {
|
||||
if (!selectedItem || !('type' in selectedItem)) return;
|
||||
|
||||
const updates: Partial<SchemaItem> = {
|
||||
name: editingName,
|
||||
description: editingDescription,
|
||||
required: editingRequired,
|
||||
format: editingFormat || undefined,
|
||||
properties: {
|
||||
...selectedItem.properties,
|
||||
nullable: versionConfig.features.nullable ? editingNullable : undefined,
|
||||
minLength: editingMinLength ? parseInt(editingMinLength) : undefined,
|
||||
maxLength: editingMaxLength ? parseInt(editingMaxLength) : undefined,
|
||||
minimum: editingMinimum ? parseFloat(editingMinimum) : undefined,
|
||||
maximum: editingMaximum ? parseFloat(editingMaximum) : undefined,
|
||||
pattern: editingPattern || undefined,
|
||||
enum: editingEnumValues.length > 0 ? editingEnumValues : undefined,
|
||||
example: editingExample || undefined
|
||||
}
|
||||
};
|
||||
|
||||
updateSchema(selectedItem.id, updates);
|
||||
}
|
||||
|
||||
function addEnumValue() {
|
||||
editingEnumValues = [...editingEnumValues, ''];
|
||||
}
|
||||
|
||||
function removeEnumValue(index: number) {
|
||||
editingEnumValues = editingEnumValues.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
function updateEnumValue(index: number, value: string) {
|
||||
editingEnumValues[index] = value;
|
||||
editingEnumValues = [...editingEnumValues];
|
||||
}
|
||||
|
||||
let availableFormats = $derived(
|
||||
selectedItem && 'type' in selectedItem
|
||||
? getAvailableFormats($schemaStore.openApiVersion, selectedItem.type)
|
||||
: []
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- Created comprehensive property editor for schema components -->
|
||||
<div class="space-y-4">
|
||||
{#if selectedItem}
|
||||
{#if 'type' in selectedItem}
|
||||
<!-- Schema Item Editor -->
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<Settings class="w-4 h-4" />
|
||||
Schema Properties
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Basic Properties -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={editingName}
|
||||
placeholder="Property name"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={editingDescription}
|
||||
placeholder="Describe this property..."
|
||||
rows={2}
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="required">Required</Label>
|
||||
<Switch
|
||||
id="required"
|
||||
bind:checked={editingRequired}
|
||||
onchange={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if versionConfig.features.nullable}
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="nullable">Nullable</Label>
|
||||
<Switch
|
||||
id="nullable"
|
||||
bind:checked={editingNullable}
|
||||
onchange={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Format Selection -->
|
||||
{#if availableFormats.length > 0}
|
||||
<Separator />
|
||||
<div>
|
||||
<Label for="format">Format</Label>
|
||||
<select
|
||||
id="format"
|
||||
class="w-full mt-1 p-2 rounded border bg-input text-foreground"
|
||||
bind:value={editingFormat}
|
||||
onchange={saveChanges}
|
||||
>
|
||||
<option value="">No format</option>
|
||||
{#each availableFormats as format}
|
||||
<option value={format}>{format}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Type-specific constraints -->
|
||||
{#if selectedItem.type === 'string'}
|
||||
<Separator />
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-sm font-medium">String Constraints</h4>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label for="minLength">Min Length</Label>
|
||||
<Input
|
||||
id="minLength"
|
||||
type="number"
|
||||
bind:value={editingMinLength}
|
||||
placeholder="0"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="maxLength">Max Length</Label>
|
||||
<Input
|
||||
id="maxLength"
|
||||
type="number"
|
||||
bind:value={editingMaxLength}
|
||||
placeholder="100"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="pattern">Pattern (Regex)</Label>
|
||||
<Input
|
||||
id="pattern"
|
||||
bind:value={editingPattern}
|
||||
placeholder="^[a-zA-Z0-9]+$"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedItem.type === 'number' || selectedItem.type === 'integer'}
|
||||
<Separator />
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-sm font-medium">Numeric Constraints</h4>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label for="minimum">Minimum</Label>
|
||||
<Input
|
||||
id="minimum"
|
||||
type="number"
|
||||
bind:value={editingMinimum}
|
||||
placeholder="0"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="maximum">Maximum</Label>
|
||||
<Input
|
||||
id="maximum"
|
||||
type="number"
|
||||
bind:value={editingMaximum}
|
||||
placeholder="100"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedItem.type === 'enum' || editingEnumValues.length > 0}
|
||||
<Separator />
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-medium">Enum Values</h4>
|
||||
<Button size="sm" variant="outline" onclick={addEnumValue}>
|
||||
<Plus class="w-3 h-3 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{#each editingEnumValues as value, index}
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={editingEnumValues[index]}
|
||||
placeholder="enum value"
|
||||
oninput={(e) => updateEnumValue(index, e.target.value)}
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="text-destructive"
|
||||
onclick={() => removeEnumValue(index)}
|
||||
>
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Example -->
|
||||
{#if versionConfig.features.examples}
|
||||
<Separator />
|
||||
<div>
|
||||
<Label for="example">Example</Label>
|
||||
<Input
|
||||
id="example"
|
||||
bind:value={editingExample}
|
||||
placeholder="Example value"
|
||||
onblur={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{:else if 'method' in selectedItem}
|
||||
<!-- API Endpoint Editor -->
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm flex items-center gap-2">
|
||||
<Settings class="w-4 h-4" />
|
||||
Endpoint Properties
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div>
|
||||
<Label for="endpoint-path">Path</Label>
|
||||
<Input
|
||||
id="endpoint-path"
|
||||
value={selectedItem.path}
|
||||
placeholder="/api/resource"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="endpoint-summary">Summary</Label>
|
||||
<Input
|
||||
id="endpoint-summary"
|
||||
value={selectedItem.summary || ''}
|
||||
placeholder="Brief description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="endpoint-description">Description</Label>
|
||||
<Textarea
|
||||
id="endpoint-description"
|
||||
value={selectedItem.description || ''}
|
||||
placeholder="Detailed description..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge class="font-mono">{selectedItem.method}</Badge>
|
||||
<span class="text-sm text-muted-foreground">HTTP Method</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Advanced Features (version-dependent) -->
|
||||
{#if 'type' in selectedItem}
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">Advanced Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
{#if versionConfig.features.oneOf}
|
||||
<Button size="sm" variant="outline" class="w-full justify-start">
|
||||
<Plus class="w-3 h-3 mr-2" />
|
||||
Add oneOf
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if versionConfig.features.anyOf}
|
||||
<Button size="sm" variant="outline" class="w-full justify-start">
|
||||
<Plus class="w-3 h-3 mr-2" />
|
||||
Add anyOf
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if versionConfig.features.allOf}
|
||||
<Button size="sm" variant="outline" class="w-full justify-start">
|
||||
<Plus class="w-3 h-3 mr-2" />
|
||||
Add allOf
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if !versionConfig.features.oneOf && !versionConfig.features.anyOf}
|
||||
<div class="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded text-xs">
|
||||
<div class="flex items-center gap-1 mb-1">
|
||||
<AlertTriangle class="w-3 h-3 text-yellow-600" />
|
||||
<span class="font-medium text-yellow-600">Limited Features</span>
|
||||
</div>
|
||||
<p class="text-yellow-600">oneOf/anyOf not available in {versionConfig.displayName}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="text-center text-muted-foreground py-8">
|
||||
<Settings class="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p class="text-sm">Select a component to edit its properties</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
114
src/lib/components/schema/SchemaNode.svelte
Normal file
114
src/lib/components/schema/SchemaNode.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import SchemaNode from './SchemaNode.svelte';
|
||||
import { Card, CardContent, CardHeader } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ChevronDown, ChevronRight, Trash2, Plus } from 'lucide-svelte';
|
||||
import { removeSchema } from '$lib/stores/schema';
|
||||
|
||||
interface SchemaNodeProps {
|
||||
id: string;
|
||||
type: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'enum';
|
||||
name: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
format?: string;
|
||||
children?: any[];
|
||||
expanded?: boolean;
|
||||
level?: number;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
required = false,
|
||||
description = '',
|
||||
format,
|
||||
children = [],
|
||||
expanded = true,
|
||||
level = 0,
|
||||
selected = false
|
||||
}: SchemaNodeProps = $props();
|
||||
|
||||
let isExpanded = $state(expanded);
|
||||
|
||||
const typeColors = {
|
||||
object: 'bg-blue-500/10 text-blue-600 border-blue-500/20',
|
||||
array: 'bg-green-500/10 text-green-600 border-green-500/20',
|
||||
string: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20',
|
||||
number: 'bg-purple-500/10 text-purple-600 border-purple-500/20',
|
||||
integer: 'bg-purple-500/10 text-purple-600 border-purple-500/20',
|
||||
boolean: 'bg-red-500/10 text-red-600 border-red-500/20',
|
||||
enum: 'bg-orange-500/10 text-orange-600 border-orange-500/20'
|
||||
};
|
||||
|
||||
const hasChildren = children.length > 0;
|
||||
</script>
|
||||
|
||||
<!-- Updated schema node with selection state and improved interactions -->
|
||||
<div class="schema-node w-64" style="margin-left: {level * 20}px">
|
||||
<Card class="mb-2 hover:bg-accent/50 transition-colors group {selected ? 'ring-2 ring-primary' : ''}">
|
||||
<CardHeader class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if hasChildren}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-6 h-6 p-0"
|
||||
onclick={() => isExpanded = !isExpanded}
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
{:else}
|
||||
<ChevronRight class="w-3 h-3" />
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-6"></div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{name}</span>
|
||||
{#if required}
|
||||
<Badge variant="destructive" class="text-xs px-1 py-0">required</Badge>
|
||||
{/if}
|
||||
<Badge class="text-xs {typeColors[type]}">
|
||||
{format ? format : type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{#if type === 'object' || type === 'array'}
|
||||
<Button variant="ghost" size="sm" class="w-6 h-6 p-0">
|
||||
<Plus class="w-3 h-3" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-6 h-6 p-0 text-destructive"
|
||||
onclick={() => removeSchema(id)}
|
||||
>
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if description}
|
||||
<p class="text-xs text-muted-foreground mt-1">{description}</p>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
|
||||
{#if hasChildren && isExpanded}
|
||||
<CardContent class="pt-0 pb-3">
|
||||
{#each children as child}
|
||||
<SchemaNode {...child} level={level + 1} />
|
||||
{/each}
|
||||
</CardContent>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
163
src/lib/components/schema/SchemaTypes.svelte
Normal file
163
src/lib/components/schema/SchemaTypes.svelte
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Braces,
|
||||
List,
|
||||
Type,
|
||||
Hash,
|
||||
ToggleLeft,
|
||||
ListOrdered,
|
||||
Globe,
|
||||
Calendar,
|
||||
Mail,
|
||||
Link
|
||||
} from 'lucide-svelte';
|
||||
|
||||
const schemaTypes = [
|
||||
{
|
||||
type: 'object',
|
||||
icon: Braces,
|
||||
label: 'Object',
|
||||
description: 'Complex type with properties',
|
||||
color: 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
icon: List,
|
||||
label: 'Array',
|
||||
description: 'List of items',
|
||||
color: 'bg-green-500/10 text-green-600 border-green-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
icon: Type,
|
||||
label: 'String',
|
||||
description: 'Text value',
|
||||
color: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
icon: Hash,
|
||||
label: 'Number',
|
||||
description: 'Numeric value',
|
||||
color: 'bg-purple-500/10 text-purple-600 border-purple-500/20'
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
icon: Hash,
|
||||
label: 'Integer',
|
||||
description: 'Whole number',
|
||||
color: 'bg-purple-500/10 text-purple-600 border-purple-500/20'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
icon: ToggleLeft,
|
||||
label: 'Boolean',
|
||||
description: 'True or false',
|
||||
color: 'bg-red-500/10 text-red-600 border-red-500/20'
|
||||
},
|
||||
{
|
||||
type: 'enum',
|
||||
icon: ListOrdered,
|
||||
label: 'Enum',
|
||||
description: 'Fixed set of values',
|
||||
color: 'bg-orange-500/10 text-orange-600 border-orange-500/20'
|
||||
}
|
||||
];
|
||||
|
||||
const formatTypes = [
|
||||
{
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
icon: Calendar,
|
||||
label: 'Date',
|
||||
description: 'Date format (YYYY-MM-DD)',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
icon: Mail,
|
||||
label: 'Email',
|
||||
description: 'Email address format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
icon: Link,
|
||||
label: 'URI',
|
||||
description: 'URI/URL format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
icon: Globe,
|
||||
label: 'UUID',
|
||||
description: 'UUID format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
}
|
||||
];
|
||||
|
||||
function handleDragStart(event: DragEvent, schemaType: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('schema-type', schemaType.type);
|
||||
if (schemaType.format) {
|
||||
event.dataTransfer.setData('schema-format', schemaType.format);
|
||||
}
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormatDragStart(event: DragEvent, formatType: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('schema-type', formatType.type);
|
||||
event.dataTransfer.setData('schema-format', formatType.format);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">Basic Types</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each schemaTypes as schemaType}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleDragStart(e, schemaType)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<schemaType.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{schemaType.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{schemaType.description}</p>
|
||||
<Badge class="text-xs mt-1 {schemaType.color}">{schemaType.type}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">Format Types</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each formatTypes as formatType}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleFormatDragStart(e, formatType)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<formatType.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{formatType.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{formatType.description}</p>
|
||||
<Badge class="text-xs mt-1 {formatType.color}">{formatType.format}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
175
src/lib/components/schema/VersionAwareAPIComponents.svelte
Normal file
175
src/lib/components/schema/VersionAwareAPIComponents.svelte
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Globe,
|
||||
Send,
|
||||
Download,
|
||||
Upload,
|
||||
Trash,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
Key,
|
||||
Shield,
|
||||
FileText,
|
||||
Webhook,
|
||||
Link2,
|
||||
AlertTriangle
|
||||
} from 'lucide-svelte';
|
||||
import { schemaStore } from '$lib/stores/schema';
|
||||
import { getVersionConfig, isFeatureSupported } from '$lib/utils/openapi-versions';
|
||||
|
||||
let versionConfig = $derived(getVersionConfig($schemaStore.openApiVersion));
|
||||
|
||||
const httpMethods = [
|
||||
{ method: 'GET', color: 'bg-green-500 text-white', icon: Download },
|
||||
{ method: 'POST', color: 'bg-blue-500 text-white', icon: Upload },
|
||||
{ method: 'PUT', color: 'bg-orange-500 text-white', icon: Send },
|
||||
{ method: 'PATCH', color: 'bg-yellow-500 text-white', icon: Settings },
|
||||
{ method: 'DELETE', color: 'bg-red-500 text-white', icon: Trash }
|
||||
];
|
||||
|
||||
const allApiComponents = [
|
||||
{
|
||||
type: 'parameter',
|
||||
icon: Key,
|
||||
label: 'Parameter',
|
||||
description: 'Query, path, or header parameter',
|
||||
color: 'bg-indigo-500/10 text-indigo-600 border-indigo-500/20',
|
||||
requiredFeature: null
|
||||
},
|
||||
{
|
||||
type: 'response',
|
||||
icon: ArrowRight,
|
||||
label: 'Response',
|
||||
description: 'HTTP response definition',
|
||||
color: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20',
|
||||
requiredFeature: null
|
||||
},
|
||||
{
|
||||
type: 'security',
|
||||
icon: Shield,
|
||||
label: 'Security',
|
||||
description: 'Authentication scheme',
|
||||
color: 'bg-rose-500/10 text-rose-600 border-rose-500/20',
|
||||
requiredFeature: null
|
||||
},
|
||||
{
|
||||
type: 'schema',
|
||||
icon: FileText,
|
||||
label: 'Schema',
|
||||
description: 'Reusable schema definition',
|
||||
color: 'bg-violet-500/10 text-violet-600 border-violet-500/20',
|
||||
requiredFeature: null
|
||||
},
|
||||
{
|
||||
type: 'callback',
|
||||
icon: Webhook,
|
||||
label: 'Callback',
|
||||
description: 'Callback definition (3.0+)',
|
||||
color: 'bg-teal-500/10 text-teal-600 border-teal-500/20',
|
||||
requiredFeature: 'callbacks'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
icon: Link2,
|
||||
label: 'Link',
|
||||
description: 'Response link (3.0+)',
|
||||
color: 'bg-pink-500/10 text-pink-600 border-pink-500/20',
|
||||
requiredFeature: 'links'
|
||||
},
|
||||
{
|
||||
type: 'webhook',
|
||||
icon: Webhook,
|
||||
label: 'Webhook',
|
||||
description: 'Webhook definition (3.1.0 only)',
|
||||
color: 'bg-amber-500/10 text-amber-600 border-amber-500/20',
|
||||
requiredFeature: 'webhooks'
|
||||
}
|
||||
];
|
||||
|
||||
// Filter components based on version support
|
||||
let supportedApiComponents = $derived(allApiComponents.filter(component =>
|
||||
!component.requiredFeature || isFeatureSupported($schemaStore.openApiVersion, component.requiredFeature as any)
|
||||
));
|
||||
|
||||
function handleMethodDragStart(event: DragEvent, method: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('api-method', method.method);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
|
||||
function handleComponentDragStart(event: DragEvent, component: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('api-component', component.type);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">HTTP Methods</h3>
|
||||
<div class="space-y-2">
|
||||
{#each httpMethods as method}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleMethodDragStart(e, method)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge class="text-xs {method.color} font-mono">{method.method}</Badge>
|
||||
<method.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">Endpoint</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">/{method.method.toLowerCase()}/resource</p>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">API Components</h3>
|
||||
<div class="space-y-2">
|
||||
{#each supportedApiComponents as component}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleComponentDragStart(e, component)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<component.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{component.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{component.description}</p>
|
||||
<Badge class="text-xs mt-1 {component.color}">{component.type}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version-specific warnings -->
|
||||
{#if $schemaStore.openApiVersion === '2.0'}
|
||||
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-600" />
|
||||
<span class="text-xs font-medium text-yellow-600">Swagger 2.0 Limitations</span>
|
||||
</div>
|
||||
<p class="text-xs text-yellow-600">Some modern OpenAPI features like callbacks, links, and webhooks are not available in Swagger 2.0.</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Security schemes info -->
|
||||
<div class="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<Shield class="w-4 h-4 text-blue-600" />
|
||||
<span class="text-xs font-medium text-blue-600">Supported Security</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
{#each versionConfig.securitySchemes as scheme}
|
||||
<Badge variant="outline" class="text-xs">{scheme}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
232
src/lib/components/schema/VersionAwareSchemaTypes.svelte
Normal file
232
src/lib/components/schema/VersionAwareSchemaTypes.svelte
Normal file
@@ -0,0 +1,232 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Braces,
|
||||
List,
|
||||
Type,
|
||||
Hash,
|
||||
ToggleLeft,
|
||||
ListOrdered,
|
||||
Globe,
|
||||
Calendar,
|
||||
Mail,
|
||||
Link,
|
||||
AlertTriangle
|
||||
} from 'lucide-svelte';
|
||||
import { schemaStore } from '$lib/stores/schema';
|
||||
import { getVersionConfig, isTypeSupported, isFormatSupported } from '$lib/utils/openapi-versions';
|
||||
|
||||
let versionConfig = $derived(getVersionConfig($schemaStore.openApiVersion));
|
||||
|
||||
const allSchemaTypes = [
|
||||
{
|
||||
type: 'object',
|
||||
icon: Braces,
|
||||
label: 'Object',
|
||||
description: 'Complex type with properties',
|
||||
color: 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
icon: List,
|
||||
label: 'Array',
|
||||
description: 'List of items',
|
||||
color: 'bg-green-500/10 text-green-600 border-green-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
icon: Type,
|
||||
label: 'String',
|
||||
description: 'Text value',
|
||||
color: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
icon: Hash,
|
||||
label: 'Number',
|
||||
description: 'Numeric value',
|
||||
color: 'bg-purple-500/10 text-purple-600 border-purple-500/20'
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
icon: Hash,
|
||||
label: 'Integer',
|
||||
description: 'Whole number',
|
||||
color: 'bg-purple-500/10 text-purple-600 border-purple-500/20'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
icon: ToggleLeft,
|
||||
label: 'Boolean',
|
||||
description: 'True or false',
|
||||
color: 'bg-red-500/10 text-red-600 border-red-500/20'
|
||||
},
|
||||
{
|
||||
type: 'null',
|
||||
icon: AlertTriangle,
|
||||
label: 'Null',
|
||||
description: 'Null value (3.1.0 only)',
|
||||
color: 'bg-gray-500/10 text-gray-600 border-gray-500/20'
|
||||
}
|
||||
];
|
||||
|
||||
const allFormatTypes = [
|
||||
{
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
icon: Calendar,
|
||||
label: 'Date',
|
||||
description: 'Date format (YYYY-MM-DD)',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
icon: Calendar,
|
||||
label: 'DateTime',
|
||||
description: 'Date-time format (RFC3339)',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'time',
|
||||
icon: Calendar,
|
||||
label: 'Time',
|
||||
description: 'Time format (3.1.0 only)',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
icon: Mail,
|
||||
label: 'Email',
|
||||
description: 'Email address format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
icon: Link,
|
||||
label: 'URI',
|
||||
description: 'URI/URL format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
icon: Globe,
|
||||
label: 'UUID',
|
||||
description: 'UUID format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'hostname',
|
||||
icon: Globe,
|
||||
label: 'Hostname',
|
||||
description: 'Hostname format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'ipv4',
|
||||
icon: Globe,
|
||||
label: 'IPv4',
|
||||
description: 'IPv4 address format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
format: 'ipv6',
|
||||
icon: Globe,
|
||||
label: 'IPv6',
|
||||
description: 'IPv6 address format',
|
||||
color: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20'
|
||||
}
|
||||
];
|
||||
|
||||
// Filter types and formats based on version support
|
||||
let supportedSchemaTypes = $derived(allSchemaTypes.filter(type =>
|
||||
isTypeSupported($schemaStore.openApiVersion, type.type)
|
||||
));
|
||||
|
||||
let supportedFormatTypes = $derived(allFormatTypes.filter(formatType =>
|
||||
isFormatSupported($schemaStore.openApiVersion, formatType.type, formatType.format)
|
||||
));
|
||||
|
||||
function handleDragStart(event: DragEvent, schemaType: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('schema-type', schemaType.type);
|
||||
if (schemaType.format) {
|
||||
event.dataTransfer.setData('schema-format', schemaType.format);
|
||||
}
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormatDragStart(event: DragEvent, formatType: any) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('schema-type', formatType.type);
|
||||
event.dataTransfer.setData('schema-format', formatType.format);
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">Basic Types</h3>
|
||||
<Badge variant="outline" class="text-xs">{versionConfig.displayName}</Badge>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each supportedSchemaTypes as schemaType}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleDragStart(e, schemaType)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<schemaType.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{schemaType.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{schemaType.description}</p>
|
||||
<Badge class="text-xs mt-1 {schemaType.color}">{schemaType.type}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if supportedFormatTypes.length > 0}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground mb-3">Format Types</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each supportedFormatTypes as formatType}
|
||||
<Card
|
||||
class="p-3 cursor-grab hover:bg-accent/50 transition-colors border-dashed"
|
||||
draggable="true"
|
||||
ondragstart={(e) => handleFormatDragStart(e, formatType)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<formatType.icon class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium">{formatType.label}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{formatType.description}</p>
|
||||
<Badge class="text-xs mt-1 {formatType.color}">{formatType.format}</Badge>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !versionConfig.features.nullable}
|
||||
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-600" />
|
||||
<span class="text-xs font-medium text-yellow-600">Version Limitation</span>
|
||||
</div>
|
||||
<p class="text-xs text-yellow-600">Swagger 2.0 doesn't support nullable types. Use optional properties instead.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
57
src/lib/components/structure/ImportDialog.svelte
Normal file
57
src/lib/components/structure/ImportDialog.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { schemaStore, replaceSchemas, replaceEndpoints, setInfo, addServer } from "$lib/stores/schema";
|
||||
|
||||
let { open, onClose }: { open: boolean; onClose: () => void } = $props();
|
||||
|
||||
let text = $state('');
|
||||
|
||||
function importJson() {
|
||||
try {
|
||||
const spec = JSON.parse(text);
|
||||
if (spec.info) setInfo(spec.info);
|
||||
if (spec.servers && Array.isArray(spec.servers)) {
|
||||
// reset then add first server for simplicity
|
||||
}
|
||||
// minimal parse: we only map paths to endpoints
|
||||
const endpoints: any[] = [];
|
||||
if (spec.paths) {
|
||||
for (const p of Object.keys(spec.paths)) {
|
||||
const item = spec.paths[p];
|
||||
for (const m of Object.keys(item)) {
|
||||
const op = item[m];
|
||||
endpoints.push({ method: m.toUpperCase(), path: p, summary: op.summary || '', description: op.description || '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
replaceEndpoints(endpoints as any);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
// ignore for now
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-background border rounded-lg shadow-lg max-w-3xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="p-4 border-b flex items-center justify-between">
|
||||
<div class="text-sm font-medium">Import OpenAPI JSON</div>
|
||||
<Button variant="ghost" size="sm" onclick={onClose}>Close</Button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-4 space-y-3">
|
||||
<Textarea rows={16} bind:value={text} placeholder="Paste OpenAPI JSON here" />
|
||||
</div>
|
||||
<div class="p-4 border-t flex justify-end">
|
||||
<Button onclick={importJson}>Import</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
37
src/lib/components/structure/InfoEditor.svelte
Normal file
37
src/lib/components/structure/InfoEditor.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { schemaStore, setInfo } from "$lib/stores/schema";
|
||||
|
||||
let title = $state($schemaStore.info.title);
|
||||
let version = $state($schemaStore.info.version);
|
||||
let description = $state($schemaStore.info.description ?? "");
|
||||
|
||||
function commit() {
|
||||
setInfo({ title, version, description });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">API Info</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium" for="info-title">Title</label>
|
||||
<Input id="info-title" bind:value={title} onblur={commit} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium" for="info-version">Version</label>
|
||||
<Input id="info-version" bind:value={version} onblur={commit} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium" for="info-description">Description</label>
|
||||
<Textarea id="info-description" bind:value={description} rows={3} onblur={commit} />
|
||||
</div>
|
||||
</CardContent>
|
||||
<style></style>
|
||||
</Card>
|
||||
|
||||
|
||||
48
src/lib/components/structure/PathsEditor.svelte
Normal file
48
src/lib/components/structure/PathsEditor.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { schemaStore, addEndpoint } from "$lib/stores/schema";
|
||||
|
||||
let method = $state<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>('GET');
|
||||
let path = $state('/resource');
|
||||
let summary = $state('');
|
||||
|
||||
function addPath() {
|
||||
addEndpoint({ method, path, summary, description: '' , x: 0, y: 0});
|
||||
path = '/resource';
|
||||
summary = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">Paths</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-2 items-center">
|
||||
<select class="border rounded p-2" bind:value={method}>
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
<option>PUT</option>
|
||||
<option>PATCH</option>
|
||||
<option>DELETE</option>
|
||||
</select>
|
||||
<Input class="md:col-span-3" bind:value={path} />
|
||||
<Input class="md:col-span-2" bind:value={summary} placeholder="Summary" />
|
||||
<Button variant="outline" onclick={addPath}>Add</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each $schemaStore.endpoints as e}
|
||||
<div class="flex items-center justify-between border rounded p-2">
|
||||
<div class="text-sm font-medium"><span class="mr-2">{e.method}</span>{e.path}</div>
|
||||
<div class="text-xs text-muted-foreground">{e.summary}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardContent>
|
||||
<style></style>
|
||||
</Card>
|
||||
|
||||
|
||||
76
src/lib/components/structure/SecurityEditor.svelte
Normal file
76
src/lib/components/structure/SecurityEditor.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { schemaStore, addSecurityScheme, updateSecurityScheme, removeSecurityScheme, setSecurity } from "$lib/stores/schema";
|
||||
|
||||
let newKey = $state('auth');
|
||||
let type = $state<'http' | 'apiKey' | 'oauth2'>('http');
|
||||
let scheme = $state<'bearer' | 'basic'>('bearer');
|
||||
let apiKeyName = $state('X-API-Key');
|
||||
let apiKeyIn = $state<'query' | 'header' | 'cookie'>('header');
|
||||
|
||||
function addScheme() {
|
||||
if (type === 'http') {
|
||||
addSecurityScheme(newKey, { type: 'http', scheme });
|
||||
} else if (type === 'apiKey') {
|
||||
addSecurityScheme(newKey, { type: 'apiKey', name: apiKeyName, in: apiKeyIn });
|
||||
} else {
|
||||
addSecurityScheme(newKey, { type: 'oauth2', flows: {} });
|
||||
}
|
||||
}
|
||||
|
||||
function toggleGlobal(key: string) {
|
||||
const current = $schemaStore.security;
|
||||
const exists = current.find((r) => key in r);
|
||||
if (exists) {
|
||||
setSecurity(current.filter((r) => !(key in r)));
|
||||
} else {
|
||||
setSecurity([...current, { [key]: [] }]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-sm">Security Schemes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-5 items-center">
|
||||
<Input placeholder="scheme key" bind:value={newKey} />
|
||||
<select class="border rounded p-2" bind:value={type}>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="apiKey">API Key</option>
|
||||
<option value="oauth2">OAuth2</option>
|
||||
</select>
|
||||
{#if type === 'http'}
|
||||
<select class="border rounded p-2" bind:value={scheme}>
|
||||
<option value="bearer">bearer</option>
|
||||
<option value="basic">basic</option>
|
||||
</select>
|
||||
{:else if type === 'apiKey'}
|
||||
<Input placeholder="name" bind:value={apiKeyName} />
|
||||
<select class="border rounded p-2" bind:value={apiKeyIn}>
|
||||
<option value="header">header</option>
|
||||
<option value="query">query</option>
|
||||
<option value="cookie">cookie</option>
|
||||
</select>
|
||||
{/if}
|
||||
<Button variant="outline" onclick={addScheme}>Add</Button>
|
||||
</div>
|
||||
|
||||
{#each Object.entries($schemaStore.securitySchemes) as [key, value]}
|
||||
<div class="flex items-center justify-between border rounded p-2">
|
||||
<div class="text-sm font-medium">{key}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onclick={() => removeSecurityScheme(key)}>Remove</Button>
|
||||
<Button size="sm" variant="outline" class={($schemaStore.security.find((r)=>key in r) ? 'bg-primary/10' : '')} onclick={() => toggleGlobal(key)}>Global</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
43
src/lib/components/structure/ServersEditor.svelte
Normal file
43
src/lib/components/structure/ServersEditor.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card";
|
||||
import { schemaStore, addServer, updateServer, removeServer } from "$lib/stores/schema";
|
||||
|
||||
function add() {
|
||||
addServer({ url: "https://api.example.com/v1", description: "New server" });
|
||||
}
|
||||
|
||||
function updateUrl(i: number, value: string) {
|
||||
const current = $schemaStore.servers[i];
|
||||
updateServer(i, { ...current, url: value });
|
||||
}
|
||||
|
||||
function updateDesc(i: number, value: string) {
|
||||
const current = $schemaStore.servers[i];
|
||||
updateServer(i, { ...current, description: value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-sm">Servers</CardTitle>
|
||||
<Button size="sm" variant="outline" onclick={add}>Add</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
{#each $schemaStore.servers as server, i}
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-5 items-center">
|
||||
<Input class="md:col-span-3" value={server.url} oninput={(event) => updateUrl(i, (event.currentTarget as HTMLInputElement).value)} />
|
||||
<Input class="md:col-span-2" value={server.description || ''} oninput={(event) => updateDesc(i, (event.currentTarget as HTMLInputElement).value)} />
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" variant="outline" class="text-destructive" onclick={() => removeServer(i)}>Remove</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</CardContent>
|
||||
<style></style>
|
||||
</Card>
|
||||
|
||||
|
||||
68
src/lib/components/ui/badge/badge.svelte
Normal file
68
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes, HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type AnchorOrSpanProps = WithElementRef<HTMLAnchorAttributes> &
|
||||
WithElementRef<HTMLAttributes<HTMLSpanElement>> & {
|
||||
href?: string;
|
||||
class?: string;
|
||||
variant?: BadgeVariant;
|
||||
};
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: AnchorOrSpanProps = $props();
|
||||
|
||||
const { class: _class, ...rest } = restProps as Record<string, any>;
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
href={href}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
{/if}
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
82
src/lib/components/ui/button/button.svelte
Normal file
82
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
disabled,
|
||||
type = "button",
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
|
||||
const { class: _class, ...rest } = restProps as Record<string, any>;
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card.svelte
Normal file
23
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
51
src/lib/components/ui/input/input.svelte
Normal file
51
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
src/lib/components/ui/label/label.svelte
Normal file
20
src/lib/components/ui/label/label.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
20
src/lib/components/ui/separator/separator.svelte
Normal file
20
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/switch/index.ts
Normal file
7
src/lib/components/ui/switch/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./switch.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch,
|
||||
};
|
||||
29
src/lib/components/ui/switch/switch.svelte
Normal file
29
src/lib/components/ui/switch/switch.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
data-slot="switch"
|
||||
class={cn(
|
||||
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
class={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
7
src/lib/components/ui/textarea/index.ts
Normal file
7
src/lib/components/ui/textarea/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
22
src/lib/components/ui/textarea/textarea.svelte
Normal file
22
src/lib/components/ui/textarea/textarea.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot="textarea"
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
></textarea>
|
||||
209
src/lib/stores/schema.ts
Normal file
209
src/lib/stores/schema.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export interface SchemaItem {
|
||||
id: string
|
||||
type: "object" | "array" | "string" | "number" | "integer" | "boolean" | "enum" | "null"
|
||||
name: string
|
||||
required?: boolean
|
||||
description?: string
|
||||
format?: string
|
||||
children?: SchemaItem[]
|
||||
properties?: {
|
||||
nullable?: boolean
|
||||
readOnly?: boolean
|
||||
writeOnly?: boolean
|
||||
deprecated?: boolean
|
||||
example?: any
|
||||
examples?: any[]
|
||||
default?: any
|
||||
// String constraints
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
pattern?: string
|
||||
// Numeric constraints
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
exclusiveMinimum?: boolean
|
||||
exclusiveMaximum?: boolean
|
||||
multipleOf?: number
|
||||
// Array constraints
|
||||
minItems?: number
|
||||
maxItems?: number
|
||||
uniqueItems?: boolean
|
||||
// Object constraints
|
||||
minProperties?: number
|
||||
maxProperties?: number
|
||||
additionalProperties?: boolean | SchemaItem
|
||||
// Enum values
|
||||
enum?: string[]
|
||||
// Composition
|
||||
oneOf?: SchemaItem[]
|
||||
anyOf?: SchemaItem[]
|
||||
allOf?: SchemaItem[]
|
||||
not?: SchemaItem
|
||||
// Discriminator
|
||||
discriminator?: {
|
||||
propertyName: string
|
||||
mapping?: Record<string, string>
|
||||
}
|
||||
}
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
export interface APIEndpoint {
|
||||
id: string
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
||||
path: string
|
||||
summary?: string
|
||||
description?: string
|
||||
parameters?: any[]
|
||||
responses?: Record<string, any>
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
export interface ApiInfo {
|
||||
title: string
|
||||
version: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
url: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type SecurityScheme =
|
||||
| { type: "http"; scheme: "bearer" | "basic"; bearerFormat?: string; description?: string }
|
||||
| { type: "apiKey"; name: string; in: "query" | "header" | "cookie"; description?: string }
|
||||
| { type: "oauth2"; description?: string; flows: Record<string, any> }
|
||||
|
||||
export type SecurityRequirement = Record<string, string[]>
|
||||
|
||||
export interface SchemaState {
|
||||
schemas: SchemaItem[]
|
||||
endpoints: APIEndpoint[]
|
||||
selectedItem: SchemaItem | APIEndpoint | null
|
||||
openApiVersion: string
|
||||
info: ApiInfo
|
||||
servers: Server[]
|
||||
securitySchemes: Record<string, SecurityScheme>
|
||||
security: SecurityRequirement[]
|
||||
}
|
||||
|
||||
const initialState: SchemaState = {
|
||||
schemas: [],
|
||||
endpoints: [],
|
||||
selectedItem: null,
|
||||
openApiVersion: "3.1.0",
|
||||
info: { title: "My API", version: "1.0.0", description: "" },
|
||||
servers: [{ url: "https://api.example.com/v1", description: "Production" }],
|
||||
securitySchemes: {},
|
||||
security: [],
|
||||
}
|
||||
|
||||
export const schemaStore = writable<SchemaState>(initialState)
|
||||
|
||||
// Helper functions
|
||||
export function addSchema(schema: Omit<SchemaItem, "id">) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
schemas: [...state.schemas, { ...schema, id: crypto.randomUUID() }],
|
||||
}))
|
||||
}
|
||||
|
||||
export function addEndpoint(endpoint: Omit<APIEndpoint, "id">) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
endpoints: [...state.endpoints, { ...endpoint, id: crypto.randomUUID() }],
|
||||
}))
|
||||
}
|
||||
|
||||
export function selectItem(item: SchemaItem | APIEndpoint | null) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
selectedItem: item,
|
||||
}))
|
||||
}
|
||||
|
||||
export function updateSchema(id: string, updates: Partial<SchemaItem>) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
schemas: state.schemas.map((schema) => (schema.id === id ? { ...schema, ...updates } : schema)),
|
||||
selectedItem: state.selectedItem?.id === id ? { ...state.selectedItem, ...updates } : state.selectedItem,
|
||||
}))
|
||||
}
|
||||
|
||||
export function removeSchema(id: string) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
schemas: state.schemas.filter((schema) => schema.id !== id),
|
||||
selectedItem: state.selectedItem?.id === id ? null : state.selectedItem,
|
||||
}))
|
||||
}
|
||||
|
||||
export function replaceSchemas(schemas: SchemaItem[]) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
schemas,
|
||||
}))
|
||||
}
|
||||
|
||||
export function replaceEndpoints(endpoints: APIEndpoint[]) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
endpoints,
|
||||
}))
|
||||
}
|
||||
|
||||
// Info
|
||||
export function setInfo(info: Partial<ApiInfo>) {
|
||||
schemaStore.update((state) => ({ ...state, info: { ...state.info, ...info } }))
|
||||
}
|
||||
|
||||
// Servers
|
||||
export function addServer(server: Server) {
|
||||
schemaStore.update((state) => ({ ...state, servers: [...state.servers, server] }))
|
||||
}
|
||||
|
||||
export function updateServer(index: number, server: Server) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
servers: state.servers.map((s, i) => (i === index ? server : s)),
|
||||
}))
|
||||
}
|
||||
|
||||
export function removeServer(index: number) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
servers: state.servers.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
// Security Schemes and Requirements
|
||||
export function addSecurityScheme(key: string, scheme: SecurityScheme) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
securitySchemes: { ...state.securitySchemes, [key]: scheme },
|
||||
}))
|
||||
}
|
||||
|
||||
export function updateSecurityScheme(key: string, scheme: SecurityScheme) {
|
||||
schemaStore.update((state) => ({
|
||||
...state,
|
||||
securitySchemes: { ...state.securitySchemes, [key]: scheme },
|
||||
}))
|
||||
}
|
||||
|
||||
export function removeSecurityScheme(key: string) {
|
||||
schemaStore.update((state) => {
|
||||
const next = { ...state.securitySchemes }
|
||||
delete next[key]
|
||||
return { ...state, securitySchemes: next }
|
||||
})
|
||||
}
|
||||
|
||||
export function setSecurity(requirements: SecurityRequirement[]) {
|
||||
schemaStore.update((state) => ({ ...state, security: requirements }))
|
||||
}
|
||||
13
src/lib/utils.ts
Normal file
13
src/lib/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(...inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
267
src/lib/utils/openapi-generator.ts
Normal file
267
src/lib/utils/openapi-generator.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import type { SchemaItem, APIEndpoint, SchemaState } from "$lib/stores/schema"
|
||||
import { getVersionConfig } from "./openapi-versions"
|
||||
|
||||
export interface OpenAPISpec {
|
||||
openapi?: string
|
||||
swagger?: string
|
||||
info: {
|
||||
title: string
|
||||
version: string
|
||||
description?: string
|
||||
}
|
||||
servers?: Array<{
|
||||
url: string
|
||||
description?: string
|
||||
}>
|
||||
paths: Record<string, any>
|
||||
components?: {
|
||||
schemas?: Record<string, any>
|
||||
parameters?: Record<string, any>
|
||||
responses?: Record<string, any>
|
||||
securitySchemes?: Record<string, any>
|
||||
}
|
||||
webhooks?: Record<string, any>
|
||||
}
|
||||
|
||||
export function generateOpenAPISpec(state: SchemaState): OpenAPISpec {
|
||||
const versionConfig = getVersionConfig(state.openApiVersion)
|
||||
|
||||
const spec: OpenAPISpec = {
|
||||
info: {
|
||||
title: state.info.title,
|
||||
version: state.info.version,
|
||||
description: state.info.description,
|
||||
},
|
||||
paths: {},
|
||||
}
|
||||
|
||||
// Set version field based on OpenAPI version
|
||||
if (state.openApiVersion === "2.0") {
|
||||
spec.swagger = "2.0"
|
||||
} else {
|
||||
spec.openapi = state.openApiVersion
|
||||
}
|
||||
|
||||
// Servers
|
||||
if (state.openApiVersion !== "2.0") {
|
||||
if (state.servers && state.servers.length > 0) {
|
||||
spec.servers = state.servers.map((s) => ({ url: s.url, description: s.description }))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
if (state.openApiVersion !== "2.0") {
|
||||
spec.components = {
|
||||
schemas: {},
|
||||
parameters: {},
|
||||
responses: {},
|
||||
securitySchemes: { ...state.securitySchemes },
|
||||
}
|
||||
}
|
||||
|
||||
// Convert schema items to OpenAPI schema definitions
|
||||
state.schemas.forEach((schema) => {
|
||||
const schemaDefinition = convertSchemaToOpenAPI(schema, state.openApiVersion)
|
||||
if (state.openApiVersion === "2.0") {
|
||||
// Swagger 2.0 uses definitions instead of components/schemas
|
||||
if (!spec.definitions) spec.definitions = {}
|
||||
spec.definitions[schema.name] = schemaDefinition
|
||||
} else {
|
||||
spec.components!.schemas![schema.name] = schemaDefinition
|
||||
}
|
||||
})
|
||||
|
||||
// Convert endpoints to paths
|
||||
state.endpoints.forEach((endpoint) => {
|
||||
const pathItem = convertEndpointToPath(endpoint, state.openApiVersion)
|
||||
if (!spec.paths[endpoint.path]) {
|
||||
spec.paths[endpoint.path] = {}
|
||||
}
|
||||
spec.paths[endpoint.path][endpoint.method.toLowerCase()] = pathItem
|
||||
})
|
||||
|
||||
// Top-level security requirements (OpenAPI 3.x)
|
||||
if (state.openApiVersion !== "2.0" && state.security && state.security.length > 0) {
|
||||
;(spec as any).security = state.security
|
||||
}
|
||||
|
||||
// Add webhooks for OpenAPI 3.1.0
|
||||
if (state.openApiVersion === "3.1.0" && versionConfig.features.webhooks) {
|
||||
spec.webhooks = {}
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
function convertSchemaToOpenAPI(schema: SchemaItem, version: string): any {
|
||||
const openApiSchema: any = {
|
||||
type: schema.type === "null" ? undefined : schema.type,
|
||||
}
|
||||
|
||||
// Handle null type for OpenAPI 3.1.0
|
||||
if (schema.type === "null" && version === "3.1.0") {
|
||||
openApiSchema.type = "null"
|
||||
}
|
||||
|
||||
if (schema.description) {
|
||||
openApiSchema.description = schema.description
|
||||
}
|
||||
|
||||
if (schema.format) {
|
||||
openApiSchema.format = schema.format
|
||||
}
|
||||
|
||||
// Add properties from the properties object
|
||||
if (schema.properties) {
|
||||
const props = schema.properties
|
||||
|
||||
if (props.nullable && version !== "2.0") {
|
||||
if (version === "3.1.0") {
|
||||
openApiSchema.type = [openApiSchema.type, "null"]
|
||||
} else {
|
||||
openApiSchema.nullable = true
|
||||
}
|
||||
}
|
||||
|
||||
if (props.readOnly) openApiSchema.readOnly = props.readOnly
|
||||
if (props.writeOnly) openApiSchema.writeOnly = props.writeOnly
|
||||
if (props.deprecated) openApiSchema.deprecated = props.deprecated
|
||||
if (props.example !== undefined) openApiSchema.example = props.example
|
||||
if (props.default !== undefined) openApiSchema.default = props.default
|
||||
|
||||
// String constraints
|
||||
if (props.minLength !== undefined) openApiSchema.minLength = props.minLength
|
||||
if (props.maxLength !== undefined) openApiSchema.maxLength = props.maxLength
|
||||
if (props.pattern) openApiSchema.pattern = props.pattern
|
||||
|
||||
// Numeric constraints
|
||||
if (props.minimum !== undefined) openApiSchema.minimum = props.minimum
|
||||
if (props.maximum !== undefined) openApiSchema.maximum = props.maximum
|
||||
if (props.exclusiveMinimum !== undefined) openApiSchema.exclusiveMinimum = props.exclusiveMinimum
|
||||
if (props.exclusiveMaximum !== undefined) openApiSchema.exclusiveMaximum = props.exclusiveMaximum
|
||||
if (props.multipleOf !== undefined) openApiSchema.multipleOf = props.multipleOf
|
||||
|
||||
// Array constraints
|
||||
if (props.minItems !== undefined) openApiSchema.minItems = props.minItems
|
||||
if (props.maxItems !== undefined) openApiSchema.maxItems = props.maxItems
|
||||
if (props.uniqueItems !== undefined) openApiSchema.uniqueItems = props.uniqueItems
|
||||
|
||||
// Object constraints
|
||||
if (props.minProperties !== undefined) openApiSchema.minProperties = props.minProperties
|
||||
if (props.maxProperties !== undefined) openApiSchema.maxProperties = props.maxProperties
|
||||
if (props.additionalProperties !== undefined) openApiSchema.additionalProperties = props.additionalProperties
|
||||
|
||||
// Enum values
|
||||
if (props.enum && props.enum.length > 0) {
|
||||
openApiSchema.enum = props.enum
|
||||
}
|
||||
|
||||
// Composition keywords
|
||||
if (props.oneOf) openApiSchema.oneOf = props.oneOf.map((s) => convertSchemaToOpenAPI(s, version))
|
||||
if (props.anyOf) openApiSchema.anyOf = props.anyOf.map((s) => convertSchemaToOpenAPI(s, version))
|
||||
if (props.allOf) openApiSchema.allOf = props.allOf.map((s) => convertSchemaToOpenAPI(s, version))
|
||||
if (props.not) openApiSchema.not = convertSchemaToOpenAPI(props.not, version)
|
||||
|
||||
// Discriminator
|
||||
if (props.discriminator) openApiSchema.discriminator = props.discriminator
|
||||
}
|
||||
|
||||
// Handle object properties
|
||||
if (schema.type === "object" && schema.children && schema.children.length > 0) {
|
||||
openApiSchema.properties = {}
|
||||
openApiSchema.required = []
|
||||
|
||||
schema.children.forEach((child) => {
|
||||
openApiSchema.properties[child.name] = convertSchemaToOpenAPI(child, version)
|
||||
if (child.required) {
|
||||
openApiSchema.required.push(child.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (openApiSchema.required.length === 0) {
|
||||
delete openApiSchema.required
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array items
|
||||
if (schema.type === "array" && schema.children && schema.children.length > 0) {
|
||||
openApiSchema.items = convertSchemaToOpenAPI(schema.children[0], version)
|
||||
}
|
||||
|
||||
return openApiSchema
|
||||
}
|
||||
|
||||
function convertEndpointToPath(endpoint: APIEndpoint, version: string): any {
|
||||
const operation: any = {}
|
||||
|
||||
if (endpoint.summary) operation.summary = endpoint.summary
|
||||
if (endpoint.description) operation.description = endpoint.description
|
||||
|
||||
// Add default response
|
||||
operation.responses = {
|
||||
"200": {
|
||||
description: "Successful response",
|
||||
},
|
||||
}
|
||||
|
||||
// Add parameters if any
|
||||
if (endpoint.parameters && endpoint.parameters.length > 0) {
|
||||
operation.parameters = endpoint.parameters
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
export function validateOpenAPISpec(
|
||||
spec: OpenAPISpec,
|
||||
): Array<{ type: "error" | "warning"; message: string; path?: string }> {
|
||||
const issues: Array<{ type: "error" | "warning"; message: string; path?: string }> = []
|
||||
|
||||
// Basic validation
|
||||
if (!spec.info?.title) {
|
||||
issues.push({ type: "error", message: "API title is required", path: "info.title" })
|
||||
}
|
||||
|
||||
if (!spec.info?.version) {
|
||||
issues.push({ type: "error", message: "API version is required", path: "info.version" })
|
||||
}
|
||||
|
||||
// Validate paths
|
||||
if (Object.keys(spec.paths).length === 0) {
|
||||
issues.push({ type: "warning", message: "No API paths defined" })
|
||||
}
|
||||
|
||||
// Check for empty paths
|
||||
Object.entries(spec.paths).forEach(([path, pathItem]) => {
|
||||
if (!pathItem || Object.keys(pathItem).length === 0) {
|
||||
issues.push({ type: "warning", message: `Path "${path}" has no operations defined`, path: `paths.${path}` })
|
||||
}
|
||||
})
|
||||
|
||||
// Validate schemas
|
||||
if (spec.components?.schemas) {
|
||||
Object.entries(spec.components.schemas).forEach(([name, schema]) => {
|
||||
if (!schema.type && !schema.oneOf && !schema.anyOf && !schema.allOf) {
|
||||
issues.push({
|
||||
type: "warning",
|
||||
message: `Schema "${name}" has no type defined`,
|
||||
path: `components.schemas.${name}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
export function downloadOpenAPISpec(spec: OpenAPISpec, filename = "openapi-spec.json") {
|
||||
const blob = new Blob([JSON.stringify(spec, null, 2)], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
123
src/lib/utils/openapi-versions.ts
Normal file
123
src/lib/utils/openapi-versions.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
export interface OpenAPIVersionConfig {
|
||||
version: string
|
||||
displayName: string
|
||||
supportedTypes: string[]
|
||||
supportedFormats: Record<string, string[]>
|
||||
features: {
|
||||
nullable: boolean
|
||||
discriminator: boolean
|
||||
oneOf: boolean
|
||||
anyOf: boolean
|
||||
allOf: boolean
|
||||
examples: boolean
|
||||
callbacks: boolean
|
||||
links: boolean
|
||||
webhooks: boolean
|
||||
}
|
||||
securitySchemes: string[]
|
||||
}
|
||||
|
||||
export const OPENAPI_VERSIONS: Record<string, OpenAPIVersionConfig> = {
|
||||
"3.1.0": {
|
||||
version: "3.1.0",
|
||||
displayName: "OpenAPI 3.1.0",
|
||||
supportedTypes: ["null", "boolean", "object", "array", "number", "string", "integer"],
|
||||
supportedFormats: {
|
||||
string: [
|
||||
"date",
|
||||
"date-time",
|
||||
"time",
|
||||
"email",
|
||||
"hostname",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"uri",
|
||||
"uri-reference",
|
||||
"uuid",
|
||||
"regex",
|
||||
],
|
||||
number: ["float", "double"],
|
||||
integer: ["int32", "int64"],
|
||||
},
|
||||
features: {
|
||||
nullable: true,
|
||||
discriminator: true,
|
||||
oneOf: true,
|
||||
anyOf: true,
|
||||
allOf: true,
|
||||
examples: true,
|
||||
callbacks: true,
|
||||
links: true,
|
||||
webhooks: true,
|
||||
},
|
||||
securitySchemes: ["apiKey", "http", "oauth2", "openIdConnect", "mutualTLS"],
|
||||
},
|
||||
"3.0.3": {
|
||||
version: "3.0.3",
|
||||
displayName: "OpenAPI 3.0.3",
|
||||
supportedTypes: ["boolean", "object", "array", "number", "string", "integer"],
|
||||
supportedFormats: {
|
||||
string: ["date", "date-time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid", "byte", "binary", "password"],
|
||||
number: ["float", "double"],
|
||||
integer: ["int32", "int64"],
|
||||
},
|
||||
features: {
|
||||
nullable: true,
|
||||
discriminator: true,
|
||||
oneOf: true,
|
||||
anyOf: true,
|
||||
allOf: true,
|
||||
examples: true,
|
||||
callbacks: true,
|
||||
links: true,
|
||||
webhooks: false,
|
||||
},
|
||||
securitySchemes: ["apiKey", "http", "oauth2", "openIdConnect"],
|
||||
},
|
||||
"2.0": {
|
||||
version: "2.0",
|
||||
displayName: "Swagger 2.0",
|
||||
supportedTypes: ["boolean", "object", "array", "number", "string", "integer"],
|
||||
supportedFormats: {
|
||||
string: ["date", "date-time", "email", "hostname", "ipv4", "ipv6", "uri", "byte", "binary", "password"],
|
||||
number: ["float", "double"],
|
||||
integer: ["int32", "int64"],
|
||||
},
|
||||
features: {
|
||||
nullable: false,
|
||||
discriminator: true,
|
||||
oneOf: false,
|
||||
anyOf: false,
|
||||
allOf: true,
|
||||
examples: false,
|
||||
callbacks: false,
|
||||
links: false,
|
||||
webhooks: false,
|
||||
},
|
||||
securitySchemes: ["basic", "apiKey", "oauth2"],
|
||||
},
|
||||
}
|
||||
|
||||
export function getVersionConfig(version: string): OpenAPIVersionConfig {
|
||||
return OPENAPI_VERSIONS[version] || OPENAPI_VERSIONS["3.1.0"]
|
||||
}
|
||||
|
||||
export function isTypeSupported(version: string, type: string): boolean {
|
||||
const config = getVersionConfig(version)
|
||||
return config.supportedTypes.includes(type)
|
||||
}
|
||||
|
||||
export function isFormatSupported(version: string, type: string, format: string): boolean {
|
||||
const config = getVersionConfig(version)
|
||||
return config.supportedFormats[type]?.includes(format) || false
|
||||
}
|
||||
|
||||
export function isFeatureSupported(version: string, feature: keyof OpenAPIVersionConfig["features"]): boolean {
|
||||
const config = getVersionConfig(version)
|
||||
return config.features[feature]
|
||||
}
|
||||
|
||||
export function getAvailableFormats(version: string, type: string): string[] {
|
||||
const config = getVersionConfig(version)
|
||||
return config.supportedFormats[type] || []
|
||||
}
|
||||
10
src/routes/+layout.svelte
Normal file
10
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<!-- Added dark class to html for dark mode as primary theme -->
|
||||
<div class="dark min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
</div>
|
||||
179
src/routes/+page.svelte
Normal file
179
src/routes/+page.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Plus, FileText, Settings, Download, Upload, Info } from 'lucide-svelte';
|
||||
|
||||
import VersionAwareSchemaTypes from '$lib/components/schema/VersionAwareSchemaTypes.svelte';
|
||||
import VersionAwareAPIComponents from '$lib/components/schema/VersionAwareAPIComponents.svelte';
|
||||
import DragDropCanvas from '$lib/components/schema/DragDropCanvas.svelte';
|
||||
import PropertyEditor from '$lib/components/schema/PropertyEditor.svelte';
|
||||
import ExportDialog from '$lib/components/schema/ExportDialog.svelte';
|
||||
import InfoEditor from '$lib/components/structure/InfoEditor.svelte';
|
||||
import ServersEditor from '$lib/components/structure/ServersEditor.svelte';
|
||||
import SecurityEditor from '$lib/components/structure/SecurityEditor.svelte';
|
||||
import PathsEditor from '$lib/components/structure/PathsEditor.svelte';
|
||||
import { schemaStore } from '$lib/stores/schema';
|
||||
import { getVersionConfig } from '$lib/utils/openapi-versions';
|
||||
|
||||
let versionConfig = $derived(getVersionConfig($schemaStore.openApiVersion));
|
||||
let showExportDialog = $state(false);
|
||||
let showPreviewDialog = $state(false);
|
||||
let activePanel = $state<'schemas' | 'paths' | 'info' | 'servers' | 'security'>('schemas');
|
||||
|
||||
function handleExport() {
|
||||
showExportDialog = true;
|
||||
}
|
||||
|
||||
function handlePreview() {
|
||||
showPreviewDialog = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen bg-background">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-80 bg-sidebar border-r border-sidebar-border flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-sidebar-border">
|
||||
<h1 class="text-2xl font-bold text-sidebar-foreground">OpenAPI Designer</h1>
|
||||
<p class="text-sm text-sidebar-foreground/70 mt-1">Drag & drop schema builder</p>
|
||||
</div>
|
||||
|
||||
<!-- Version Selector -->
|
||||
<div class="p-4 border-b border-sidebar-border">
|
||||
<label class="text-sm font-medium text-sidebar-foreground mb-2 block" for="openapi-version">OpenAPI Version</label>
|
||||
<select
|
||||
id="openapi-version"
|
||||
class="w-full p-2 rounded-md bg-sidebar-accent border border-sidebar-border text-sidebar-accent-foreground"
|
||||
bind:value={$schemaStore.openApiVersion}
|
||||
>
|
||||
<option value="3.1.0">OpenAPI 3.1.0</option>
|
||||
<option value="3.0.3">OpenAPI 3.0.3</option>
|
||||
<option value="2.0">Swagger 2.0</option>
|
||||
</select>
|
||||
|
||||
<!-- Version info -->
|
||||
<div class="mt-2 p-2 bg-sidebar-accent/50 rounded text-xs">
|
||||
<div class="flex items-center gap-1 mb-1">
|
||||
<Info class="w-3 h-3" />
|
||||
<span class="font-medium">Version Features</span>
|
||||
</div>
|
||||
<div class="space-y-1 text-sidebar-foreground/70">
|
||||
<div>Types: {versionConfig.supportedTypes.length}</div>
|
||||
<div>Nullable: {versionConfig.features.nullable ? 'Yes' : 'No'}</div>
|
||||
<div>Callbacks: {versionConfig.features.callbacks ? 'Yes' : 'No'}</div>
|
||||
<div>Webhooks: {versionConfig.features.webhooks ? 'Yes' : 'No'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation & Editors -->
|
||||
<div class="flex-1 p-4 overflow-y-auto space-y-3">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button size="sm" variant={activePanel === 'schemas' ? 'default' : 'outline'} onclick={() => activePanel = 'schemas'}>Schemas</Button>
|
||||
<Button size="sm" variant={activePanel === 'paths' ? 'default' : 'outline'} onclick={() => activePanel = 'paths'}>Paths</Button>
|
||||
<Button size="sm" variant={activePanel === 'info' ? 'default' : 'outline'} onclick={() => activePanel = 'info'}>Info</Button>
|
||||
<Button size="sm" variant={activePanel === 'servers' ? 'default' : 'outline'} onclick={() => activePanel = 'servers'}>Servers</Button>
|
||||
<Button size="sm" variant={activePanel === 'security' ? 'default' : 'outline'} onclick={() => activePanel = 'security'}>Security</Button>
|
||||
</div>
|
||||
|
||||
{#if activePanel === 'schemas'}
|
||||
<VersionAwareSchemaTypes />
|
||||
<Separator class="my-4" />
|
||||
<VersionAwareAPIComponents />
|
||||
{/if}
|
||||
|
||||
{#if activePanel === 'paths'}
|
||||
<PathsEditor />
|
||||
{/if}
|
||||
|
||||
{#if activePanel === 'info'}
|
||||
<InfoEditor />
|
||||
{/if}
|
||||
|
||||
{#if activePanel === 'servers'}
|
||||
<ServersEditor />
|
||||
{/if}
|
||||
|
||||
{#if activePanel === 'security'}
|
||||
<SecurityEditor />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="p-4 border-t border-sidebar-border">
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" class="flex-1">
|
||||
<Upload class="w-4 h-4 mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="flex-1" onclick={handleExport}>
|
||||
<Download class="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<!-- Toolbar -->
|
||||
<div class="h-14 border-b border-border bg-card flex items-center justify-between px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-semibold text-card-foreground">Schema Designer</h2>
|
||||
<Badge variant="outline">
|
||||
{$schemaStore.schemas.length + $schemaStore.endpoints.length} components
|
||||
</Badge>
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
{versionConfig.displayName}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline">
|
||||
<Settings class="w-4 h-4 mr-2" />
|
||||
Settings
|
||||
</Button>
|
||||
<Button size="sm" onclick={handlePreview}>
|
||||
<FileText class="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Canvas Area -->
|
||||
<div class="flex-1 p-6 overflow-auto">
|
||||
{#if activePanel === 'schemas'}
|
||||
<div class="max-w-full mx-auto">
|
||||
<DragDropCanvas />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-full mx-auto text-sm text-muted-foreground">
|
||||
Use the sidebar to manage {activePanel}.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Properties Panel -->
|
||||
<div class="w-80 bg-card border-l border-border flex flex-col">
|
||||
<div class="p-4 border-b border-border">
|
||||
<h3 class="font-semibold text-card-foreground">Properties</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">Configure selected component</p>
|
||||
</div>
|
||||
<div class="flex-1 p-4 overflow-y-auto">
|
||||
<PropertyEditor />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Dialog -->
|
||||
<ExportDialog
|
||||
open={showExportDialog}
|
||||
onClose={() => showExportDialog = false}
|
||||
/>
|
||||
|
||||
<!-- Preview Dialog (reuse ExportDialog for now) -->
|
||||
<ExportDialog
|
||||
open={showPreviewDialog}
|
||||
onClose={() => showPreviewDialog = false}
|
||||
/>
|
||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user