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