aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.d.ts13
-rw-r--r--src/app.html11
-rw-r--r--src/lib/Alerts.svelte184
-rw-r--r--src/lib/assets/favicon.old.svg9
-rw-r--r--src/lib/assets/favicon.pngbin0 -> 1149 bytes
-rw-r--r--src/lib/assets/favicon.svg1
-rw-r--r--src/lib/index.ts1
-rw-r--r--src/routes/+layout.svelte11
-rw-r--r--src/routes/+page.svelte202
9 files changed, 432 insertions, 0 deletions
diff --git a/src/app.d.ts b/src/app.d.ts
new file mode 100644
index 0000000..da08e6d
--- /dev/null
+++ b/src/app.d.ts
@@ -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 {};
diff --git a/src/app.html b/src/app.html
new file mode 100644
index 0000000..b0b3788
--- /dev/null
+++ b/src/app.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ %sveltekit.head%
+ </head>
+ <body data-sveltekit-preload-data="hover" class="cs">
+ <div style="display: contents">%sveltekit.body%</div>
+ </body>
+</html>
diff --git a/src/lib/Alerts.svelte b/src/lib/Alerts.svelte
new file mode 100644
index 0000000..1e4641b
--- /dev/null
+++ b/src/lib/Alerts.svelte
@@ -0,0 +1,184 @@
+<!--
+MIT License
+
+Copyright (c) 2025 memdmp
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ -->
+
+<script lang="ts" module>
+ let ids = $state([] as number[]);
+ let idCtr = $state(0);
+ let dialogObject = $state({
+ open: false,
+ title: '',
+ content: '',
+ closeBtn: false,
+ buttons: [
+ {
+ text: 'Close',
+ },
+ ] as {
+ text: string;
+ callback?: () =>
+ | {
+ success: true;
+ content: string;
+ }
+ | {
+ success: false;
+ content: any;
+ };
+ }[],
+ });
+ let closeAlert = (m: string) => void 0;
+ let cancelAlert = (err?: any) => void 0;
+ let dialogPromises = [] as Promise<string>[];
+ let dialogPromise = Promise.resolve('');
+ const createDialogPromise = (setAlertTo?: typeof dialogObject) => (
+ dialogPromises.push(
+ (dialogPromise = new Promise<string>((rs, rj) => {
+ let oldCloseAlert = closeAlert;
+ let oldCancelAlert = cancelAlert;
+ closeAlert = (m) => {
+ closeAlert = oldCloseAlert;
+ dialogObject = setAlertTo ?? dialogObject;
+ rs(m);
+ };
+ cancelAlert = (m) => {
+ cancelAlert = oldCancelAlert;
+ dialogObject = setAlertTo ?? dialogObject;
+ rj(m);
+ };
+ }))
+ ),
+ dialogPromise
+ );
+ export const alert = async (
+ message: string,
+ options?: {
+ title?: string;
+ /** prioritise over existing alerts */
+ priority?: boolean;
+ /** if we can close the dialog with the x at the top */
+ canClose?: boolean;
+ /** button list */
+ buttons?: typeof dialogObject.buttons;
+ }
+ ) => {
+ if (!options?.priority) await Promise.allSettled(dialogPromises);
+ const oldAlert = dialogObject;
+ dialogObject = {
+ open: true,
+ title: options?.title ?? 'Alert',
+ closeBtn: options?.canClose ?? true,
+ buttons: options?.buttons ?? [
+ {
+ text: 'Close',
+ },
+ ],
+ content: message,
+ };
+ return createDialogPromise(oldAlert);
+ };
+ export const confirm = async (
+ message: string,
+ options?: Omit<Parameters<typeof alert>[1], 'buttons'>
+ ) => {
+ const rs = await alert(message, {
+ title: 'Confirmation',
+ ...(options ?? {}),
+ buttons: [
+ {
+ text: 'OK',
+ callback: () => ({
+ success: true,
+ content: 'OK',
+ }),
+ },
+ {
+ text: 'Cancel',
+ callback: () => ({
+ success: true,
+ content: 'Cancel',
+ }),
+ },
+ ],
+ });
+ return rs === 'OK';
+ };
+</script>
+
+<script lang="ts">
+ import { onDestroy, onMount } from 'svelte';
+
+ let dialog = $state(null as HTMLDialogElement | null);
+ let id: number = $state(-1);
+ onMount(() => {
+ id = idCtr++;
+ ids.push(id);
+ });
+ onDestroy(() => (ids = ids.filter((v) => v !== id)));
+ $effect(() => {
+ dialog?.showModal();
+ });
+</script>
+
+{#if ids[0] === id && dialogObject.open}
+ <section>
+ <dialog bind:this={dialog}>
+ <form method="dialog">
+ <div class="heading">
+ <div class="wrapper">
+ <div class="icon"></div>
+ <p class="text">{dialogObject.title}</p>
+ </div>
+ <button
+ class="close"
+ aria-label="Close"
+ disabled={!dialogObject.closeBtn}
+ onclick={() => {
+ cancelAlert(new Error('User Cancelled Alert'));
+ }}
+ ></button>
+ </div>
+ <div class="content">
+ {dialogObject.content}
+ </div>
+ <menu class="footer-btns" style="gap: 4px; display: flex;">
+ {#each dialogObject.buttons as button}
+ <button
+ onclick={(e) => {
+ const callback =
+ button.callback ??
+ (() => ({
+ success: true,
+ content: button.text,
+ }));
+ const result = callback();
+ if (result.success) closeAlert(result.content);
+ else cancelAlert(result.content);
+ }}>{button.text}</button
+ >
+ {/each}
+ </menu>
+ </form>
+ </dialog>
+ </section>
+{/if}
diff --git a/src/lib/assets/favicon.old.svg b/src/lib/assets/favicon.old.svg
new file mode 100644
index 0000000..d69dee8
--- /dev/null
+++ b/src/lib/assets/favicon.old.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128">
+ <title>svelte-logo</title>
+ <path
+ d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116"
+ style="fill:#BF64CC" />
+ <path
+ d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328"
+ style="fill:#fff" />
+</svg> \ No newline at end of file
diff --git a/src/lib/assets/favicon.png b/src/lib/assets/favicon.png
new file mode 100644
index 0000000..d7ac7dd
--- /dev/null
+++ b/src/lib/assets/favicon.png
Binary files differ
diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg
new file mode 100644
index 0000000..45f50a4
--- /dev/null
+++ b/src/lib/assets/favicon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox="0 0 24 24" width="256" height="256"><rect fill="#bd61ce2f" x="13" y="1" width="1" height="1"/><rect fill="#bf64cc65" x="14" y="1" width="1" height="1"/><rect fill="#be63cc73" x="15" y="1" width="1" height="1"/><rect fill="#bf64cb54" x="16" y="1" width="1" height="1"/><rect fill="#bb5dc913" x="17" y="1" width="1" height="1"/><rect fill="#bf62cd34" x="11" y="2" width="1" height="1"/><rect fill="#bf64ccc4" x="12" y="2" width="1" height="1"/><rect fill="#bf64cc" x="13" y="2" width="1" height="1"/><rect fill="#bf64cc" x="14" y="2" width="1" height="1"/><rect fill="#bf64cc" x="15" y="2" width="1" height="1"/><rect fill="#bf64cc" x="16" y="2" width="1" height="1"/><rect fill="#be63ccfa" x="17" y="2" width="1" height="1"/><rect fill="#bf63cb94" x="18" y="2" width="1" height="1"/><rect fill="#bf6ad40c" x="19" y="2" width="1" height="1"/><rect fill="#b95cd00b" x="9" y="3" width="1" height="1"/><rect fill="#be64cc8e" x="10" y="3" width="1" height="1"/><rect fill="#bf64ccfc" x="11" y="3" width="1" height="1"/><rect fill="#bf64cc" x="12" y="3" width="1" height="1"/><rect fill="#bf64cc" x="13" y="3" width="1" height="1"/><rect fill="#bf64cc" x="14" y="3" width="1" height="1"/><rect fill="#c26bce" x="15" y="3" width="1" height="1"/><rect fill="#bf64cc" x="16" y="3" width="1" height="1"/><rect fill="#bf64cc" x="17" y="3" width="1" height="1"/><rect fill="#bf64cc" x="18" y="3" width="1" height="1"/><rect fill="#bf64cbd1" x="19" y="3" width="1" height="1"/><rect fill="#c369d211" x="20" y="3" width="1" height="1"/><rect fill="#bf63cd48" x="8" y="4" width="1" height="1"/><rect fill="#be63cbdf" x="9" y="4" width="1" height="1"/><rect fill="#bf64cc" x="10" y="4" width="1" height="1"/><rect fill="#bf64cc" x="11" y="4" width="1" height="1"/><rect fill="#c26bce" x="12" y="4" width="1" height="1"/><rect fill="#e1b5e7" x="13" y="4" width="1" height="1"/><rect fill="#fbf5fc" x="14" y="4" width="1" height="1"/><rect fill="#ffffff" x="15" y="4" width="1" height="1"/><rect fill="#f8edf9" x="16" y="4" width="1" height="1"/><rect fill="#daa5e1" x="17" y="4" width="1" height="1"/><rect fill="#bf64cc" x="18" y="4" width="1" height="1"/><rect fill="#bf64cc" x="19" y="4" width="1" height="1"/><rect fill="#be64cbb2" x="20" y="4" width="1" height="1"/><rect fill="#bb66cc0f" x="6" y="5" width="1" height="1"/><rect fill="#bf63cba4" x="7" y="5" width="1" height="1"/><rect fill="#bf64cc" x="8" y="5" width="1" height="1"/><rect fill="#bf64cc" x="9" y="5" width="1" height="1"/><rect fill="#bf64cc" x="10" y="5" width="1" height="1"/><rect fill="#d190db" x="11" y="5" width="1" height="1"/><rect fill="#f7ecf9" x="12" y="5" width="1" height="1"/><rect fill="#ffffff" x="13" y="5" width="1" height="1"/><rect fill="#ffffff" x="14" y="5" width="1" height="1"/><rect fill="#ffffff" x="15" y="5" width="1" height="1"/><rect fill="#ffffff" x="16" y="5" width="1" height="1"/><rect fill="#ffffff" x="17" y="5" width="1" height="1"/><rect fill="#ddade4" x="18" y="5" width="1" height="1"/><rect fill="#bf64cc" x="19" y="5" width="1" height="1"/><rect fill="#bf64cc" x="20" y="5" width="1" height="1"/><rect fill="#bd65ca3a" x="21" y="5" width="1" height="1"/><rect fill="#bb5dc913" x="5" y="6" width="1" height="1"/><rect fill="#be63cbd7" x="6" y="6" width="1" height="1"/><rect fill="#bf64cc" x="7" y="6" width="1" height="1"/><rect fill="#bf64cc" x="8" y="6" width="1" height="1"/><rect fill="#c571d0" x="9" y="6" width="1" height="1"/><rect fill="#e8c8ed" x="10" y="6" width="1" height="1"/><rect fill="#ffffff" x="11" y="6" width="1" height="1"/><rect fill="#ffffff" x="12" y="6" width="1" height="1"/><rect fill="#ffffff" x="13" y="6" width="1" height="1"/><rect fill="#f5e7f7" x="14" y="6" width="1" height="1"/><rect fill="#deafe5" x="15" y="6" width="1" height="1"/><rect fill="#f7ecf9" x="16" y="6" width="1" height="1"/><rect fill="#ffffff" x="17" y="6" width="1" height="1"/><rect fill="#fbf6fc" x="18" y="6" width="1" height="1"/><rect fill="#c26ccf" x="19" y="6" width="1" height="1"/><rect fill="#bf64cc" x="20" y="6" width="1" height="1"/><rect fill="#be64cb8b" x="21" y="6" width="1" height="1"/><rect fill="#be64ccaa" x="5" y="7" width="1" height="1"/><rect fill="#bf64cc" x="6" y="7" width="1" height="1"/><rect fill="#bf64cc" x="7" y="7" width="1" height="1"/><rect fill="#d497dd" x="8" y="7" width="1" height="1"/><rect fill="#faf3fb" x="9" y="7" width="1" height="1"/><rect fill="#ffffff" x="10" y="7" width="1" height="1"/><rect fill="#ffffff" x="11" y="7" width="1" height="1"/><rect fill="#fdfbfe" x="12" y="7" width="1" height="1"/><rect fill="#dfb2e6" x="13" y="7" width="1" height="1"/><rect fill="#c168cd" x="14" y="7" width="1" height="1"/><rect fill="#bf64cc" x="15" y="7" width="1" height="1"/><rect fill="#d59ade" x="16" y="7" width="1" height="1"/><rect fill="#fffeff" x="17" y="7" width="1" height="1"/><rect fill="#ffffff" x="18" y="7" width="1" height="1"/><rect fill="#cb81d5" x="19" y="7" width="1" height="1"/><rect fill="#bf64cc" x="20" y="7" width="1" height="1"/><rect fill="#bf63ccac" x="21" y="7" width="1" height="1"/><rect fill="#c164c821" x="4" y="8" width="1" height="1"/><rect fill="#be64cbfe" x="5" y="8" width="1" height="1"/><rect fill="#bf64cc" x="6" y="8" width="1" height="1"/><rect fill="#d496dd" x="7" y="8" width="1" height="1"/><rect fill="#fefeff" x="8" y="8" width="1" height="1"/><rect fill="#ffffff" x="9" y="8" width="1" height="1"/><rect fill="#ffffff" x="10" y="8" width="1" height="1"/><rect fill="#f0dcf3" x="11" y="8" width="1" height="1"/><rect fill="#ca7ed5" x="12" y="8" width="1" height="1"/><rect fill="#bf64cc" x="13" y="8" width="1" height="1"/><rect fill="#bf64cc" x="14" y="8" width="1" height="1"/><rect fill="#bf64cc" x="15" y="8" width="1" height="1"/><rect fill="#bf64cc" x="16" y="8" width="1" height="1"/><rect fill="#c97cd4" x="17" y="8" width="1" height="1"/><rect fill="#ecd0f0" x="18" y="8" width="1" height="1"/><rect fill="#c879d3" x="19" y="8" width="1" height="1"/><rect fill="#bf64cc" x="20" y="8" width="1" height="1"/><rect fill="#be64cca2" x="21" y="8" width="1" height="1"/><rect fill="#be63cb5f" x="4" y="9" width="1" height="1"/><rect fill="#bf64cc" x="5" y="9" width="1" height="1"/><rect fill="#bf64cc" x="6" y="9" width="1" height="1"/><rect fill="#f3e1f5" x="7" y="9" width="1" height="1"/><rect fill="#ffffff" x="8" y="9" width="1" height="1"/><rect fill="#fdfafd" x="9" y="9" width="1" height="1"/><rect fill="#daa5e1" x="10" y="9" width="1" height="1"/><rect fill="#bf65cc" x="11" y="9" width="1" height="1"/><rect fill="#bf64cc" x="12" y="9" width="1" height="1"/><rect fill="#bf64cc" x="13" y="9" width="1" height="1"/><rect fill="#c676d2" x="14" y="9" width="1" height="1"/><rect fill="#ca7ed4" x="15" y="9" width="1" height="1"/><rect fill="#c066cd" x="16" y="9" width="1" height="1"/><rect fill="#bf64cc" x="17" y="9" width="1" height="1"/><rect fill="#bf64cc" x="18" y="9" width="1" height="1"/><rect fill="#bf64cc" x="19" y="9" width="1" height="1"/><rect fill="#bf64cc" x="20" y="9" width="1" height="1"/><rect fill="#be65cc6a" x="21" y="9" width="1" height="1"/><rect fill="#bf64cc70" x="4" y="10" width="1" height="1"/><rect fill="#bf64cc" x="5" y="10" width="1" height="1"/><rect fill="#bf64cc" x="6" y="10" width="1" height="1"/><rect fill="#fbf5fc" x="7" y="10" width="1" height="1"/><rect fill="#ffffff" x="8" y="10" width="1" height="1"/><rect fill="#e8c6ec" x="9" y="10" width="1" height="1"/><rect fill="#bf64cc" x="10" y="10" width="1" height="1"/><rect fill="#bf64cc" x="11" y="10" width="1" height="1"/><rect fill="#cd86d7" x="12" y="10" width="1" height="1"/><rect fill="#f1def4" x="13" y="10" width="1" height="1"/><rect fill="#ffffff" x="14" y="10" width="1" height="1"/><rect fill="#ffffff" x="15" y="10" width="1" height="1"/><rect fill="#faf2fb" x="16" y="10" width="1" height="1"/><rect fill="#d69ddf" x="17" y="10" width="1" height="1"/><rect fill="#bf64cc" x="18" y="10" width="1" height="1"/><rect fill="#bf64cc" x="19" y="10" width="1" height="1"/><rect fill="#be63cbee" x="20" y="10" width="1" height="1"/><rect fill="#bb66cc0f" x="21" y="10" width="1" height="1"/><rect fill="#be65ca53" x="4" y="11" width="1" height="1"/><rect fill="#bf64cc" x="5" y="11" width="1" height="1"/><rect fill="#bf64cc" x="6" y="11" width="1" height="1"/><rect fill="#f2dff4" x="7" y="11" width="1" height="1"/><rect fill="#ffffff" x="8" y="11" width="1" height="1"/><rect fill="#faf4fb" x="9" y="11" width="1" height="1"/><rect fill="#d8a1e0" x="10" y="11" width="1" height="1"/><rect fill="#e4bfea" x="11" y="11" width="1" height="1"/><rect fill="#fefeff" x="12" y="11" width="1" height="1"/><rect fill="#ffffff" x="13" y="11" width="1" height="1"/><rect fill="#ffffff" x="14" y="11" width="1" height="1"/><rect fill="#ffffff" x="15" y="11" width="1" height="1"/><rect fill="#ffffff" x="16" y="11" width="1" height="1"/><rect fill="#fffeff" x="17" y="11" width="1" height="1"/><rect fill="#d394dc" x="18" y="11" width="1" height="1"/><rect fill="#bf64cc" x="19" y="11" width="1" height="1"/><rect fill="#be63ccf6" x="20" y="11" width="1" height="1"/><rect fill="#c369d211" x="21" y="11" width="1" height="1"/><rect fill="#c369d211" x="4" y="12" width="1" height="1"/><rect fill="#be63ccf6" x="5" y="12" width="1" height="1"/><rect fill="#bf64cc" x="6" y="12" width="1" height="1"/><rect fill="#d394dc" x="7" y="12" width="1" height="1"/><rect fill="#fffeff" x="8" y="12" width="1" height="1"/><rect fill="#ffffff" x="9" y="12" width="1" height="1"/><rect fill="#ffffff" x="10" y="12" width="1" height="1"/><rect fill="#ffffff" x="11" y="12" width="1" height="1"/><rect fill="#ffffff" x="12" y="12" width="1" height="1"/><rect fill="#fefeff" x="13" y="12" width="1" height="1"/><rect fill="#e4bfea" x="14" y="12" width="1" height="1"/><rect fill="#d8a0e0" x="15" y="12" width="1" height="1"/><rect fill="#faf4fb" x="16" y="12" width="1" height="1"/><rect fill="#ffffff" x="17" y="12" width="1" height="1"/><rect fill="#f2dff4" x="18" y="12" width="1" height="1"/><rect fill="#bf64cc" x="19" y="12" width="1" height="1"/><rect fill="#bf64cc" x="20" y="12" width="1" height="1"/><rect fill="#be65ca53" x="21" y="12" width="1" height="1"/><rect fill="#bf5fcf10" x="4" y="13" width="1" height="1"/><rect fill="#be63cbee" x="5" y="13" width="1" height="1"/><rect fill="#bf64cc" x="6" y="13" width="1" height="1"/><rect fill="#bf64cc" x="7" y="13" width="1" height="1"/><rect fill="#d69ddf" x="8" y="13" width="1" height="1"/><rect fill="#faf2fb" x="9" y="13" width="1" height="1"/><rect fill="#ffffff" x="10" y="13" width="1" height="1"/><rect fill="#ffffff" x="11" y="13" width="1" height="1"/><rect fill="#f1def4" x="12" y="13" width="1" height="1"/><rect fill="#cd86d7" x="13" y="13" width="1" height="1"/><rect fill="#bf64cc" x="14" y="13" width="1" height="1"/><rect fill="#bf64cc" x="15" y="13" width="1" height="1"/><rect fill="#e8c6ec" x="16" y="13" width="1" height="1"/><rect fill="#ffffff" x="17" y="13" width="1" height="1"/><rect fill="#fbf5fc" x="18" y="13" width="1" height="1"/><rect fill="#bf64cc" x="19" y="13" width="1" height="1"/><rect fill="#bf64cc" x="20" y="13" width="1" height="1"/><rect fill="#be63cc6e" x="21" y="13" width="1" height="1"/><rect fill="#be64cc6b" x="4" y="14" width="1" height="1"/><rect fill="#bf64cc" x="5" y="14" width="1" height="1"/><rect fill="#bf64cc" x="6" y="14" width="1" height="1"/><rect fill="#bf64cc" x="7" y="14" width="1" height="1"/><rect fill="#bf64cc" x="8" y="14" width="1" height="1"/><rect fill="#c066cd" x="9" y="14" width="1" height="1"/><rect fill="#c97cd4" x="10" y="14" width="1" height="1"/><rect fill="#c674d1" x="11" y="14" width="1" height="1"/><rect fill="#bf64cc" x="12" y="14" width="1" height="1"/><rect fill="#bf64cc" x="13" y="14" width="1" height="1"/><rect fill="#bf65cc" x="14" y="14" width="1" height="1"/><rect fill="#daa5e1" x="15" y="14" width="1" height="1"/><rect fill="#fdfafd" x="16" y="14" width="1" height="1"/><rect fill="#ffffff" x="17" y="14" width="1" height="1"/><rect fill="#f3e1f5" x="18" y="14" width="1" height="1"/><rect fill="#bf64cc" x="19" y="14" width="1" height="1"/><rect fill="#bf64cc" x="20" y="14" width="1" height="1"/><rect fill="#bd64cb5e" x="21" y="14" width="1" height="1"/><rect fill="#be64cba3" x="4" y="15" width="1" height="1"/><rect fill="#bf64cc" x="5" y="15" width="1" height="1"/><rect fill="#c879d3" x="6" y="15" width="1" height="1"/><rect fill="#ecd0f0" x="7" y="15" width="1" height="1"/><rect fill="#c97dd4" x="8" y="15" width="1" height="1"/><rect fill="#bf64cc" x="9" y="15" width="1" height="1"/><rect fill="#bf64cc" x="10" y="15" width="1" height="1"/><rect fill="#bf64cc" x="11" y="15" width="1" height="1"/><rect fill="#bf64cc" x="12" y="15" width="1" height="1"/><rect fill="#ca7ed5" x="13" y="15" width="1" height="1"/><rect fill="#f0dcf3" x="14" y="15" width="1" height="1"/><rect fill="#ffffff" x="15" y="15" width="1" height="1"/><rect fill="#ffffff" x="16" y="15" width="1" height="1"/><rect fill="#fefdfe" x="17" y="15" width="1" height="1"/><rect fill="#d395dc" x="18" y="15" width="1" height="1"/><rect fill="#bf64cc" x="19" y="15" width="1" height="1"/><rect fill="#be64cbfe" x="20" y="15" width="1" height="1"/><rect fill="#bd62cd1f" x="21" y="15" width="1" height="1"/><rect fill="#be63cbae" x="4" y="16" width="1" height="1"/><rect fill="#bf64cc" x="5" y="16" width="1" height="1"/><rect fill="#cb81d6" x="6" y="16" width="1" height="1"/><rect fill="#ffffff" x="7" y="16" width="1" height="1"/><rect fill="#fffeff" x="8" y="16" width="1" height="1"/><rect fill="#d59ade" x="9" y="16" width="1" height="1"/><rect fill="#bf64cc" x="10" y="16" width="1" height="1"/><rect fill="#c168cd" x="11" y="16" width="1" height="1"/><rect fill="#dfb2e6" x="12" y="16" width="1" height="1"/><rect fill="#fdfbfe" x="13" y="16" width="1" height="1"/><rect fill="#ffffff" x="14" y="16" width="1" height="1"/><rect fill="#ffffff" x="15" y="16" width="1" height="1"/><rect fill="#faf3fb" x="16" y="16" width="1" height="1"/><rect fill="#d496dd" x="17" y="16" width="1" height="1"/><rect fill="#bf64cc" x="18" y="16" width="1" height="1"/><rect fill="#bf64cc" x="19" y="16" width="1" height="1"/><rect fill="#be63cca7" x="20" y="16" width="1" height="1"/><rect fill="#bf64cb8c" x="4" y="17" width="1" height="1"/><rect fill="#bf64cc" x="5" y="17" width="1" height="1"/><rect fill="#c36dcf" x="6" y="17" width="1" height="1"/><rect fill="#fbf6fc" x="7" y="17" width="1" height="1"/><rect fill="#ffffff" x="8" y="17" width="1" height="1"/><rect fill="#f7ecf9" x="9" y="17" width="1" height="1"/><rect fill="#deafe5" x="10" y="17" width="1" height="1"/><rect fill="#f5e7f7" x="11" y="17" width="1" height="1"/><rect fill="#ffffff" x="12" y="17" width="1" height="1"/><rect fill="#ffffff" x="13" y="17" width="1" height="1"/><rect fill="#ffffff" x="14" y="17" width="1" height="1"/><rect fill="#e8c8ed" x="15" y="17" width="1" height="1"/><rect fill="#c571d0" x="16" y="17" width="1" height="1"/><rect fill="#bf64cc" x="17" y="17" width="1" height="1"/><rect fill="#bf64cc" x="18" y="17" width="1" height="1"/><rect fill="#bf64cbd5" x="19" y="17" width="1" height="1"/><rect fill="#b863c612" x="20" y="17" width="1" height="1"/><rect fill="#bf66cc3c" x="4" y="18" width="1" height="1"/><rect fill="#bf64cc" x="5" y="18" width="1" height="1"/><rect fill="#bf64cc" x="6" y="18" width="1" height="1"/><rect fill="#ddaee4" x="7" y="18" width="1" height="1"/><rect fill="#ffffff" x="8" y="18" width="1" height="1"/><rect fill="#ffffff" x="9" y="18" width="1" height="1"/><rect fill="#ffffff" x="10" y="18" width="1" height="1"/><rect fill="#ffffff" x="11" y="18" width="1" height="1"/><rect fill="#ffffff" x="12" y="18" width="1" height="1"/><rect fill="#f7ecf9" x="13" y="18" width="1" height="1"/><rect fill="#d190db" x="14" y="18" width="1" height="1"/><rect fill="#bf64cc" x="15" y="18" width="1" height="1"/><rect fill="#bf64cc" x="16" y="18" width="1" height="1"/><rect fill="#bf64cc" x="17" y="18" width="1" height="1"/><rect fill="#bf63cba4" x="18" y="18" width="1" height="1"/><rect fill="#b65bc80e" x="19" y="18" width="1" height="1"/><rect fill="#be64cbb2" x="5" y="19" width="1" height="1"/><rect fill="#bf64cc" x="6" y="19" width="1" height="1"/><rect fill="#bf64cc" x="7" y="19" width="1" height="1"/><rect fill="#daa5e1" x="8" y="19" width="1" height="1"/><rect fill="#f8edf9" x="9" y="19" width="1" height="1"/><rect fill="#ffffff" x="10" y="19" width="1" height="1"/><rect fill="#fbf5fc" x="11" y="19" width="1" height="1"/><rect fill="#e1b5e7" x="12" y="19" width="1" height="1"/><rect fill="#c26bce" x="13" y="19" width="1" height="1"/><rect fill="#bf64cc" x="14" y="19" width="1" height="1"/><rect fill="#bf64cc" x="15" y="19" width="1" height="1"/><rect fill="#be63cbdf" x="16" y="19" width="1" height="1"/><rect fill="#bf63cd48" x="17" y="19" width="1" height="1"/><rect fill="#c369d211" x="5" y="20" width="1" height="1"/><rect fill="#bf64cbd1" x="6" y="20" width="1" height="1"/><rect fill="#bf64cc" x="7" y="20" width="1" height="1"/><rect fill="#bf64cc" x="8" y="20" width="1" height="1"/><rect fill="#bf64cc" x="9" y="20" width="1" height="1"/><rect fill="#c26bce" x="10" y="20" width="1" height="1"/><rect fill="#bf64cc" x="11" y="20" width="1" height="1"/><rect fill="#bf64cc" x="12" y="20" width="1" height="1"/><rect fill="#bf64cc" x="13" y="20" width="1" height="1"/><rect fill="#bf64ccfc" x="14" y="20" width="1" height="1"/><rect fill="#be64cc8e" x="15" y="20" width="1" height="1"/><rect fill="#b95cd00b" x="16" y="20" width="1" height="1"/><rect fill="#bf6ad40c" x="6" y="21" width="1" height="1"/><rect fill="#be64cc93" x="7" y="21" width="1" height="1"/><rect fill="#bf64cbf9" x="8" y="21" width="1" height="1"/><rect fill="#bf64cc" x="9" y="21" width="1" height="1"/><rect fill="#bf64cc" x="10" y="21" width="1" height="1"/><rect fill="#bf64cc" x="11" y="21" width="1" height="1"/><rect fill="#bf64cc" x="12" y="21" width="1" height="1"/><rect fill="#be63cbc3" x="13" y="21" width="1" height="1"/><rect fill="#bf62cd34" x="14" y="21" width="1" height="1"/><rect fill="#c369d211" x="8" y="22" width="1" height="1"/><rect fill="#be65ca53" x="9" y="22" width="1" height="1"/><rect fill="#bf64cc70" x="10" y="22" width="1" height="1"/><rect fill="#bd62ca62" x="11" y="22" width="1" height="1"/><rect fill="#bd64c92b" x="12" y="22" width="1" height="1"/></svg> \ No newline at end of file
diff --git a/src/lib/index.ts b/src/lib/index.ts
new file mode 100644
index 0000000..856f2b6
--- /dev/null
+++ b/src/lib/index.ts
@@ -0,0 +1 @@
+// place files you want to import through the `$lib` alias in this folder.
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100644
index 0000000..20f8d04
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,11 @@
+<script lang="ts">
+ import favicon from '$lib/assets/favicon.svg';
+
+ let { children } = $props();
+</script>
+
+<svelte:head>
+ <link rel="icon" href={favicon} />
+</svelte:head>
+
+{@render children?.()}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
new file mode 100644
index 0000000..7cc3eb7
--- /dev/null
+++ b/src/routes/+page.svelte
@@ -0,0 +1,202 @@
+<!--
+MIT License
+
+Copyright (c) 2025 memdmp
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ -->
+
+<script lang="ts">
+ import Alerts, { alert, confirm } from '$lib/Alerts.svelte';
+ import { onMount } from 'svelte';
+
+ let fileInput = $state(null as unknown as HTMLInputElement);
+ let fileName = $state('');
+ let fileRaw = $state(null as null | File);
+ let canvas = $state(null as unknown as HTMLCanvasElement);
+ let imgUrl = $state('');
+ let output = $state('');
+ $effect(() => {
+ if (fileInput?.files?.length) {
+ const file = fileInput.files![0];
+ fileName = file.name;
+ fileRaw = file ?? null;
+ }
+ });
+ onMount(() => {
+ const el = document.createElement('canvas');
+ const ctx = el.getContext('2d');
+ if (!ctx) {
+ alert(
+ "Could not create 2d Canvas Context. This application will not work. Make sure any privacy-resistance features are disabled and your browser isn't ancient.",
+ {
+ title: 'Fatal Error',
+ }
+ );
+ } else {
+ if (
+ JSON.stringify(ctx.getImageData(0, 0, 32, 32).data) !==
+ JSON.stringify(ctx.getImageData(0, 0, 32, 32).data)
+ ) {
+ alert(
+ "Canvas readbacks not identical. This application may not work correctly. Make sure any privacy-resistance features are disabled and your browser isn't ancient.",
+ {
+ title: 'Error',
+ }
+ );
+ }
+ }
+ });
+ const update = async (fileRaw: File) => {
+ const blob = new Blob([await fileRaw.bytes()]);
+ const blobUrl = URL.createObjectURL(blob);
+ imgUrl = blobUrl;
+ };
+ $effect(() => {
+ if (fileRaw) update(fileRaw);
+ else imgUrl = '';
+ });
+ const toHex = (v: number) => Math.floor(v).toString(16).padStart(2, '0');
+ const save = (
+ data: [r: number, g: number, b: number, a: number][],
+ width: number,
+ height: number
+ ) => {
+ let content = `<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${width} ${height}`)} width=${JSON.stringify('' + width)} height=${JSON.stringify('' + height)}>`;
+ let item: (typeof data)[0];
+ let x = 0;
+ let y = 0;
+ while ((item = data.shift()!)) {
+ if (++x >= width) (x = 0), y++;
+ if (item[3] !== 0)
+ content += `<rect fill="#${toHex(item[0])}${toHex(item[1])}${toHex(item[2])}${item[3] !== 255 ? toHex(item[3]) : ''}" x=${JSON.stringify('' + x)} y=${JSON.stringify('' + y)} width="1" height="1"/>`;
+ }
+ content += '</svg>';
+ output = URL.createObjectURL(
+ new Blob([content], {
+ type: 'image/svg+xml',
+ })
+ );
+ };
+</script>
+
+<svelte:head>
+ <title>Bitmap to Pixelated SVG</title>
+ <link rel="stylesheet" href="/cs16.css" />
+</svelte:head>
+
+<Alerts />
+<div>
+ <h1>PixelSVG</h1>
+ <div>
+ <label style="display: flex; gap:4px;">
+ <span class="cs-btn"
+ >{fileName ? `File: ${fileName}` : 'Upload a File'}</span
+ >
+ <img
+ src={imgUrl}
+ alt=""
+ style="height: 24px"
+ onload={async (e) => {
+ const imgEl = e.currentTarget as HTMLImageElement;
+ const rect = {
+ width: imgEl.naturalWidth,
+ height: imgEl.naturalHeight,
+ };
+ if (
+ rect.width * rect.height > 65536 &&
+ !(await confirm(
+ `This image isn't small; it's made up of ${rect.width * rect.height} pixels. The outputted file could be up to ${((`<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${rect.width} ${rect.height}`)} width=${JSON.stringify('' + rect.width)} height=${JSON.stringify('' + rect.height)}></svg>`.length + `<rect fill="#ffffffff" x="${rect.width}" y="${rect.height}" width="1" height="1"/>`.length * rect.width * rect.height) / 1024 / 1024).toFixed(1)} MB large. It may crash your browser. Are you sure you wish to continue?`,
+ {
+ title: 'Confirmation',
+ }
+ ))
+ )
+ return (imgUrl = ''), (fileName = '');
+ canvas.width = rect.width;
+ canvas.height = rect.height;
+ const ctx = canvas.getContext('2d', {
+ alpha: true,
+ colorSpace: 'display-p3',
+ });
+ if (!ctx) throw new Error('no 2d context obtainable');
+ try {
+ ctx.drawImage(e.currentTarget as HTMLImageElement, 0, 0);
+ } catch (error) {
+ await alert(
+ "Failed to draw image to canvas. The image's data may be invalid or the image may be too big. Check the Console.",
+ {
+ title: 'Failed to draw image',
+ }
+ );
+ throw error;
+ }
+ const data = ctx.getImageData(0, 0, rect.width, rect.height, {
+ colorSpace: 'display-p3',
+ });
+ let out = [] as [r: number, g: number, b: number, a: number][];
+ for (let i = 0; i < data.data.length; i += 4) {
+ const r = data.data[i + 0];
+ const g = data.data[i + 1];
+ const b = data.data[i + 2];
+ const a = data.data[i + 3];
+ out.push([r, g, b, a]);
+ }
+ save(out, rect.width, rect.height);
+ }}
+ onerror={(e) => {
+ if (imgUrl !== '')
+ alert(
+ 'Failed to load the image into an HTML Image Element. Please make sure the image you provided is valid and in a format your browser understands.',
+ {
+ title: 'Failed to load image',
+ }
+ );
+ }}
+ />
+ <input
+ type="file"
+ bind:this={fileInput}
+ onchange={() => {
+ if (fileInput?.files?.length) {
+ const file = fileInput.files![0];
+ fileName = file.name;
+ fileRaw = file ?? null;
+ }
+ }}
+ style="opacity:0;position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;"
+ />
+ </label>
+ </div>
+ {#if output}
+ <div style="margin-top:12px;margin-bottom:12px;">
+ Output:<br />
+ <a href={output} target="_blank"
+ ><img src={output} alt="could not render" /> (open)</a
+ >
+ </div>
+ {/if}
+ <div style="opacity:0.5">
+ {imgUrl ? 'Canvas:' : ''}<br /><canvas
+ bind:this={canvas}
+ width="0"
+ height="0"
+ ></canvas>
+ </div>
+</div>