From 0d2687d1dee3bd98bf72a2186c72e8f1512b70fd Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Sun, 25 Jan 2026 19:21:05 -0700
Subject: [PATCH 01/21] add copy button on images
---
package-lock.json | 25 +++++++++
package.json | 1 +
src/app.html | 2 +-
src/lib/Extension/Component.svelte | 89 +++++++++++++++++++++++-------
4 files changed, 95 insertions(+), 22 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 805e4e9f4..6a6c06a3f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "penguinmod-extensionsgallery",
"version": "0.0.1",
"dependencies": {
+ "localforage": "^1.10.0",
"markdown-it": "^14.1.0"
},
"devDependencies": {
@@ -1132,6 +1133,12 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -1152,6 +1159,15 @@
"node": ">=6"
}
},
+ "node_modules/lie": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -1161,6 +1177,15 @@
"uc.micro": "^2.0.0"
}
},
+ "node_modules/localforage": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lie": "3.1.1"
+ }
+ },
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
diff --git a/package.json b/package.json
index 8ee094f80..3278512a8 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
},
"type": "module",
"dependencies": {
+ "localforage": "^1.10.0",
"markdown-it": "^14.1.0"
}
}
diff --git a/src/app.html b/src/app.html
index 01be09850..4fddeb91d 100644
--- a/src/app.html
+++ b/src/app.html
@@ -21,7 +21,7 @@
diff --git a/src/lib/extension-tags.js b/src/lib/extension-tags.js
new file mode 100644
index 000000000..f0750666f
--- /dev/null
+++ b/src/lib/extension-tags.js
@@ -0,0 +1,54 @@
+/*
+NOTE: This file manages aliases & groupings for tags.
+To make a new tag or list it, add it to an extension in `extensions.js`
+*/
+export const Tags = [
+ {
+ name: "new",
+ banner: "/icons/tag-banners/new.svg",
+ group: "extensiontypes",
+ },
+ {
+ name: "addons",
+ alias: "Editor Addons",
+ group: "extensiontypes",
+ },
+ {
+ name: "expansion",
+ alias: "Category Expansions",
+ group: "extensiontypes",
+ },
+ {
+ name: "collection",
+ alias: "Extension Collections",
+ group: "extensiontypes",
+ },
+
+ {
+ name: "customtype",
+ alias: "New Block Type",
+ },
+ {
+ name: "genai",
+ alias: "Generative AI",
+ },
+ {
+ name: "ai",
+ alias: "Algorithms and AI",
+ },
+ {
+ name: "large",
+ alias: "Large Extensions",
+ },
+ {
+ name: "small",
+ alias: "Small Extensions",
+ },
+];
+
+export const DefaultTag = {
+ name: "",
+ // alias: "",
+ // banner: "/icons/tag-banners/new.svg",
+ group: "ungrouped",
+};
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 101707d4c..e04fc12d8 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -18,6 +18,8 @@
return `${origin}/extensions/${relativeUrl}`;
};
+ // searching & filtering
+ let filterBarOpened = $state(false);
let showNoExtensionsFound = $state(false);
$effect(() => {
const matchingExts = extensions
@@ -40,7 +42,7 @@
PenguinMod Extra Extensions
-
+
See some cool extensions made by other people here.
To use some of these extensions in your projects, click the "Copy Link"
@@ -48,60 +50,129 @@
load it into PenguinMod,
or click the "Try it out" button to create a new project with the extension.
+
+
+
+ { filterBarOpened = !filterBarOpened; }}>Show filters
+
+ Sort by
+
+ recommended order
+ reversed recommended order
+ names A-Z
+ names Z-A
+ creators A-Z
+ creators Z-A
+
+
+
+
+
+
+
Filters
+
+
Tags
+
+
+
+ Graphics
+
+
+
+ Sound
+
+
+
+ Math
+
+
+
+ Jokes
+
+
+
+
+ Editor Addons
+
-
+
Note: Some extensions may be added to the Extension Gallery in
- PenguinMod Studio. If you cannot find an extension that was
+ PenguinMod Studio.
+
+ If you cannot find an extension that was
previously listed here, check there.
@@ -114,6 +185,10 @@
color: white;
}
+ label {
+ display: block;
+ }
+
.top {
width: 100%;
@@ -129,7 +204,7 @@
font-size: 1.35em;
}
- .main {
+ .buffer {
width: 80%;
margin-left: 10%;
@@ -137,6 +212,58 @@
flex-direction: column;
}
+ .extension-list-controls {
+ width: 80%;
+ height: 24px;
+ margin-left: 10%;
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+ .extension-list-controls button {
+ height: 100%;
+ border: 0;
+
+ cursor: pointer;
+ }
+
+ .extension-list-container {
+ width: 80%;
+ margin-left: 10%;
+
+ display: flex;
+ flex-direction: column;
+ }
+ .extension-list-container[data-filteropen="true"] {
+ width: 100%;
+ margin-left: initial;
+ }
+ .extension-list-bars {
+ width: 100%;
+
+ display: flex;
+ flex-direction: row;
+ }
+ .extension-list-filters {
+ display: none;
+ width: calc(20% - (16px + 16px + 2px));
+ border: 1px solid black;
+ margin: 8px;
+ padding: 0px 8px;
+
+ border-radius: 16px;
+ }
+ .extension-list-filters[data-filteropen="true"] {
+ display: initial;
+ }
+ .extension-list-filters-label {
+ display: block;
+ margin-top: 8px;
+
+ font-style: italic;
+ opacity: 0.7;
+ }
.extension-list {
width: 100%;
@@ -145,6 +272,10 @@
flex-wrap: wrap;
justify-content: center;
}
+ .extension-list[data-filteropen="true"] {
+ width: calc(100% - 20%);
+ }
+
.no-exts {
padding: 8px 32px;
border: 1px solid rgba(0, 0, 0, 0.25);
diff --git a/static/icons/tag-banners/new.svg b/static/icons/tag-banners/new.svg
new file mode 100644
index 000000000..3bc6959ff
--- /dev/null
+++ b/static/icons/tag-banners/new.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 078ed4573c3debfef99d6d6c94285596ba288891 Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Sun, 25 Jan 2026 21:10:39 -0700
Subject: [PATCH 06/21] make tag list based on yk the listed tags
---
src/lib/extension-tags.js | 19 ++++++---
src/routes/+page.svelte | 84 +++++++++++++++++++++++++++++----------
2 files changed, 77 insertions(+), 26 deletions(-)
diff --git a/src/lib/extension-tags.js b/src/lib/extension-tags.js
index f0750666f..5a0958632 100644
--- a/src/lib/extension-tags.js
+++ b/src/lib/extension-tags.js
@@ -3,6 +3,12 @@ NOTE: This file manages aliases & groupings for tags.
To make a new tag or list it, add it to an extension in `extensions.js`
*/
export const Tags = [
+ // reserved tags (dont add these to an extension)
+ {
+ name: "separator",
+ },
+
+ // extensiontypes
{
name: "new",
banner: "/icons/tag-banners/new.svg",
@@ -24,6 +30,7 @@ export const Tags = [
group: "extensiontypes",
},
+ // any other misc ones that just need aliases
{
name: "customtype",
alias: "New Block Type",
@@ -46,9 +53,11 @@ export const Tags = [
},
];
-export const DefaultTag = {
- name: "",
- // alias: "",
- // banner: "/icons/tag-banners/new.svg",
- group: "ungrouped",
+export function makeDefaultTag() {
+ return {
+ name: "",
+ // alias: "",
+ // banner: "/icons/tag-banners/new.svg",
+ group: "ungrouped",
+ };
};
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index e04fc12d8..f9e4e4886 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,4 +1,5 @@
@@ -110,22 +131,22 @@
+ />
+ />
-
- recommended order
- reversed recommended order
- names A-Z
- names Z-A
- creators A-Z
- creators Z-A
+
+ Recommended order
+ Reversed recommended order
+ Names from A-Z
+ Names from Z-A
+ Creators from A-Z
+ Creators from Z-A
@@ -137,10 +158,14 @@
Tags
{#each tagsListShown as extensionTag}
{#if extensionTag.name === "separator"}
-
+
{:else}
-
+
{extensionTag.alias}
{/if}
@@ -191,29 +216,26 @@
- {#each extensions as extension}
- {#if searchable(extension.name).includes(stateSearchBar.query)}
-
- {extension.description}
-
- {/if}
- {/each}
- {#if showNoExtensionsFound}
+ {#each shownExtensions as extension}
+
+ {extension.description}
+
+ {:else}
No extensions match your selected filters.
- {/if}
+ {/each}
From 10482546625bbee283554fb5cee015e0a3404ced Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Mon, 26 Jan 2026 00:58:57 -0700
Subject: [PATCH 10/21] make features toggles work
---
src/lib/Extension/Component.svelte | 7 +++++--
src/routes/+page.svelte | 29 +++++++++++++++++++----------
2 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/src/lib/Extension/Component.svelte b/src/lib/Extension/Component.svelte
index ac1e0d970..274493441 100644
--- a/src/lib/Extension/Component.svelte
+++ b/src/lib/Extension/Component.svelte
@@ -356,13 +356,16 @@
.unstable-message {
display: none;
position: absolute;
+ width: 250px;
+ padding: 8px;
+
background: #000000de;
font-size: medium;
font-weight: normal;
- padding: 8px;
border-radius: 8px;
white-space: pre-wrap;
- width: 250px;
+
+ z-index: 60;
user-select: text;
}
.unstable-warning:hover .unstable-message {
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 1fee6b6be..0b383cdca 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -77,10 +77,19 @@
let filterBarOpened = $state(false);
let selectedSorting = $state("none");
const tagsSelected = $state({});
+ const featuresSelected = $state({
+ documentation: "show",
+ exampleprojects: "show",
+ warnings: "show",
+ });
const updateExtensionList = () => {
shownExtensions = [...extensions]
.filter(extension => searchable(extension.name).includes(stateSearchBar.query))
- .filter(extension => Object.values(tagsSelected).some(bool => !!bool) ? (extension.tags || []).find(extTag => tagsSelected[extTag] === true) : true);
+ .filter(extension => Object.values(tagsSelected).some(bool => !!bool) ? (extension.tags || []).find(extTag => tagsSelected[extTag] === true) : true)
+ .filter(extension => featuresSelected.documentation === "exclusive" ? (!!extension.documentation) : (featuresSelected.documentation === "hide" ? !extension.documentation : true))
+ .filter(extension => featuresSelected.exampleprojects === "exclusive" ? (!!extension.example) : (featuresSelected.exampleprojects === "hide" ? !extension.example : true))
+ .filter(extension => featuresSelected.warnings === "exclusive" ? (!!extension.unstable) : (featuresSelected.warnings === "hide" ? !extension.unstable : true))
+ ;
if (selectedSorting === "namedesc" || selectedSorting === "nameasce") {
shownExtensions.sort((a, b) => a.name.localeCompare(b.name));
@@ -174,43 +183,43 @@
Features
Documentation
-
+
Show extensions with documentation
-
+
Only show extensions with documentation
-
+
Hide extensions with documentation
Example projects
-
+
Show extensions with example projects
-
+
Only show extensions with example projects
-
+
Hide extensions with example projects
Warnings
-
+
Show extensions with warnings
-
+
Only show extensions with warnings
-
+
Hide extensions with warnings
From 35cb9dd32dab0288e0f4e8f557cb2c6ba184c8f3 Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Mon, 26 Jan 2026 01:10:50 -0700
Subject: [PATCH 11/21] add example projects
---
src/lib/Extension/Component.svelte | 10 +++++++++-
src/routes/+page.svelte | 1 +
static/examples/projects/JeremyGamer13/test.pmp | Bin 0 -> 19534 bytes
3 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 static/examples/projects/JeremyGamer13/test.pmp
diff --git a/src/lib/Extension/Component.svelte b/src/lib/Extension/Component.svelte
index 274493441..b12b2c1e6 100644
--- a/src/lib/Extension/Component.svelte
+++ b/src/lib/Extension/Component.svelte
@@ -1,5 +1,6 @@
{
text = String(text);
@@ -20,6 +23,34 @@
const createExtUrl = (relativeUrl) => {
return `${origin}/extensions/${relativeUrl}`;
};
+ $effect(() => {
+ if (messageHandlersAdded) return;
+ if (!stateApplication.fromEditor) return;
+ console.log("Loaded from editor (supposedly)");
+
+ window.addEventListener("message", (e) => {
+ try {
+ const successfulLoad = ExtensionLoader.handleWindowMessage(e);
+ if (successfulLoad) {
+ const event = new CustomEvent("penguinmod-editor-extension-loaded");
+ document.dispatchEvent(event);
+ }
+ } catch (err) {
+ const event = new CustomEvent("penguinmod-editor-extension-load-failed", { detail: err });
+ document.dispatchEvent(event);
+ }
+ });
+ document.addEventListener("penguinmod-editor-extension-load-failed", (event) => {
+ const err = event.detail;
+ console.error("Error loading extension to editor;", err);
+
+ switch (err) {
+ default:
+ alert("Failed to import the extension to your project!\nMake sure the \"Choose an Extension\" menu is still open in your project.");
+ }
+ });
+ messageHandlersAdded = true;
+ });
// create the tag groups for the filter menu
const tagGrouping = {};
@@ -127,12 +158,21 @@
See some cool extensions made by other people here.
-
- To use some of these extensions in your projects, click the "Copy Link"
- button on an extension and
- load it into PenguinMod,
- or click the "Try it out" button to create a new project with the extension.
-
+ {#if stateApplication.fromEditor}
+
+ To add an extension to your project, click the "Add to Project" button.
+ You can also click the "Copy" button and
+ load it into PenguinMod
+ if the former fails.
+
+ {:else}
+
+ To use some of these extensions in your projects, click the "Copy Link"
+ button on an extension and
+ load it into PenguinMod,
+ or click the "Try it out" button to create a new project with the extension.
+
+ {/if}
From 51851ba38106c48b4115ab271fb8b5df6719e2f3 Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Mon, 26 Jan 2026 03:18:25 -0700
Subject: [PATCH 15/21] show Added to project bubble
---
src/lib/Extension/Component.svelte | 5 +++++
src/lib/extension-loader.js | 4 ++--
src/routes/+page.svelte | 6 +++---
3 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/lib/Extension/Component.svelte b/src/lib/Extension/Component.svelte
index 61f2bb6d5..482cf980e 100644
--- a/src/lib/Extension/Component.svelte
+++ b/src/lib/Extension/Component.svelte
@@ -114,6 +114,11 @@
const event = new CustomEvent("penguinmod-editor-extension-load-failed", { detail: err });
document.dispatchEvent(event);
};
+ onMount(() => {
+ document.addEventListener("penguinmod-editor-extension-loaded", (event) => {
+ if (url === event.detail) displayBubbleMessage(addToProjectPrompt);
+ });
+ });
// tags that have banners
let displayedTags = $derived.by(() => {
diff --git a/src/lib/extension-loader.js b/src/lib/extension-loader.js
index 9c150bef8..9bc85a7e5 100644
--- a/src/lib/extension-loader.js
+++ b/src/lib/extension-loader.js
@@ -13,7 +13,7 @@ class ExtensionLoader {
}, origin);
}
static handleWindowMessage(e) {
- // return false, invalid message, return true, success, throw error, something failed
+ // return false, invalid message; return extension "id", success; throw error, something failed
const intendedOrigin = ExtensionLoader.getTargetOrigin();
console.log('Recieved message from', e.origin, e);
@@ -38,7 +38,7 @@ class ExtensionLoader {
// evil win
if (eventData.type === 'success') {
console.log('Loading extension was a success', eventData);
- return true;
+ return eventData.extensionId;
}
// evil fail
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 8bcc5c2db..ad93e4c0a 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -30,9 +30,9 @@
window.addEventListener("message", (e) => {
try {
- const successfulLoad = ExtensionLoader.handleWindowMessage(e);
- if (successfulLoad) {
- const event = new CustomEvent("penguinmod-editor-extension-loaded");
+ const loadedExtensionId = ExtensionLoader.handleWindowMessage(e);
+ if (loadedExtensionId) {
+ const event = new CustomEvent("penguinmod-editor-extension-loaded", { detail: loadedExtensionId });
document.dispatchEvent(event);
}
} catch (err) {
From 116be4fe7f473474c8a530a2f25fd084411ee24e Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Mon, 26 Jan 2026 03:51:45 -0700
Subject: [PATCH 16/21] add option saving
---
src/routes/+page.svelte | 70 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 69 insertions(+), 1 deletion(-)
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index ad93e4c0a..1eb5d9a0a 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { browser } from "$app/environment";
+ import localforage from "localforage";
// Components
import Extension from "$lib/Extension/Component.svelte";
@@ -104,16 +105,48 @@
});
// searching & filtering
+ let hasStorageBeenLoaded = false;
let shownExtensions = $state([]);
let filterBarOpened = $state(false);
let selectedSorting = $state("none");
+ let favoritedExtensions = $state([]);
const tagsSelected = $state({});
const featuresSelected = $state({
documentation: "show",
exampleprojects: "show",
warnings: "show",
+ favorites: "show",
+ favoritessplit: true,
});
+ const saveToStorage = async () => {
+ // NOTE: If saveToStorage gets called on the server then this will cause Vite to crash (so dont call it outside of any function)
+ await localforage.setItem("pm:filter-bar-open", $state.snapshot(filterBarOpened));
+ await localforage.setItem("pm:sorting", $state.snapshot(selectedSorting));
+ await localforage.setItem("pm:filters-tags", $state.snapshot(tagsSelected));
+ await localforage.setItem("pm:filters-features", $state.snapshot(featuresSelected));
+ await localforage.setItem("pm:favorites", $state.snapshot(favoritedExtensions));
+ };
+ const loadFromStorage = async () => {
+ const localFilterBarOpened = await localforage.getItem("pm:filter-bar-open");
+ const localSelectedSorting = await localforage.getItem("pm:sorting");
+ const localTagsSelected = await localforage.getItem("pm:filters-tags");
+ const localFeaturesSelected = await localforage.getItem("pm:filters-features");
+ const localFavoritedExtensions = await localforage.getItem("pm:favorites");
+ filterBarOpened = localFilterBarOpened;
+ selectedSorting = localSelectedSorting;
+ for (const key in localTagsSelected) {
+ tagsSelected[key] = localTagsSelected[key];
+ }
+ for (const key in localFeaturesSelected) {
+ featuresSelected[key] = localFeaturesSelected[key];
+ }
+ favoritedExtensions = localFavoritedExtensions;
+
+ // reproececss
+ updateExtensionList();
+ };
const updateExtensionList = () => {
+ // update the list
shownExtensions = [...extensions]
.filter(extension => searchable(extension.name).includes(stateSearchBar.query))
.filter(extension => Object.values(tagsSelected).some(bool => !!bool) ? (extension.tags || []).find(extTag => tagsSelected[extTag] === true) : true)
@@ -131,6 +164,15 @@
if (selectedSorting === "reversed" || selectedSorting === "nameasce" || selectedSorting === "creatorasce") {
shownExtensions.reverse();
}
+
+ // update localforage
+ if (hasStorageBeenLoaded) saveToStorage();
+ };
+ const clearTags = () => {
+ for (const key in tagsSelected) {
+ tagsSelected[key] = false;
+ }
+ updateExtensionList();
};
$effect(() => {
// goive recommendatiaons based on the searched things
@@ -142,12 +184,16 @@
const event = new CustomEvent("penguinmod-recommendations-updated");
document.dispatchEvent(event);
});
+ onMount(async () => {
+ await loadFromStorage();
+ hasStorageBeenLoaded = true;
+ });
if (browser) {
document.addEventListener("penguinmod-search-bar-input", () => {
updateExtensionList();
});
+ updateExtensionList();
}
- updateExtensionList();
@@ -219,6 +265,9 @@
{/if}
{/each}
+
+ Clear tags
+
Features
Documentation
@@ -262,6 +311,25 @@
Hide extensions with warnings
+
+ Favorites
+
+
+ Show favorited extensions
+
+
+
+ Only show favorited extensions
+
+
+
+ Hide favorited extensions
+
+
+
+
+ Separate favorited extensions
+
From 25b6c1157623b0034e23860c731f142d4ee79704 Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Sun, 1 Feb 2026 23:28:27 -0700
Subject: [PATCH 17/21] upgrade UI
---
src/lib/Checkbox/Checkbox.svelte | 78 +++++++++++++
src/lib/Checkbox/icon-enabled.svg | 8 ++
.../ThreeStateCheckbox.svelte | 83 ++++++++++++++
src/lib/ThreeStateCheckbox/icon-enabled.svg | 8 ++
src/lib/ThreeStateCheckbox/icon-third.svg | 8 ++
src/routes/+page.svelte | 108 ++++++++----------
6 files changed, 231 insertions(+), 62 deletions(-)
create mode 100644 src/lib/Checkbox/Checkbox.svelte
create mode 100644 src/lib/Checkbox/icon-enabled.svg
create mode 100644 src/lib/ThreeStateCheckbox/ThreeStateCheckbox.svelte
create mode 100644 src/lib/ThreeStateCheckbox/icon-enabled.svg
create mode 100644 src/lib/ThreeStateCheckbox/icon-third.svg
diff --git a/src/lib/Checkbox/Checkbox.svelte b/src/lib/Checkbox/Checkbox.svelte
new file mode 100644
index 000000000..bfe4d8d3e
--- /dev/null
+++ b/src/lib/Checkbox/Checkbox.svelte
@@ -0,0 +1,78 @@
+
+
+
+
+ {#if checked}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/src/lib/Checkbox/icon-enabled.svg b/src/lib/Checkbox/icon-enabled.svg
new file mode 100644
index 000000000..ecea4cce4
--- /dev/null
+++ b/src/lib/Checkbox/icon-enabled.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/ThreeStateCheckbox/ThreeStateCheckbox.svelte b/src/lib/ThreeStateCheckbox/ThreeStateCheckbox.svelte
new file mode 100644
index 000000000..4a4039e5c
--- /dev/null
+++ b/src/lib/ThreeStateCheckbox/ThreeStateCheckbox.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+ {#if value === 1}
+
+ {:else if value === 2}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/src/lib/ThreeStateCheckbox/icon-enabled.svg b/src/lib/ThreeStateCheckbox/icon-enabled.svg
new file mode 100644
index 000000000..ecea4cce4
--- /dev/null
+++ b/src/lib/ThreeStateCheckbox/icon-enabled.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/ThreeStateCheckbox/icon-third.svg b/src/lib/ThreeStateCheckbox/icon-third.svg
new file mode 100644
index 000000000..e168efec0
--- /dev/null
+++ b/src/lib/ThreeStateCheckbox/icon-third.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 1eb5d9a0a..d6a68f8af 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -5,7 +5,9 @@
import localforage from "localforage";
// Components
+ import ThreeStateCheckbox from "$lib/ThreeStateCheckbox/ThreeStateCheckbox.svelte";
import Extension from "$lib/Extension/Component.svelte";
+ import Checkbox from "$lib/Checkbox/Checkbox.svelte";
import Footer from "$lib/Footer/Component.svelte";
import Logo from "$lib/Logo/Component.svelte";
@@ -112,12 +114,16 @@
let favoritedExtensions = $state([]);
const tagsSelected = $state({});
const featuresSelected = $state({
- documentation: "show",
- exampleprojects: "show",
- warnings: "show",
- favorites: "show",
+ documentation: 0,
+ exampleprojects: 0,
+ warnings: 0,
+ favorites: 0,
favoritessplit: true,
});
+ const toggleFilterBar = () => {
+ filterBarOpened = !filterBarOpened;
+ saveToStorage();
+ };
const saveToStorage = async () => {
// NOTE: If saveToStorage gets called on the server then this will cause Vite to crash (so dont call it outside of any function)
await localforage.setItem("pm:filter-bar-open", $state.snapshot(filterBarOpened));
@@ -150,9 +156,9 @@
shownExtensions = [...extensions]
.filter(extension => searchable(extension.name).includes(stateSearchBar.query))
.filter(extension => Object.values(tagsSelected).some(bool => !!bool) ? (extension.tags || []).find(extTag => tagsSelected[extTag] === true) : true)
- .filter(extension => featuresSelected.documentation === "exclusive" ? (!!extension.documentation) : (featuresSelected.documentation === "hide" ? !extension.documentation : true))
- .filter(extension => featuresSelected.exampleprojects === "exclusive" ? (!!extension.example) : (featuresSelected.exampleprojects === "hide" ? !extension.example : true))
- .filter(extension => featuresSelected.warnings === "exclusive" ? (!!extension.unstable) : (featuresSelected.warnings === "hide" ? !extension.unstable : true))
+ .filter(extension => featuresSelected.documentation === 1 ? (!!extension.documentation) : (featuresSelected.documentation === 2 ? !extension.documentation : true))
+ .filter(extension => featuresSelected.exampleprojects === 1 ? (!!extension.example) : (featuresSelected.exampleprojects === 2 ? !extension.example : true))
+ .filter(extension => featuresSelected.warnings === 1 ? (!!extension.unstable) : (featuresSelected.warnings === 2 ? !extension.unstable : true))
;
if (selectedSorting === "namedesc" || selectedSorting === "nameasce") {
@@ -222,7 +228,7 @@
@@ -377,10 +345,16 @@
}
label {
- display: block;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
user-select: none;
}
+ :global(*[data-penguinmodsvelteui-threestatecheckbox="true"]),
+ :global(*[data-penguinmodsvelteui-checkbox="true"]) {
+ margin-right: 4px !important;
+ }
.top {
width: 100%;
@@ -487,6 +461,16 @@
font-style: italic;
opacity: 0.7;
}
+ .extension-list-filters-clear {
+ border-color: rgba(0, 0, 0, 0.25);
+ border-radius: 3px;
+ background: dodgerblue;
+ color: white;
+ font-weight: bold;
+ font-size: 16px;
+
+ cursor: pointer;
+ }
.extension-list {
width: 100%;
From 7bd87dc71ca2b30415cf84e6f6a827c457023f9f Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Sun, 1 Feb 2026 23:54:13 -0700
Subject: [PATCH 18/21] add favorites
---
src/lib/Extension/Component.svelte | 22 +++++++++++++++++-
src/routes/+page.svelte | 37 ++++++++++++++++++++++--------
static/icons/favorite-filled.svg | 1 +
static/icons/favorite-outline.svg | 1 +
4 files changed, 50 insertions(+), 11 deletions(-)
create mode 100644 static/icons/favorite-filled.svg
create mode 100644 static/icons/favorite-outline.svg
diff --git a/src/lib/Extension/Component.svelte b/src/lib/Extension/Component.svelte
index 482cf980e..1333659ca 100644
--- a/src/lib/Extension/Component.svelte
+++ b/src/lib/Extension/Component.svelte
@@ -6,7 +6,10 @@
import stateSearchBar from '$lib/state/searchBar.svelte.js';
import ExtensionLoader from "$lib/extension-loader.js";
- let props = $props();
+ let {
+ favorited = $bindable(),
+ ...props
+ } = $props();
let name = $derived(props.name || "Test");
let image = $derived(props.image || "/images/example.avif");
let tags = $derived(props.tags || []);
@@ -201,6 +204,13 @@
{unstableReason}
{/if}
+
props?.onfavoriteclicked(relUrl)}>
+ {#if favorited}
+
+ {:else}
+
+ {/if}
+
{@render props.children?.()}
@@ -408,6 +418,7 @@
white-space: pre-wrap;
}
+ .favorite-button,
.unstable-warning {
position: relative;
display: inline;
@@ -417,6 +428,8 @@
margin: 0;
background: transparent;
+ }
+ .unstable-warning {
background-image: url('/icons/warning2.png');
background-position: center;
background-size: 80%;
@@ -447,6 +460,13 @@
.unstable-message:hover {
cursor: auto;
}
+ .favorite-button img {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 1.3em;
+ height: 1.3em;
+ }
.blue {
background-color: #00a2ff;
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d6a68f8af..b4b6c590b 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -111,7 +111,7 @@
let shownExtensions = $state([]);
let filterBarOpened = $state(false);
let selectedSorting = $state("none");
- let favoritedExtensions = $state([]);
+ let favoritedExtensions = $state({});
const tagsSelected = $state({});
const featuresSelected = $state({
documentation: 0,
@@ -133,11 +133,11 @@
await localforage.setItem("pm:favorites", $state.snapshot(favoritedExtensions));
};
const loadFromStorage = async () => {
- const localFilterBarOpened = await localforage.getItem("pm:filter-bar-open");
- const localSelectedSorting = await localforage.getItem("pm:sorting");
- const localTagsSelected = await localforage.getItem("pm:filters-tags");
- const localFeaturesSelected = await localforage.getItem("pm:filters-features");
- const localFavoritedExtensions = await localforage.getItem("pm:favorites");
+ const localFilterBarOpened = (await localforage.getItem("pm:filter-bar-open")) || false;
+ const localSelectedSorting = (await localforage.getItem("pm:sorting")) || "none";
+ const localTagsSelected = (await localforage.getItem("pm:filters-tags")) || {};
+ const localFeaturesSelected = (await localforage.getItem("pm:filters-features")) || {};
+ const localFavoritedExtensions = (await localforage.getItem("pm:favorites")) || {};
filterBarOpened = localFilterBarOpened;
selectedSorting = localSelectedSorting;
for (const key in localTagsSelected) {
@@ -146,7 +146,7 @@
for (const key in localFeaturesSelected) {
featuresSelected[key] = localFeaturesSelected[key];
}
- favoritedExtensions = localFavoritedExtensions;
+ favoritedExtensions = Array.isArray(localFavoritedExtensions) ? {} : localFavoritedExtensions;
// reproececss
updateExtensionList();
@@ -159,6 +159,7 @@
.filter(extension => featuresSelected.documentation === 1 ? (!!extension.documentation) : (featuresSelected.documentation === 2 ? !extension.documentation : true))
.filter(extension => featuresSelected.exampleprojects === 1 ? (!!extension.example) : (featuresSelected.exampleprojects === 2 ? !extension.example : true))
.filter(extension => featuresSelected.warnings === 1 ? (!!extension.unstable) : (featuresSelected.warnings === 2 ? !extension.unstable : true))
+ .filter(extension => featuresSelected.favorites === 1 ? (!!(favoritedExtensions[extension.code])) : (featuresSelected.favorites === 2 ? !(favoritedExtensions[extension.code]) : true))
;
if (selectedSorting === "namedesc" || selectedSorting === "nameasce") {
@@ -171,6 +172,13 @@
shownExtensions.reverse();
}
+ // split favorites
+ if (featuresSelected.favoritessplit) {
+ const notFavorites = shownExtensions.filter(extension => !(favoritedExtensions[extension.code]));
+ const favorites = shownExtensions.filter(extension => !!(favoritedExtensions[extension.code]));
+ shownExtensions = [].concat(favorites, notFavorites);
+ }
+
// update localforage
if (hasStorageBeenLoaded) saveToStorage();
};
@@ -180,6 +188,12 @@
}
updateExtensionList();
};
+ const onFavoriteClicked = (relUrl) => {
+ favoritedExtensions[relUrl] = !favoritedExtensions[relUrl];
+
+ // reproececss
+ updateExtensionList();
+ };
$effect(() => {
// goive recommendatiaons based on the searched things
stateSearchBar.recommendations = [];
@@ -267,10 +281,10 @@
{/if}
{:else}
-
No tags currently exist. Extension creators are encouraged to add some soon.
+
No tags currently exist.
{/each}
- Clear tags
+ Clear selected tags
Features
@@ -292,7 +306,7 @@
-
+
Separate favorited extensions
@@ -316,6 +330,9 @@
isGitHub={String(extension.isGitHub) === "true"}
unstable={String(extension.unstable) === "true"}
unstableReason={extension.unstableReason}
+
+ bind:favorited={favoritedExtensions[extension.code]}
+ onfavoriteclicked={onFavoriteClicked}
>
{extension.description}
diff --git a/static/icons/favorite-filled.svg b/static/icons/favorite-filled.svg
new file mode 100644
index 000000000..5bba68830
--- /dev/null
+++ b/static/icons/favorite-filled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/icons/favorite-outline.svg b/static/icons/favorite-outline.svg
new file mode 100644
index 000000000..ad97d149f
--- /dev/null
+++ b/static/icons/favorite-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
From c400f7b0c292ce5596d19d9b6d80d469b78f7fbf Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Sun, 1 Feb 2026 23:57:45 -0700
Subject: [PATCH 19/21] nudge to add tags in readme
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 9a169831e..645330084 100644
--- a/README.md
+++ b/README.md
@@ -223,6 +223,7 @@ Each extension is incased in `{}` brackets. Look below on how to copy it.
documentation: "page-name", // This is the page name for the documentation you created.
// These next ones are optional. You can choose not to include them.
+ tags: ["new", "graphics"], // Optional. A list of tags to add to your extension. You can put anything here, but it may be adjusted later.
creatorAlias: "Joe", // Optional. This will not change the creator link, but change the name that links to it.
notes: "Additional help by someguy", // Optional. Allows you to note anyone else who helped you or any small info.
unstable: false, // Optional. Will add a warning message that your extension is unstable.
From 853d3a2f3f3e72960709dfe461f1c45fe01ca957 Mon Sep 17 00:00:00 2001
From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com>
Date: Mon, 2 Feb 2026 00:23:33 -0700
Subject: [PATCH 20/21] add a button to still show "test in new proj"
---
src/lib/Extension/Component.svelte | 21 +++++++++++++++-
src/routes/+page.svelte | 40 +++++++++++++++++++++++++-----
static/icons/test-disabled.svg | 7 ++++++
static/icons/test-enabled.svg | 19 ++++++++++++++
4 files changed, 80 insertions(+), 7 deletions(-)
create mode 100644 static/icons/test-disabled.svg
create mode 100644 static/icons/test-enabled.svg
diff --git a/src/lib/Extension/Component.svelte b/src/lib/Extension/Component.svelte
index 1333659ca..9e0760ed9 100644
--- a/src/lib/Extension/Component.svelte
+++ b/src/lib/Extension/Component.svelte
@@ -244,8 +244,19 @@
{/if}
+