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.

+
+ +
+ + +
+
+
+
+

Filters

+ +

Tags

+ + + + + +
+ -
-
- - - - +

Features

+ Documentation + + + + + Example projects + + + + + Warnings + + +
-
-
- - -
-
- - {#each extensions as extension} - {#if searchable(extension.name).includes(stateSearchBar.query)} - - {extension.description} - - {/if} - {/each} - {#if showNoExtensionsFound} -

No extensions found under that search query.

+
+ + {#each extensions as extension} + {#if searchable(extension.name).includes(stateSearchBar.query)} + + {extension.description} + {/if} -
+ {/each} + {#if showNoExtensionsFound} +

No extensions match your selected filters.

+ {/if}
+
+

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 @@ Filters + />
Sort + />
@@ -137,10 +158,14 @@

Tags

{#each tagsListShown as extensionTag} {#if extensionTag.name === "separator"} -
+
{:else} {/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 Example projects 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} +

Features

Documentation @@ -262,6 +311,25 @@ Hide extensions with warnings + + Favorites + + + +
+
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 @@ + + + + + + \ 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 @@ + + + + + + \ 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 @@
-

Features

- Documentation - - - - - Example projects - - Warnings - - - - - Favorites - -
+ + +
@@ -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} +

{@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}

Features

@@ -292,7 +306,7 @@
@@ -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}
+
-
+ {#if props.showTestAlways && stateApplication.fromEditor} + + {/if} +
{#if stateApplication.fromEditor}
Sort
+ {#if stateApplication.fromEditor} + + {/if}
@@ -333,6 +360,7 @@ bind:favorited={favoritedExtensions[extension.code]} onfavoriteclicked={onFavoriteClicked} + showTestAlways={showingTestInNewProject} > {extension.description} diff --git a/static/icons/test-disabled.svg b/static/icons/test-disabled.svg new file mode 100644 index 000000000..3d7fabcc3 --- /dev/null +++ b/static/icons/test-disabled.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/static/icons/test-enabled.svg b/static/icons/test-enabled.svg new file mode 100644 index 000000000..0f84103cb --- /dev/null +++ b/static/icons/test-enabled.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file From e0c14dbeac9f973d3d349a8c9018610ace7c74d6 Mon Sep 17 00:00:00 2001 From: JeremyGamer13 <69337718+JeremyGamer13@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:15:03 -0700 Subject: [PATCH 21/21] add example proj to readme & fix sort cursor --- README.md | 1 + src/routes/+page.svelte | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 645330084..ad320c370 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ Each extension is incased in `{}` brackets. Look below on how to copy it. // 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. + example: "Username/extension.pmp", // Optional. An example project to show off how to use the extension. 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. diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d32a940ea..073b5fdd5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -442,7 +442,8 @@ border-radius: 8px; background: transparent; - + } + .extension-list-controls button { cursor: pointer; } .extension-list-controls button:active {