Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/components/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { format as jsoncFormat, applyEdits } from "jsonc-parser";
import * as YAML from "js-yaml";
import { settings } from "../stores/settings.js";
import { pushNotification } from "../stores/notifications.svelte.js";
import FormatterIcon from "./FormatterIcon.svelte";
import jsonLexer from "../lib/json-lexer.js";
import yamlLexer from "../lib/yaml-lexer.js";
Expand Down Expand Up @@ -39,13 +40,21 @@
});

const formatCode = () => {
if (format === "json") {
const edits = jsoncFormat(value, undefined, { tabSize: $settings.indentSize, insertSpaces: true, eol: "\n", keepLines: $settings.keepLines });
value = applyEdits(value, edits);
} else if (format === "yaml") {
value = YAML.dump(YAML.load(value), { indent: $settings.indentSize });
} else {
throw Error("Unsupported format");
try {
if (format === "json") {
const edits = jsoncFormat(value, undefined, { tabSize: $settings.indentSize, insertSpaces: true, eol: "\n", keepLines: $settings.keepLines });
value = applyEdits(value, edits);
} else if (format === "yaml") {
value = YAML.dump(YAML.load(value), { indent: $settings.indentSize });
} else {
throw Error("Unsupported format");
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
pushNotification(
`Failed to format code: ${message}`,
"error"
);
}
};
</script>
Expand Down
100 changes: 100 additions & 0 deletions src/components/NotificationToast.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script lang="ts">
import { fly, fade } from "svelte/transition";
import { notifications, dismissNotification } from "../stores/notifications.svelte.js";
</script>

{#if notifications.length > 0}
<div class="toast-container" aria-live="polite">
{#each notifications as notification (notification.id)}
<div
class="toast toast-{notification.type}"
role="alert"
transition:fly={{ y: -30, duration: 300 }}
>
<span class="toast-icon">
{#if notification.type === "error"}✕
{:else if notification.type === "warning"}⚠
{:else}ℹ
{/if}
</span>
<span class="toast-message">{notification.message}</span>
<button
class="toast-dismiss"
aria-label="Dismiss notification"
onclick={() => dismissNotification(notification.id)}
>×</button>
</div>
{/each}
</div>
{/if}

<style>
.toast-container {
position: fixed;
top: 1em;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0.5em;
pointer-events: none;
max-width: 90vw;
}

.toast {
pointer-events: auto;
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.6em 1em;
border-radius: 0.5em;
font-size: 0.9em;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}

.toast-warning {
background-color: rgba(255, 170, 30, 0.85);
color: #1a1a1a;
}

.toast-error {
background-color: rgba(220, 50, 50, 0.85);
color: #fff;
}

.toast-info {
background-color: rgba(50, 140, 220, 0.85);
color: #fff;
}

.toast-icon {
flex-shrink: 0;
font-weight: bold;
font-size: 1.1em;
}

.toast-message {
flex-grow: 1;
line-height: 1.3;
}

.toast-dismiss {
flex-shrink: 0;
background: none;
border: none;
color: inherit;
font-size: 1.2em;
cursor: pointer;
padding: 0 0.2em;
opacity: 0.7;
line-height: 1;
}

.toast-dismiss:hover {
opacity: 1;
}
</style>
63 changes: 60 additions & 3 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import EditorTabs from "../components/EditorTabs.svelte";
import Results from "../components/Results.svelte";
import Footer from "../components/Footer.svelte";
import NotificationToast from "../components/NotificationToast.svelte";

import { pushNotification } from "../stores/notifications.svelte.js";

import { settings } from "../stores/settings.js";

Expand Down Expand Up @@ -156,11 +159,64 @@ $id: '${id}'`
}
};

const convertFormat = (
text: string,
fromFormat: "json" | "yaml",
toFormat: "json" | "yaml"
): { text: string; error: boolean } => {
if (!text || !text.trim()) return { text, error: false };

try {
const parsed = parse(text, fromFormat);

if (toFormat === "yaml") {
return {
text: YAML.dump(parsed, { indent: $settings.indentSize }),
error: false
};
} else {
return {
text: JSON.stringify(parsed, null, $settings.indentSize),
error: false
};
}
} catch (e) {
return { text, error: true };
}
};

const setFormat = (newFormat: "json" | "yaml") => () => {
if (format === newFormat) return;
let hasError = false;
schemas = schemas.map((schema) => {
const result = convertFormat(schema.text ?? "", format, newFormat);
if (result.error) hasError = true;

return {
...schema,
text: result.text
};
});

instances = instances.map((instance) => {
const result = convertFormat(instance.text ?? "", format, newFormat);
if (result.error) hasError = true;

return {
...instance,
text: result.text
};
});

if (hasError) {
pushNotification(
"Format switch aborted: some tabs contain syntax errors that prevent conversion.",
"warning"
);
return;
}

format = newFormat;
schemas = [newSchema("Schema", schemaUrl, true)];
instances = [newInstance()];
selectedInstance = 0;
};
</script>

Expand Down Expand Up @@ -216,6 +272,7 @@ $id: '${id}'`
</div>

<Footer />
<NotificationToast />
</main>

<style>
Expand Down
32 changes: 32 additions & 0 deletions src/stores/notifications.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
let nextId = 0;

export type NotificationType = "info" | "warning" | "error";

export type Notification = {
id: number;
message: string;
type: NotificationType;
duration: number;
};

export let notifications: Notification[] = $state([]);

export const pushNotification = (
message: string,
type: NotificationType = "info",
duration = 4000
) => {
const id = nextId++;
notifications.push({ id, message, type, duration });

setTimeout(() => {
dismissNotification(id);
}, duration);
};

export const dismissNotification = (id: number) => {
const index = notifications.findIndex((n) => n.id === id);
if (index !== -1) {
notifications.splice(index, 1);
}
};