fuzzyworld

tools / commit

Merge branch 'v2' into 'main'

bc1aeb4e0c36bbc81542d79fd2c276bee5f09b6e · Pavel Safronov · 2025-07-21 22:47:20 -0700 · parents 3f3d83365713 14b80d45c04c

Merge branch 'v2' into 'main'

V2

See merge request PavelSafronov/tools!1
 separator-page/icons/template-icon-1.png  | Bin 0 -> 23061 bytes
 separator-page/icons/template-icon-10.png | Bin 0 -> 28966 bytes
 separator-page/icons/template-icon-11.png | Bin 0 -> 39781 bytes
 separator-page/icons/template-icon-12.png | Bin 0 -> 30222 bytes
 separator-page/icons/template-icon-13.png | Bin 0 -> 19720 bytes
 separator-page/icons/template-icon-2.png  | Bin 0 -> 14143 bytes
 separator-page/icons/template-icon-3.png  | Bin 0 -> 16440 bytes
 separator-page/icons/template-icon-4.png  | Bin 0 -> 37455 bytes
 separator-page/icons/template-icon-5.png  | Bin 0 -> 24424 bytes
 separator-page/icons/template-icon-6.png  | Bin 0 -> 26763 bytes
 separator-page/icons/template-icon-7.png  | Bin 0 -> 31415 bytes
 separator-page/icons/template-icon-8.png  | Bin 0 -> 21859 bytes
 separator-page/icons/template-icon-9.png  | Bin 0 -> 26307 bytes
 separator-page/index.html                 | 842 ++++++++++++++++++++----------
 14 files changed, 571 insertions(+), 271 deletions(-)

diff --git a/separator-page/icons/template-icon-1.png b/separator-page/icons/template-icon-1.png
new file mode 100644
index 0000000..6f3825b
Binary files /dev/null and b/separator-page/icons/template-icon-1.png differ
diff --git a/separator-page/icons/template-icon-10.png b/separator-page/icons/template-icon-10.png
new file mode 100644
index 0000000..e96db50
Binary files /dev/null and b/separator-page/icons/template-icon-10.png differ
diff --git a/separator-page/icons/template-icon-11.png b/separator-page/icons/template-icon-11.png
new file mode 100644
index 0000000..0029ab6
Binary files /dev/null and b/separator-page/icons/template-icon-11.png differ
diff --git a/separator-page/icons/template-icon-12.png b/separator-page/icons/template-icon-12.png
new file mode 100644
index 0000000..1d644e2
Binary files /dev/null and b/separator-page/icons/template-icon-12.png differ
diff --git a/separator-page/icons/template-icon-13.png b/separator-page/icons/template-icon-13.png
new file mode 100644
index 0000000..b1517ac
Binary files /dev/null and b/separator-page/icons/template-icon-13.png differ
diff --git a/separator-page/icons/template-icon-2.png b/separator-page/icons/template-icon-2.png
new file mode 100644
index 0000000..e78e157
Binary files /dev/null and b/separator-page/icons/template-icon-2.png differ
diff --git a/separator-page/icons/template-icon-3.png b/separator-page/icons/template-icon-3.png
new file mode 100644
index 0000000..ca665b1
Binary files /dev/null and b/separator-page/icons/template-icon-3.png differ
diff --git a/separator-page/icons/template-icon-4.png b/separator-page/icons/template-icon-4.png
new file mode 100644
index 0000000..10eaaf3
Binary files /dev/null and b/separator-page/icons/template-icon-4.png differ
diff --git a/separator-page/icons/template-icon-5.png b/separator-page/icons/template-icon-5.png
new file mode 100644
index 0000000..48753c1
Binary files /dev/null and b/separator-page/icons/template-icon-5.png differ
diff --git a/separator-page/icons/template-icon-6.png b/separator-page/icons/template-icon-6.png
new file mode 100644
index 0000000..95ef782
Binary files /dev/null and b/separator-page/icons/template-icon-6.png differ
diff --git a/separator-page/icons/template-icon-7.png b/separator-page/icons/template-icon-7.png
new file mode 100644
index 0000000..922e89e
Binary files /dev/null and b/separator-page/icons/template-icon-7.png differ
diff --git a/separator-page/icons/template-icon-8.png b/separator-page/icons/template-icon-8.png
new file mode 100644
index 0000000..eedb323
Binary files /dev/null and b/separator-page/icons/template-icon-8.png differ
diff --git a/separator-page/icons/template-icon-9.png b/separator-page/icons/template-icon-9.png
new file mode 100644
index 0000000..1a81b6e
Binary files /dev/null and b/separator-page/icons/template-icon-9.png differ
diff --git a/separator-page/index.html b/separator-page/index.html
index c72df94..2dc61d3 100644
--- a/separator-page/index.html
+++ b/separator-page/index.html
@@ -1,320 +1,620 @@
 <!DOCTYPE html>
 <html>
 
-    <head>
-        <meta charset="utf-8" />
-        <title>Separator</title>
-        <link rel="icon"
-              href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AAD//wAA/n8AAP5/AAD+fwAA/n8AAOAHAADgBwAA/n8AAP5/AAD+fwAA/n8AAP//AAD//wAA" />
-        <script language="javascript"
-                src="https://pavelsafronov.gitlab.io/tools/separator-page/lz-string.js"></script>
-
-        <!-- https://github.com/showdownjs/showdown -->
-        <script language="javascript"
-                src="https://rawgit.com/showdownjs/showdown/develop/dist/showdown.min.js"></script>
-        <script>
-            /** @typedef {Object} State
-             * @property {string} name
-             * @property {string} description
-             */
-
-            /** @type {State} */
-            const defaultState = {
-                name: "Separator",
-                description: `# Info
+<head>
+    <meta charset="utf-8" />
+    <title>Separator</title>
+    <link rel="icon" id="favicon"
+        href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AAD//wAA/n8AAP5/AAD+fwAA/n8AAOAHAADgBwAA/n8AAP5/AAD+fwAA/n8AAP//AAD//wAA" />
+    <script src="https://cdn.jsdelivr.net/npm/showdown/dist/showdown.min.js"></script>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 0;
+            padding: 0;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 100vh;
+            background-color: #f9f9f9;
+        }
+
+        .container {
+            width: 90%;
+            max-width: 600px;
+            background: #fff;
+            border: 1px solid #ddd;
+            border-radius: 8px;
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+            padding: 16px;
+        }
+
+        .header {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            margin-bottom: 8px;
+        }
+
+        .icon-preview {
+            width: 32px;
+            height: 32px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            overflow: hidden;
+            background-size: cover;
+        }
+
+        .title {
+            font-size: 1.5em;
+            font-weight: bold;
+            width: 100%;
+            outline: none;
+            border: none;
+            background: none;
+        }
+
+        .title[contenteditable="true"] {
+            border: 1px dashed #aaa;
+            padding: 4px;
+            border-radius: 4px;
+        }
+
+        .description-container {
+            position: relative;
+        }
+
+        .description {
+            font-size: 1em;
+            line-height: 1.5;
+            color: #555;
+            outline: none;
+            width: 100%;
+            display: none;
+            height: max-content;
+        }
+
+        .description-rendered {
+            font-size: 1em;
+            line-height: 1.5;
+            color: #555;
+            width: 100%;
+        }
+
+        .description[contenteditable="true"] {
+            border: 1px dashed #aaa;
+            padding: 8px;
+            border-radius: 4px;
+            display: block;
+        }
+
+        .toolbar {
+            margin-top: 16px;
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+        }
+
+        .info {
+            font-size: 0.9em;
+            color: #666;
+            text-align: center;
+            margin-top: 12px;
+        }
+
+        .drop-area {
+            border: 2px dashed #aaa;
+            padding: 8px;
+            text-align: center;
+            color: #666;
+            font-size: 0.9em;
+            border-radius: 4px;
+        }
+
+        .drop-area.dragover {
+            background-color: #eef;
+            border-color: #666;
+        }
+
+        .custom-icon-input {
+            display: flex;
+            gap: 8px;
+            align-items: center;
+        }
+
+        .custom-icon-input input {
+            flex-grow: 1;
+        }
+
+        .edit-button {
+            position: absolute;
+            top: 0;
+            right: 0;
+            background: #ddd;
+            border: none;
+            padding: 4px;
+            cursor: pointer;
+            border-radius: 4px;
+        }
+
+        .edit-button:hover {
+            background: #ccc;
+        }
+
+        .previous-icons {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-top: 8px;
+        }
+
+        .previous-icon {
+            width: 32px;
+            height: 32px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            cursor: pointer;
+            background-size: cover;
+            background-position: center;
+            transition: transform 0.2s;
+        }
+
+        .previous-icon:hover {
+            transform: scale(1.1);
+            border-color: #999;
+        }
+
+        .previous-icons-container {
+            margin: 16px 0;
+        }
+
+        .previous-icons-container h3 {
+            margin: 0 0 8px 0;
+            font-size: 1.1em;
+            color: #333;
+        }
+    </style>
+</head>
+
+<body>
+
+    <div class="container">
+        <div class="header">
+            <div id="icon-preview" class="icon-preview" title="Icon Preview"></div>
+            <div id="title" class="title" contenteditable="true">Separator</div>
+        </div>
 
-This is a separator page!
+        <hr />
+
+        <div class="description-container">
+            <textarea id="description" class="description" placeholder="Write your Markdown here..."># Info
 
+This is a separator page!
 ## TODO
 
 1. Read this page
 1. Understand this page
-1. The page name and page content are both editable
-1. To enter "edit mode", click on the "Edit" button OR **double-click on title or description element**
-1. Double-click on either element again to return to "view mode"
+1. The page name, icon, and content are editable
+1. To enter "edit mode", click on the "Edit" button
+1. Click "Done" button to return to "view mode"
 1. Replace the contents on this page to whatever you want a new separator page to contain
 1. Markdown contents will be rendered as HTML
 1. Content links will always open in a new tab
-1. Click "Save as Default" button to save current page as default template
-1. If you messed up this page, click on "Hard Reset" to go back to this default message
-1. The name and the description are stored as zipped content in the url, so copy-pasting the url in a new tab will exactly reproduce this page
-1. Your data is stored in the hash fragment of the url, and [this data is not sent to the server](https://stackoverflow.com/questions/14462218/is-the-url-fragment-identifier-sent-to-the-server), so your notes are never sent anywhere
-1. But your notes **do** end up in the browser's history, so don't keep anything very important here
-1. The default page state (default title and contents) is kept in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), so this information also never leaves your computer
-1. Local storage is accessible by any other script that's executing on the separator page (or on the same domain), so technically *some* scripts can get your local "default separator page" state`
-            };
-            const maxDataLength = 64 * 1024;
-            let currentState = defaultState;
-
-            /** @type {HTMLElement} */
-            let nameSpan;
-            /** @type {HTMLInputElement} */
-            let nameInput;
-            /** @type {HTMLElement} */
-            let descriptionSpan;
-            /** @type {HTMLElement} */
-            let descriptionShowSpan;
-
-            const lsKey = "pavel-safronov-separator-page";
-            const storedDefaultJson = localStorage.getItem(lsKey);
-            if (isNullOrEmpty(storedDefaultJson)) {
-                localStorage.setItem(lsKey, JSON.stringify(currentState));
-            } else {
-                /** @type {State} */
-                let newState;
-                try {
-                    newState = JSON.parse(storedDefaultJson);
-                } catch {
-                    console.error({ message: "Could not parse stored default", storedDefaultJson });
-                }
-                if (newState) {
-                    currentState = newState;
-                }
-            }
+1. All the information about a tab is stored under a specific id in local storage. Copy-pasting the url in a new tab will work, as long as you're in the same browser.
+1. Your data is stored in local storage, under the tab id.
+1. Local storage is accessible by any other script that's executing on the separator page (or on the same domain), so
+technically *some* scripts can get your local "default separator page" state
+            </textarea>
+            <div id="description-rendered" class="description-rendered"></div>
+            <button id="edit-description" class="edit-button">Edit</button>
+        </div>
 
-            // MD converter
-            var shodownConverter = new showdown.Converter();
+        <hr />
+
+        <div class="previous-icons-container">
+            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
+                <h3 style="margin: 0;">Previous Icons (shift-click to remove)</h3>
+                <div style="display: flex; gap: 8px;">
+                    <button id="reset-icons"
+                        style="padding: 4px 8px; font-size: 0.8em; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">Reset</button>
+                    <button id="export-icons"
+                        style="padding: 4px 8px; font-size: 0.8em; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">Export
+                        PNG</button>
+                </div>
+            </div>
+            <div class="previous-icons" id="previous-icons">
+                <!-- Previous icons will be dynamically added here -->
+            </div>
+        </div>
 
-            /**
-             * @param {string} text
-             */
-            function isNullOrEmpty(text) {
-                return (!text || text.length === 0);
-            }
+        <div class="toolbar">
+            <div class="drop-area" id="drop-area">Drop a PNG here or paste an image URL</div>
+            <div class="custom-icon-input">
+                <input id="icon-url-input" type="text" placeholder="Paste an image URL here" />
+                <button id="apply-icon-button">Apply</button>
+            </div>
+        </div>
 
-            /**
-             * @param {State} partialState
-             */
-            function update(partialState) {
-                const path = window.location.pathname;
+        <div class="info">
+            Need icons? Check out <a href="https://flaticon.com" target="_blank">Flaticon</a>.
+            <br />
+            The default icons came from <a href="https://www.flaticon.com/authors/icongeek26"
+                target="_blank">icongeek26</a>
+        </div>
+    </div>
 
-                if (partialState) {
-                    const newState = {
-                        ...currentState,
-                        ...partialState
-                    };
-                    currentState = newState;
-                }
-                const stateJson = JSON.stringify(currentState);
-                const stateCompressed = LZString.compressToBase64(stateJson);
-                const newPath = `${path}#${stateCompressed}`;
-                if (newPath.length >= maxDataLength) {
-                    alert(`Url is too long (${newPath.length} characters), need to rethink storing data in the hash. Or not using the Separator Page as a OneNote replacement.`);
-                }
+    <script>
+        const converter = new showdown.Converter();
 
-                try {
-                    history.replaceState(currentState, "", newPath);
-                } catch (ex) {
-                    alert(`Not able to save contents: ${ex}`);
-                }
+        /** @type {string} */
+        const tabId = new URLSearchParams(window.location.search).get("tab") || Math.floor(Date.now() / 1000).toString();
 
-                let { name, description } = currentState;
-                // set name
-                document.title = isNullOrEmpty(name) ? "---" : name;
-                if (nameSpan.innerHTML !== name) {
-                    nameSpan.innerHTML = name;
-                }
-                if (nameInput.value !== name) {
-                    nameInput.value = name;
-                }
+        /** @type {string} */
+        const localStorageKey = `separator-tab-${tabId}`;
 
-                // set description
-                const descriptionToUse = description !== null && description.length > 0 ? description : "**No content specified, double-click to edit**";
-                descriptionShowSpan.innerHTML = shodownConverter.makeHtml(descriptionToUse);
-                descriptionSpan.value = isNullOrEmpty(description) ? "" : description;
+        /** @type {HTMLElement} */
+        const iconPreview = document.getElementById("icon-preview");
 
-                // update all href links to open in new tabs
-                const links = descriptionShowSpan.getElementsByTagName("a");
-                for (let i = 0; i < links.length; i++) {
-                    const link = links.item(i);
-                    link.target = "_blank";
-                }
-            }
+        /** @type {HTMLElement} */
+        const titleElement = document.getElementById("title");
 
-            /**
-             * @param {HTMLElement} a
-             * @param {HTMLElement} b
-            */
-            function flipHideShow(a, b) {
-                const isAHidden = a.className === "hidden";
-                a.className = isAHidden ? "" : "hidden";
-                b.className = isAHidden ? "hidden" : "";
-            }
+        /** @type {HTMLTextAreaElement} */
+        const descriptionElement = document.getElementById("description");
 
-            window.onpopstate = (state) => {
-                update(state);
-            };
-            window.onload = () => {
-                /** @type {State} */
-                let state = history.state;
-
-                const search = window.location.search;
-                if (search) {
-                    const urlParams = new URLSearchParams(search);
-                    const name = urlParams.get("name");
-                    if (name) {
-                        state = { name };
-                    }
-                }
+        /** @type {HTMLElement} */
+        const descriptionRendered = document.getElementById("description-rendered");
 
-                const hash = window.location.hash;
-                if (hash) {
-                    const compressed = hash.substring(1);
-                    var json = LZString.decompressFromBase64(compressed);
-                    state = JSON.parse(json);
-                }
+        /** @type {HTMLButtonElement} */
+        const editDescriptionButton = document.getElementById("edit-description");
 
-                // name input
-                nameInput = document.getElementById("name-input");
-                nameInput.onkeyup = () => update({ name: nameInput.value });
-
-                // name field
-                nameSpan = document.getElementById("name");
-
-                // description field
-                descriptionSpan = document.getElementById("description");
-                descriptionSpan.onkeyup = () => update({ description: descriptionSpan.value });
-
-                // description html field
-                descriptionShowSpan = document.getElementById("description-show");
-
-                const saveButton = document.getElementById("save");
-                const resetButton = document.getElementById("reset");
-                const editButton = document.getElementById("edit");
-                const doneButton = document.getElementById("done");
-
-                // hook up double-click switching
-                const flipEditState = () => {
-                    flipHideShow(descriptionShowSpan, descriptionSpan);
-                    flipHideShow(nameInput, nameSpan);
-                    flipHideShow(editButton, doneButton);
-                };
-                descriptionShowSpan.addEventListener("dblclick", flipEditState);
-                descriptionSpan.addEventListener("dblclick", flipEditState);
-                nameInput.addEventListener("dblclick", () => flipEditState);
-                nameSpan.addEventListener("dblclick", () => flipEditState);
-
-                saveButton.onclick = () => {
-                    if (confirm("Save current title and description as default starting values for separator page?")) {
-                        localStorage.setItem(lsKey, JSON.stringify(currentState));
-                    }
-                };
-                resetButton.onclick = () => {
-                    if (confirm("Set default title and description values to original values?")) {
-                        localStorage.setItem(lsKey, JSON.stringify(defaultState));
-                        update(defaultState);
-                    }
-                };
-                editButton.onclick = () => {
-                    flipEditState();
-                }
-                done.onclick = () => {
-                    flipEditState();
-                }
+        /** @type {HTMLInputElement} */
+        const iconUrlInput = document.getElementById("icon-url-input");
 
-                // render everything for the first time
-                update(state);
-            };
+        /** @type {HTMLButtonElement} */
+        const applyIconButton = document.getElementById("apply-icon-button");
 
-        </script>
-        <style>
-            #description {
-                color: gray;
-                font-size: small;
-                width: 100%;
-                height: 100%;
-                box-sizing: border-box;
-            }
+        /** @type {HTMLDivElement} */
+        const dropArea = document.getElementById("drop-area");
 
-            #name {
-                width: 100%;
-                font-size: xx-large;
-            }
+        /** @type {HTMLLinkElement} */
+        const faviconElement = document.getElementById("favicon");
 
-            .container {
-                height: 100%;
-            }
+        /** @type {HTMLDivElement} */
+        const previousIconsContainer = document.getElementById("previous-icons");
 
-            .info {
-                color: gray;
-                font-size: small;
+        /** @type {HTMLButtonElement} */
+        const exportIconsButton = document.getElementById("export-icons");
+
+        /** @type {HTMLButtonElement} */
+        const resetIconsButton = document.getElementById("reset-icons");
+
+        // Global storage key for previous icons (shared across all tabs)
+        const previousIconsKey = "separator-previous-icons";
+        const maxPreviousIcons = 100; // Limit to 100 recent icons
+
+        // Default icons from the icons folder
+        const defaultIcons = [
+            "./icons/template-icon-1.png",
+            "./icons/template-icon-2.png",
+            "./icons/template-icon-3.png",
+            "./icons/template-icon-4.png",
+            "./icons/template-icon-5.png",
+            "./icons/template-icon-6.png",
+            "./icons/template-icon-7.png",
+            "./icons/template-icon-8.png",
+            "./icons/template-icon-9.png",
+            "./icons/template-icon-10.png",
+            "./icons/template-icon-11.png",
+            "./icons/template-icon-12.png",
+            "./icons/template-icon-13.png"
+        ];
+
+        // Load saved data
+        const savedData = JSON.parse(localStorage.getItem(localStorageKey) || "null");
+
+        if (!new URLSearchParams(window.location.search).has("tab")) {
+            const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`;
+            history.replaceState(null, "", newUrl);
+        }
+
+        if (savedData) {
+            renderAll(savedData);
+            console.debug('Loaded saved data:', savedData);
+        } else {
+            saveTabData();
+        }
+
+        // Initialize previous icons display
+        initializeDefaultIcons();
+        renderPreviousIcons();
+
+        function initializeDefaultIcons() {
+            const existingIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
+
+            // If no icons exist, populate with defaults
+            if (existingIcons.length === 0) {
+                localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons));
+                console.log('Initialized with default icons');
             }
+        }
+
+        function resetToDefaultIcons() {
+            localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons));
+            renderPreviousIcons();
+
+            // Show feedback
+            const originalText = resetIconsButton.textContent;
+            resetIconsButton.textContent = "Reset!";
+            resetIconsButton.style.background = "#4CAF50";
+            setTimeout(() => {
+                resetIconsButton.textContent = originalText;
+                resetIconsButton.style.background = "#ddd";
+            }, 2000);
+
+            console.log('Reset to default icons');
+        }
+
+        function updateFavicon(iconUrl) {
+            console.debug('Updating favicon with URL:', iconUrl);
+            faviconElement.href = iconUrl;
+        }
+
+        function savePreviousIcon(iconUrl) {
+            // Skip default favicon and data URLs for data:image/x-icon
+            if (iconUrl.startsWith('data:image/x-icon')) {
+                return;
+            }
+
+            let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
+
+            // Remove the icon if it already exists to avoid duplicates
+            previousIcons = previousIcons.filter(icon => icon !== iconUrl);
 
-            .left {
-                float: left;
+            // Add the new icon to the beginning
+            previousIcons.unshift(iconUrl);
+
+            // Limit to maxPreviousIcons
+            if (previousIcons.length > maxPreviousIcons) {
+                previousIcons = previousIcons.slice(0, maxPreviousIcons);
             }
 
-            .right {
-                float: right;
+            localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons));
+            renderPreviousIcons();
+        }
+
+        function removePreviousIcon(iconUrl) {
+            let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
+            previousIcons = previousIcons.filter(icon => icon !== iconUrl);
+            localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons));
+            renderPreviousIcons();
+        }
+
+        function renderPreviousIcons() {
+            const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
+            previousIconsContainer.innerHTML = "";
+
+            previousIcons.forEach(iconUrl => {
+                const iconElement = document.createElement("div");
+                iconElement.className = "previous-icon";
+                iconElement.style.backgroundImage = `url('${iconUrl}')`;
+                iconElement.title = `Click to use • Shift+Click to remove`;
+                iconElement.addEventListener("click", (event) => {
+                    if (event.shiftKey) {
+                        removePreviousIcon(iconUrl);
+                    } else {
+                        handleIconChange(iconUrl);
+                    }
+                });
+                previousIconsContainer.appendChild(iconElement);
+            });
+        }
+
+        async function downloadImageAsFile(url, filename) {
+            try {
+                // If it's a data URL, we can download it directly
+                if (url.startsWith('data:')) {
+                    const link = document.createElement('a');
+                    link.href = url;
+                    link.download = filename;
+                    document.body.appendChild(link);
+                    link.click();
+                    document.body.removeChild(link);
+                    return true;
+                }
+
+                // For external URLs, we need to fetch and convert
+                const response = await fetch(url);
+                const blob = await response.blob();
+
+                // Convert to PNG if it's not already
+                const canvas = document.createElement('canvas');
+                const ctx = canvas.getContext('2d');
+                const img = new Image();
+
+                return new Promise((resolve) => {
+                    img.onload = () => {
+                        canvas.width = img.width;
+                        canvas.height = img.height;
+                        ctx.drawImage(img, 0, 0);
+
+                        canvas.toBlob((pngBlob) => {
+                            const link = document.createElement('a');
+                            link.href = URL.createObjectURL(pngBlob);
+                            link.download = filename;
+                            document.body.appendChild(link);
+                            link.click();
+                            document.body.removeChild(link);
+                            URL.revokeObjectURL(link.href);
+                            resolve(true);
+                        }, 'image/png');
+                    };
+
+                    img.onerror = () => {
+                        console.warn(`Failed to load image: ${url}`);
+                        resolve(false);
+                    };
+
+                    img.crossOrigin = 'anonymous';
+                    img.src = URL.createObjectURL(blob);
+                });
+            } catch (error) {
+                console.warn(`Failed to download ${url}:`, error);
+                return false;
             }
+        }
+
+        async function exportIcons() {
+            const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
 
-            .hidden {
-                display: none;
+            if (previousIcons.length === 0) {
+                alert('No icons to export!');
+                return;
             }
 
-        </style>
-    </head>
+            // Update button to show progress
+            const originalText = exportIconsButton.textContent;
+            exportIconsButton.textContent = "Exporting...";
+            exportIconsButton.style.background = "#ffa500";
+            exportIconsButton.disabled = true;
 
-    <body>
+            let successCount = 0;
 
-        <div class="container">
+            for (let i = 0; i < previousIcons.length; i++) {
+                const iconUrl = previousIcons[i];
+                const filename = `template-icon-${i + 1}.png`;
 
-            <div class="container">
+                const success = await downloadImageAsFile(iconUrl, filename);
+                if (success) {
+                    successCount++;
+                }
 
-                <input autofocus
-                       id="name-input"
-                       title="Name"
-                       class="hidden"
-                       size="100" />
+                // Small delay between downloads to prevent overwhelming the browser
+                await new Promise(resolve => setTimeout(resolve, 100));
+            }
 
-                <span id="name"
-                      title="Tab title - double-click to edit">
-                    Separator
-                </span>
+            // Show completion feedback
+            exportIconsButton.textContent = `Exported ${successCount}/${previousIcons.length}`;
+            exportIconsButton.style.background = successCount === previousIcons.length ? "#4CAF50" : "#ff9800";
+
+            setTimeout(() => {
+                exportIconsButton.textContent = originalText;
+                exportIconsButton.style.background = "#ddd";
+                exportIconsButton.disabled = false;
+            }, 3000);
+        }
+
+        function saveTabData() {
+            const data = {
+                id: tabId,
+                icon: faviconElement.href,
+                title: titleElement.innerText,
+                description: descriptionElement.value,
+            };
+            console.debug('Saving tab data to localStorage:', data);
+            renderAll(data);
+            localStorage.setItem(localStorageKey, JSON.stringify(data));
+        }
+
+        function renderAll(data) {
+            iconPreview.style.backgroundImage = `url('${data.icon}')`;
+            if (titleElement.innerText !== data.title) {
+                titleElement.innerText = data.title;
+            }
+            document.title = titleElement.innerText || "Separator";
+            renderDescription(data.description);
+            updateFavicon(data.icon);
+        }
+
+        function renderDescription(description) {
+            descriptionElement.value = description;
+            descriptionElement.style.height = "";
+            descriptionElement.style.height = `${descriptionElement.scrollHeight}px`;
+            const html = converter.makeHtml(description);
+            descriptionRendered.innerHTML = html;
+        }
+
+        function handleIconChange(iconUrl) {
+            iconPreview.style.backgroundImage = `url('${iconUrl}')`;
+            updateFavicon(iconUrl);
+            savePreviousIcon(iconUrl);
+            saveTabData();
+        }
+
+        // Toggle edit mode for description
+        editDescriptionButton.addEventListener("click", () => {
+            const isEditing = descriptionElement.style.display === "block";
+
+            console.debug('Toggling edit mode:', isEditing ? 'Done' : 'Edit');
+            descriptionElement.style.display = isEditing ? "none" : "block";
+            descriptionRendered.style.display = isEditing ? "block" : "none";
+            editDescriptionButton.innerText = isEditing ? "Edit" : "Done";
+
+            // renderDescription();
+            saveTabData();
+        });
+
+        // Drop area handling
+        dropArea.addEventListener("dragover", (event) => {
+            event.preventDefault();
+            dropArea.classList.add("dragover");
+        });
+
+        dropArea.addEventListener("dragleave", () => {
+            dropArea.classList.remove("dragover");
+        });
+
+        dropArea.addEventListener("drop", (event) => {
+            event.preventDefault();
+            dropArea.classList.remove("dragover");
+            const file = event.dataTransfer.files[0];
+            if (file && file.type.startsWith("image/")) {
+                const reader = new FileReader();
+                reader.onload = () => handleIconChange(reader.result);
+                reader.readAsDataURL(file);
+            }
+        });
+
+        // Clipboard paste handling
+        document.addEventListener("paste", (event) => {
+            const clipboardItems = event.clipboardData.items;
+            for (const item of clipboardItems) {
+                if (item.type.startsWith("image/")) {
+                    const blob = item.getAsFile();
+                    const reader = new FileReader();
+                    reader.onload = () => handleIconChange(reader.result);
+                    reader.readAsDataURL(blob);
+                    break;
+                }
+            }
+        });
 
-                <span class="info">
-                    Tab title
-                </span>
-            </div>
+        // Apply icon from URL
+        applyIconButton.addEventListener("click", () => {
+            const iconUrl = iconUrlInput.value.trim();
+            if (iconUrl) {
+                handleIconChange(iconUrl);
+            }
+        });
 
-            <div class="container">
+        // Export icons as PNG files
+        exportIconsButton.addEventListener("click", exportIcons);
 
-                <textarea rows="30"
-                          id="description"
-                          title="content"
-                          class="hidden"></textarea>
+        // Reset icons to defaults
+        resetIconsButton.addEventListener("click", resetToDefaultIcons);
 
-                <span id="description-show"
-                      title="Description - double-click to edit"></span>
-            </div>
-        </div>
+        console.debug("here!");
+        // Title and description event listeners
+        titleElement.addEventListener("input", () => {
+            console.debug('Title updated to:', titleElement.innerText);
+            saveTabData();
+        });
 
-        <div class="container">
-
-            <div class="left">
-                <button type="button"
-                        id="edit">
-                    Edit
-                </button>
-                <button type="button"
-                        id="done"
-                        class="hidden">
-                    Done
-                </button>
-                <button type="button"
-                        id="save">
-                    Save as Default
-                </button>
-            </div>
+        descriptionElement.addEventListener("input", () => {
+            // console.debug('Description updated to:', descriptionElement.value);
+        });
+    </script>
 
-            <div class="right">
-                <button type="button"
-                        id="reset">
-                    Hard Reset
-                </button>
-            </div>
-        </div>
-        </div>
-    </body>
+</body>
 
-</html>
+</html>
\ No newline at end of file