Initial commit

This commit is contained in:
Luke Hagar
2025-09-15 03:03:36 +00:00
commit 9502c119cd
54 changed files with 5163 additions and 0 deletions

23
.gitignore vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- esbuild

76
src/app.css Normal file
View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>

View 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}

View File

@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View 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}

View 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,
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View File

@@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View 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}

View File

@@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View 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}
/>

View File

@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View 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}
/>

View File

@@ -0,0 +1,7 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};

View 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>

View File

@@ -0,0 +1,7 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};

View 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
View 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
View 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 };

View 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)
}

View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View 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
View 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
View 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()]
});