feat(dashboard): ignore messages page, remove ignore channel page

This commit is contained in:
sam 2024-11-18 20:26:03 +01:00
parent 0cac964aa6
commit 19d9f33454
Signed by: sam
GPG key ID: 5F3C3C1B3166639D
18 changed files with 575 additions and 315 deletions

View file

@ -14,7 +14,7 @@
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "3",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltestrap/sveltestrap": "^6.2.7",
"@types/eslint": "^9.6.0",
"@types/file-saver": "^2.0.7",
@ -32,7 +32,7 @@
"sass": "^1.80.1",
"snarkdown": "^2.0.0",
"svelecte": "^4.3.1",
"svelte": "4",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",

View file

@ -78,7 +78,10 @@ export type FullGuild = {
categories: GuildCategory[];
channels_without_category: GuildChannel[];
roles: GuildRole[];
config: GuildConfig;
ignored_channels: string[];
ignored_roles: string[];
messages: MessageConfig;
channels: ChannelConfig;
key_roles: string[];
};
@ -114,14 +117,15 @@ export type ApiError = {
message: string;
};
export type GuildConfig = GuildChannelConfig & {
export type MessageConfig = {
ignored_channels: string[];
ignored_users: string[];
ignored_roles: string[];
ignored_users_per_channel: Record<string, string[]>;
redirects: Record<string, string>;
};
export type GuildChannelConfig = {
export type ChannelConfig = {
redirects: Record<string, string>;
guild_update: string;
guild_emojis_update: string;
guild_role_create: string;

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Button, ListGroupItem } from "@sveltestrap/sveltestrap";
export let buttonText = "Remove";
</script>
<ListGroupItem class="d-flex justify-content-between align-items-center">
<span>
<slot />
</span>
<Button color="link" on:click>{buttonText}</Button>
</ListGroupItem>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import type { GuildRole } from "$lib/api";
import BaseListItem from "./RemovableListItem.svelte";
export let buttonText: string | undefined = undefined;
export let role: GuildRole | string;
const colour = (c: string) => (c === "#000000" ? "current-color" : c);
</script>
<BaseListItem on:click {buttonText}>
{#if typeof role === "string"}
(unknown role {role})
{:else}
<span style="color: {colour(role.colour)};">&#x25CF;</span>
{role.name}
{/if}
</BaseListItem>

View file

@ -2,7 +2,7 @@
import { Button, Nav, NavItem, NavLink } from "@sveltestrap/sveltestrap";
import type { LayoutData } from "./$types";
import { page } from "$app/stores";
import apiFetch, { type ApiError, type GuildConfig } from "$lib/api";
import apiFetch, { type ApiError, type ChannelConfig } from "$lib/api";
import { addToast } from "$lib/toast";
export let data: LayoutData;
@ -10,16 +10,16 @@
// This only saves log channels. All other pages are lists and are saved immediately upon adding/removing an entry.
const save = async () => {
try {
const resp = await apiFetch<GuildConfig>(
const resp = await apiFetch<ChannelConfig>(
"PATCH",
`/api/guilds/${data.guild.id}`,
data.guild.config,
data.guild.channels,
);
data.guild.config = resp;
data.guild.channels = resp;
addToast({
header: "Saved log channels.",
body: "Successfully edited log channels and ignored channels.",
body: "Successfully edited log channels.",
});
} catch (e) {
addToast({
@ -51,16 +51,10 @@
Redirects
</NavLink>
<NavLink
href="/dash/{data.guild.id}/ignored-channels"
active={$page.url.pathname === `/dash/${data.guild.id}/ignored-channels`}
href="/dash/{data.guild.id}/ignored-messages"
active={$page.url.pathname === `/dash/${data.guild.id}/ignored-messages`}
>
Ignored channels
</NavLink>
<NavLink
href="/dash/{data.guild.id}/ignored-users"
active={$page.url.pathname === `/dash/${data.guild.id}/ignored-users`}
>
Ignored users
Ignored messages
</NavLink>
<NavLink
href="/dash/{data.guild.id}/key-roles"

View file

@ -1,12 +1,11 @@
<script lang="ts">
import { Label } from "@sveltestrap/sveltestrap";
import type { GuildChannelConfig } from "$lib/api";
import type { PageData } from "./$types";
import ChannelSelect from "./ChannelSelect.svelte";
export let data: PageData;
$: channels = data.guild.config as GuildChannelConfig;
$: channels = data.guild.channels;
</script>
<h3>Log channels</h3>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import Svelecte from "svelecte";
type Option = { label: string; value: string };
export let options: Array<Option>;
export let placeholder: string = "Select a role";
export let value: string | string[] | null;
</script>
<Svelecte
bind:value
{options}
{placeholder}
labelField="label"
valueField="value"
searchable={true}
/>

View file

@ -1,114 +0,0 @@
<script lang="ts">
import {
Label,
ListGroup,
ListGroupItem,
Button,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { makeFullOptions } from "$lib/util";
import ChannelSelect from "../ChannelSelect.svelte";
import { fastFetch, type ApiError } from "$lib/api";
import { addToast } from "$lib/toast";
export let data: PageData;
$: ignored = data.guild.config.ignored_channels;
$: options = makeFullOptions(data.guild, ignored);
$: allChannels = [
...data.guild.channels_without_category.map((c) => ({
id: c.id,
name: `#${c.name}`,
})),
...data.guild.categories.map((cat) => ({ id: cat.id, name: cat.name })),
...data.guild.categories.flatMap((cat) =>
cat.channels.map((c) => ({ id: c.id, name: `#${c.name}` })),
),
];
const channelName = (id: string) =>
allChannels.find((c) => c.id === id)?.name || `(unknown channel ${id})`;
let toIgnore: string | null = null;
const addIgnore = async () => {
if (!toIgnore) return;
try {
await fastFetch(
"PUT",
`/api/guilds/${data.guild.id}/ignored-channels/${toIgnore}`,
);
ignored.push(toIgnore);
ignored = ignored;
addToast({
header: "Ignored channel",
body: `Added ${channelName(toIgnore)} to the list of ignored channels.`,
});
toIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring channel",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-channels/${id}`,
);
const idx = ignored.indexOf(id);
if (idx > -1) ignored.splice(idx, 1);
ignored = ignored;
addToast({
header: "Stopped ignoring channel",
body: `Removed ${channelName(id)} from the list of ignored channels.`,
});
} catch (e) {
addToast({
header: "Error removing channel",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
</script>
<h3>Ignored channels</h3>
<p>
Messages from ignored channels will not be logged. Changes to ignored channels
will also not be logged, but note that ignored channels being <em>deleted</em>
(or new channels being created in an ignored category) will still be logged.
</p>
<div>
<Label><strong>Ignore a new channel</strong></Label>
<ChannelSelect bind:value={toIgnore} {options} />
</div>
<div class="my-2 d-grid d-md-block">
<Button color="primary" on:click={() => addIgnore()} disabled={!toIgnore}>
Ignore channel
</Button>
</div>
<div>
<h4>Currently ignored channels</h4>
</div>
<ListGroup>
{#each ignored as id}
<ListGroupItem class="d-flex justify-content-between align-items-center">
{channelName(id)}
<Button color="link" on:click={() => removeIgnore(id)}>
Stop ignoring
</Button>
</ListGroupItem>
{/each}
</ListGroup>

View file

@ -0,0 +1,320 @@
<script lang="ts">
import { ListGroup, Button } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { makeFullOptions } from "$lib/util";
import ChannelSelect from "../ChannelSelect.svelte";
import apiFetch, {
fastFetch,
TOKEN_KEY,
type ApiError,
type GuildRole,
} from "$lib/api";
import { addToast } from "$lib/toast";
import RoleSelect from "../RoleSelect.svelte";
import RoleListItem from "$lib/components/RoleListItem.svelte";
import RemovableListItem from "$lib/components/RemovableListItem.svelte";
import Svelecte from "svelecte";
export let data: PageData;
$: ignoredChannels = data.guild.messages.ignored_channels;
$: channelOptions = makeFullOptions(data.guild, ignoredChannels);
$: ignoredRoles = data.guild.messages.ignored_roles;
$: roleOptions = data.guild.roles
.filter((r) => ignoredRoles.indexOf(r.id) === -1)
.map((r) => ({ label: r.name, value: r.id }));
$: allChannels = [
...data.guild.channels_without_category.map((c) => ({
id: c.id,
name: `#${c.name}`,
})),
...data.guild.categories.map((cat) => ({ id: cat.id, name: cat.name })),
...data.guild.categories.flatMap((cat) =>
cat.channels.map((c) => ({ id: c.id, name: `#${c.name}` })),
),
];
const channelName = (id: string) =>
allChannels.find((c) => c.id === id)?.name || `(unknown channel ${id})`;
const roleName = (id: string) =>
data.guild.roles.find((r) => r.id === id)?.name || `(unknown role ${id})`;
const roleObject = (id: string): GuildRole | string =>
data.guild.roles.find((r) => r.id === id) || id;
let channelToIgnore: string | null = null;
let roleToIgnore: string | null = null;
let userToIgnore: string | null = null;
const userIdRegex = /^\d{15,}$/;
const addChannelIgnore = async () => {
if (!channelToIgnore) return;
try {
await fastFetch(
"PUT",
`/api/guilds/${data.guild.id}/ignored-messages/channels/${channelToIgnore}`,
);
ignoredChannels.push(channelToIgnore);
ignoredChannels = ignoredChannels;
addToast({
header: "Ignored channel",
body: `Added ${channelName(channelToIgnore)} to the list of ignored channels.`,
});
channelToIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring channel",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeChannelIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-messages/channels/${id}`,
);
const idx = ignoredChannels.indexOf(id);
if (idx > -1) ignoredChannels.splice(idx, 1);
ignoredChannels = ignoredChannels;
addToast({
header: "Stopped ignoring channel",
body: `Removed ${channelName(id)} from the list of ignored channels.`,
});
} catch (e) {
addToast({
header: "Error removing channel",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const addRoleIgnore = async () => {
if (!roleToIgnore) return;
try {
await fastFetch(
"PUT",
`/api/guilds/${data.guild.id}/ignored-messages/roles/${roleToIgnore}`,
);
ignoredRoles.push(roleToIgnore);
ignoredRoles = ignoredRoles;
addToast({
header: "Ignored role",
body: `Added ${roleName(roleToIgnore)} to the list of ignored roles.`,
});
roleToIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring role",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeRoleIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-messages/roles/${id}`,
);
const idx = ignoredRoles.indexOf(id);
if (idx > -1) ignoredRoles.splice(idx, 1);
ignoredRoles = ignoredRoles;
addToast({
header: "Stopped ignoring role",
body: `Removed ${roleName(id)} from the list of ignored roles.`,
});
} catch (e) {
addToast({
header: "Error removing role",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const addUserIgnore = async () => {
if (!userToIgnore) return;
try {
const user = await apiFetch<{ id: string; tag: string }>(
"PUT",
`/api/guilds/${data.guild.id}/ignored-users/${userToIgnore}`,
);
data.users.push(user);
data.users = data.users;
addToast({
header: "Ignored user",
body: `Added ${user.tag} to the list of ignored users.`,
});
userToIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeUserIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-users/${id}`,
);
const user = data.users.find((u) => u.id === id);
const idx = data.users.findIndex((u) => u.id === id);
if (idx > -1) data.users.splice(idx, 1);
data.users = data.users;
addToast({
header: "Stopped ignoring user",
body: `Removed ${user?.tag || "unknown user " + id} from the list of ignored users.`,
});
} catch (e) {
addToast({
header: "Error removing user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const fetchProps: RequestInit = {
method: "GET",
headers: {
Authorization: localStorage.getItem(TOKEN_KEY) || "",
},
};
</script>
<h3>Ignored messages</h3>
<p>Here you can select which channels and roles to ignore messages from.</p>
<h4>How it works</h4>
<p>Messages will be ignored if:</p>
<ul>
<li>They are posted in a channel that is listed here</li>
<li>
They are posted in a channel in a <em>category</em> that is listed here
</li>
<li>They are posted in a thread that's in a channel that is listed here</li>
<li>
They are posted by a person with <em>at least one role</em> that is listed here
</li>
<li>They are posted by a user that is listed here</li>
</ul>
<p>
Additionally, messages from <a href="/dash/{data.guild.id}/ignored-users"
>ignored users</a
> are also ignored.
</p>
<h4 class="mt-4">Channels</h4>
<div>
<ChannelSelect bind:value={channelToIgnore} options={channelOptions} />
</div>
<div class="mt-2 mb-3 d-grid d-md-block">
<Button
color="primary"
on:click={() => addChannelIgnore()}
disabled={!channelToIgnore}
>
Ignore messages from this channel
</Button>
</div>
<ListGroup>
{#each ignoredChannels as id}
<RemovableListItem
on:click={() => removeChannelIgnore(id)}
buttonText="Stop ignoring"
>
{channelName(id)}
</RemovableListItem>
{/each}
</ListGroup>
<h4 class="mt-4">Roles</h4>
<div>
<RoleSelect bind:value={roleToIgnore} options={roleOptions} />
</div>
<div class="mt-2 mb-3 d-grid d-md-block">
<Button
color="primary"
on:click={() => addRoleIgnore()}
disabled={!roleToIgnore}
>
Ignore messages from this role
</Button>
</div>
<ListGroup>
{#each ignoredRoles as id}
<RoleListItem
role={roleObject(id)}
on:click={() => removeRoleIgnore(id)}
buttonText="Stop ignoring"
/>
{/each}
</ListGroup>
<h4 class="mt-4">Users</h4>
<div>
<Svelecte
bind:value={userToIgnore}
fetch="/api/guilds/{data.guild.id}/users?query=[query]"
{fetchProps}
multiple={false}
searchable={true}
creatable={true}
labelField="name"
valueField="id"
creatablePrefix="user with ID "
/>
{#if userToIgnore && !userIdRegex.test(userToIgnore)}
<p class="text-danger mt-2">
If you're not ignoring a member of your server, you need to give a
<strong>user ID</strong>, not their username.
</p>
{/if}
</div>
<div class="my-2 d-grid d-md-block">
<Button
color="primary"
on:click={() => addUserIgnore()}
disabled={!userToIgnore || !userIdRegex.test(userToIgnore)}
>
Ignore user
</Button>
</div>
<ListGroup>
{#each data.users as user (user.id)}
<RemovableListItem
on:click={() => removeUserIgnore(user.id)}
buttonText="Stop ignoring"
>
{user.tag} (ID: {user.id})
</RemovableListItem>
{/each}
</ListGroup>

View file

@ -1,119 +0,0 @@
<script lang="ts">
import {
Button,
Label,
ListGroup,
ListGroupItem,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { addToast } from "$lib/toast";
import apiFetch, { fastFetch, TOKEN_KEY, type ApiError } from "$lib/api";
import Svelecte from "svelecte";
export let data: PageData;
let toIgnore: string | null = null;
const idRegex = /^\d{15,}$/;
const addIgnore = async () => {
if (!toIgnore) return;
try {
const user = await apiFetch<{ id: string; tag: string }>(
"PUT",
`/api/guilds/${data.guild.id}/ignored-users/${toIgnore}`,
);
data.users.push(user);
data.users = data.users;
addToast({
header: "Ignored user",
body: `Added ${user.tag} to the list of ignored users.`,
});
toIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-users/${id}`,
);
const user = data.users.find((u) => u.id === id);
const idx = data.users.findIndex((u) => u.id === id);
if (idx > -1) data.users.splice(idx, 1);
data.users = data.users;
addToast({
header: "Stopped ignoring user",
body: `Removed ${user?.tag || "unknown user " + id} from the list of ignored users.`,
});
} catch (e) {
addToast({
header: "Error removing user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const fetchProps: RequestInit = {
method: "GET",
headers: {
Authorization: localStorage.getItem(TOKEN_KEY) || "",
},
};
</script>
<h3>Ignored users</h3>
<p>Messages from ignored users will not be logged.</p>
<div>
<Label><strong>Ignore a new user</strong></Label>
<Svelecte
bind:value={toIgnore}
fetch="/api/guilds/{data.guild.id}/users?query=[query]"
{fetchProps}
multiple={false}
searchable={true}
creatable={true}
labelField="name"
valueField="id"
creatablePrefix="user with ID "
/>
{#if toIgnore && !idRegex.test(toIgnore)}
<p class="text-danger mt-2">
If you're not ignoring a member of your server, you need to give a
<strong>user ID</strong>, not their username.
</p>
{/if}
</div>
<div class="my-2 d-grid d-md-block">
<Button
color="primary"
on:click={() => addIgnore()}
disabled={!toIgnore || !idRegex.test(toIgnore)}
>
Ignore user
</Button>
</div>
<h4>Currently ignored users</h4>
<ListGroup>
{#each data.users as user (user.id)}
<ListGroupItem class="d-flex justify-content-between align-items-center">
<span>{user.tag} (ID: {user.id})</span>
<Button color="link" on:click={() => removeIgnore(user.id)}>
Stop ignoring
</Button>
</ListGroupItem>
{/each}
</ListGroup>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Button, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
import { Alert, Button, ButtonGroup, Input } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import apiFetch, { fastFetch, type ApiError } from "$lib/api";
import saveAs from "file-saver";
@ -64,6 +64,12 @@
too.
</p>
<Alert color="danger">
<h4 class="alert-heading">This will overwrite existing settings</h4>
If you have already made some changes to this server's settings, they will be lost
when importing a backup.
</Alert>
<p>
<Input
type="file"

View file

@ -3,12 +3,12 @@
Button,
Label,
ListGroup,
ListGroupItem,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import Svelecte from "svelecte";
import { addToast } from "$lib/toast";
import { fastFetch, type ApiError } from "$lib/api";
import RoleListItem from "$lib/components/RoleListItem.svelte";
export let data: PageData;
@ -63,8 +63,6 @@
});
}
};
const colour = (c: string) => (c === "#000000" ? "current-color" : c);
</script>
<h3>Key roles</h3>
@ -95,14 +93,8 @@
<h4>Current key roles</h4>
<ListGroup>
{#each keyRoles as r (r.id)}
<ListGroupItem class="d-flex justify-content-between align-items-center">
<span>
<span style="color: {colour(r.colour)};">&#x25CF;</span>
{r.name}
</span>
<Button color="link" on:click={() => removeRole(r.id)}>Remove</Button>
</ListGroupItem>
{#each keyRoles as role (role.id)}
<RoleListItem {role} on:click={() => removeRole(role.id)} />
{/each}
</ListGroup>
</div>

View file

@ -11,10 +11,11 @@
import { fastFetch } from "$lib/api";
import { addToast } from "$lib/toast";
import { makeFullOptions } from "$lib/util";
import RemovableListItem from "$lib/components/RemovableListItem.svelte";
export let data: PageData;
$: redirects = data.guild.config.redirects;
$: redirects = data.guild.channels.redirects;
$: allChannels = [
...data.guild.channels_without_category.map((c) => ({
@ -51,7 +52,7 @@
target,
});
data.guild.config.redirects[source] = target;
data.guild.channels.redirects[source] = target;
addToast({ body: "Successfully added redirect." });
} catch (e) {
@ -67,8 +68,8 @@
try {
await fastFetch("DELETE", `/api/guilds/${data.guild.id}/redirects/${id}`);
delete data.guild.config.redirects[id];
data.guild.config.redirects = data.guild.config.redirects;
delete data.guild.channels.redirects[id];
data.guild.channels.redirects = data.guild.channels.redirects;
addToast({ body: "Successfully removed redirect." });
} catch (e) {
@ -108,16 +109,12 @@
<ListGroup>
{#each Object.keys(redirects) as redirectSource}
<ListGroupItem class="d-flex justify-content-between align-items-center">
<span
>{channelName(redirectSource)}{channelName(
redirects[redirectSource],
)}</span
>
<Button color="link" on:click={() => removeRedirect(redirectSource)}>
Remove redirect
</Button>
</ListGroupItem>
<RemovableListItem
on:click={() => removeRedirect(redirectSource)}
buttonText="Remove redirect"
>
{channelName(redirectSource)}{channelName(redirects[redirectSource])}
</RemovableListItem>
{/each}
</ListGroup>
{:else}

View file

@ -696,8 +696,8 @@ __metadata:
linkType: hard
"@sveltejs/kit@npm:^2.0.0":
version: 2.8.0
resolution: "@sveltejs/kit@npm:2.8.0"
version: 2.8.1
resolution: "@sveltejs/kit@npm:2.8.1"
dependencies:
"@types/cookie": "npm:^0.6.0"
cookie: "npm:^0.6.0"
@ -717,7 +717,7 @@ __metadata:
vite: ^5.0.3
bin:
svelte-kit: svelte-kit.js
checksum: 10c0/f4fdad81bf5f8f645eed21ffd7015b14c76481b4df054f7dc16ab6bb6a32dc0c9fd28790a7da3e3e3a0b9860e6405cbd082df8cc689694f09bef30659c47cc23
checksum: 10c0/531cf8fdff0e039f51ad3e9aa288acea5fd9eb03b3189fda68dc45e7ac12a288f6a1792aace9c9fec3979475b5e487eb3af4b6160ad23c5bea1f503b89da5cd2
languageName: node
linkType: hard
@ -734,7 +734,7 @@ __metadata:
languageName: node
linkType: hard
"@sveltejs/vite-plugin-svelte@npm:3":
"@sveltejs/vite-plugin-svelte@npm:^3.0.0":
version: 3.1.2
resolution: "@sveltejs/vite-plugin-svelte@npm:3.1.2"
dependencies:
@ -1117,7 +1117,7 @@ __metadata:
dependencies:
"@sveltejs/adapter-static": "npm:^3.0.5"
"@sveltejs/kit": "npm:^2.0.0"
"@sveltejs/vite-plugin-svelte": "npm:3"
"@sveltejs/vite-plugin-svelte": "npm:^3.0.0"
"@sveltestrap/sveltestrap": "npm:^6.2.7"
"@types/eslint": "npm:^9.6.0"
"@types/file-saver": "npm:^2.0.7"
@ -1135,7 +1135,7 @@ __metadata:
sass: "npm:^1.80.1"
snarkdown: "npm:^2.0.0"
svelecte: "npm:^4.3.1"
svelte: "npm:4"
svelte: "npm:^4.2.7"
svelte-check: "npm:^4.0.0"
typescript: "npm:^5.0.0"
typescript-eslint: "npm:^8.0.0"
@ -3124,7 +3124,7 @@ __metadata:
languageName: node
linkType: hard
"svelte@npm:4":
"svelte@npm:^4.2.7":
version: 4.2.19
resolution: "svelte@npm:4.2.19"
dependencies: