commit 012945313019498357225fdd85f9cb745d270bb9 parent 0ebae0d8a72bc088199f43508b6f6be9b4e86da1 Author: Hunter Date: Wed, 18 Mar 2026 17:42:12 -0400 restructure Diffstat:
150 files changed, 1247 insertions(+), 885 deletions(-)
diff --git a/.github/workflows/generate-manifest.yml b/.github/workflows/generate-manifest.yml @@ -3,7 +3,7 @@ name: Generate Resource Manifest on: push: branches: [ main ] - paths: [ 'images/**', 'resources/**' ] # Run when images or resources are added/changed + paths: [ 'resources/**' ] # Run when resources (including images) are added/changed workflow_dispatch: # Allow manual trigger jobs: @@ -24,7 +24,7 @@ jobs: python-version: '3.x' - name: Generate resource manifest - run: python generate_resource_manifest.py + run: python resources/scripts/generate_resource_manifest.py - name: Check for changes id: verify-changed-files @@ -40,6 +40,6 @@ jobs: run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add resource-manifest.json + git add resources/resource-manifest.json git commit -m "Auto-update resource manifest [skip ci]" git push \ No newline at end of file diff --git a/.gitignore b/.gitignore @@ -2,5 +2,5 @@ jen-only/ -vendor/node_modules/ -vendor/package-lock.json +resources/vendor/node_modules/ +resources/vendor/package-lock.json diff --git a/README.md b/README.md @@ -2,14 +2,14 @@ Craft handmade websites in their natural habitat (your web browser). -<img src="images/pika_construction.gif"> +<img src="resources/images/pika_construction.gif"> Web Workshop was born as a teaching tool for web development classes, with the goal of making web publishing more accessible, especially to those who have never written code before. By stripping away complexity and focusing on the basics, Web Workshop turns website building into something playful and toylike; a creative playground where you can experiment, break things, and watch your ideas materialize in real-time. <a href="https://hunterirving.github.io/web_workshop/"> -<img src="readme_images/picnic.png" width="650"></a> +<img src="resources/readme_images/picnic.png" width="650"></a> <br><br> Recently, Web Workshop has become an editor of choice for <a href="https://html.energy/html-day/2025/index.html">HTML Day</a>, an annual celebration of handmade websites and the creative web. @@ -32,7 +32,7 @@ Recently, Web Workshop has become an editor of choice for <a href="https://html. A library of stock images is included for your convenience. To browse them, type `<img src="?">` anywhere in the editor pane, then click to select an image from the resulting table. <a href="https://hunterirving.github.io/web_workshop/"> -<img src="readme_images/stock_images.png" width="650"></a> +<img src="resources/readme_images/stock_images.png" width="650"></a> <br><br> Alternatively, if you know the name of the image you'd like to use, you can add it to your project like so: `<img src="coffee.gif">`. @@ -58,7 +58,7 @@ Type `<!>` to insert the following starter HTML: - to toggle <b>line numbers</b>, press <kbd>F1</kbd> (disabled by default) - to toggle <b>line wrapping</b>, press <kbd>F2</kbd> (enabled by default) -<img src="images/hint.gif" width=60px> +<img src="resources/images/hint.gif" width=60px> <i> Wanna make websites on your phone? Try installing Web Workshop as a <a href="https://hunterirving.github.io/web_workshop/pages/pwa">Progressive Web App</a>!</i> @@ -67,17 +67,17 @@ Wanna make websites on your phone? Try installing Web Workshop as a <a href="htt - [CodeMirror 6](https://codemirror.net/) - [GitHub Dark Theme for CodeMirror](https://github.com/fsegurai/codemirror-themes) -<img src="images/bright_idea.gif"> +<img src="resources/images/bright_idea.gif"> ## License GPLv3 (see <a href="LICENSE">LICENSE</a> for details). <br> -<img src="images/bigN.gif" width=110px> +<img src="resources/images/bigN.gif" width=110px> ## Contributing Feel free to open issues or submit pull requests for improvements! -<img src="images/gaia.gif" width=180px> +<img src="resources/images/gaia.gif" width=180px> diff --git a/generate_resource_manifest.py b/generate_resource_manifest.py @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate resource manifest for PWA caching -Scans the images/ and resources/ directories and creates a JSON manifest file -""" - -import os -import json -from pathlib import Path - -def generate_resource_manifest(): - """Generate a JSON manifest of all resources in the images/ and resources/ directories""" - - # Define supported image extensions - image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'} - - # Get the directory where this script is located - script_dir = Path(__file__).parent - images_dir = script_dir / 'images' - resources_dir = script_dir / 'resources' - - # Collect all image files - image_files = [] - if images_dir.exists(): - for file_path in images_dir.iterdir(): - if file_path.is_file() and file_path.suffix.lower() in image_extensions: - # Create relative path from web root (GitHub Pages subdirectory) - relative_path = f"/web_workshop/images/{file_path.name}" - image_files.append(relative_path) - - # Collect ALL resource files (no extension filtering) - resource_files = [] - if resources_dir.exists(): - for file_path in resources_dir.rglob('*'): - if file_path.is_file(): - # Create relative path from web root (GitHub Pages subdirectory) - relative_path = f"/web_workshop/resources/{file_path.relative_to(resources_dir).as_posix()}" - resource_files.append(relative_path) - - # Sort for consistent output - image_files.sort() - resource_files.sort() - - # Create manifest object - manifest = { - "images": image_files, - "resources": resource_files, - "generated_at": "auto-generated by GitHub Actions" - } - - # Write manifest file - manifest_path = script_dir / 'resource-manifest.json' - with open(manifest_path, 'w', encoding='utf-8') as f: - json.dump(manifest, f, indent=2, sort_keys=True) - - print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:") - for img in image_files: - print(f" - {img}") - for res in resource_files: - print(f" - {res}") - - return manifest - -if __name__ == "__main__": - manifest = generate_resource_manifest() - print(f"\nManifest saved to: resource-manifest.json") -\ No newline at end of file diff --git a/index.html b/index.html @@ -4,11 +4,11 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <title>Web Workshop</title> - <link rel="icon" href="resources/construction.png"> + <link rel="icon" href="resources/icons/construction.png"> <link rel="manifest" href="manifest.json"> - <link rel="stylesheet" href="styles.css"> + <link rel="stylesheet" href="resources/css/styles.css"> <script type="module"> - import {EditorView, keymap, placeholder, lineNumbers, Decoration, EditorState, Compartment, defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, html, githubDark, indentUnit, search, searchKeymap, closeSearchPanel, openSearchPanel} from "./codemirror-bundle.js" + import {EditorView, keymap, placeholder, lineNumbers, Decoration, EditorState, Compartment, defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, html, githubDark, indentUnit, search, searchKeymap, closeSearchPanel, openSearchPanel} from "./resources/js/codemirror-bundle.js" window.CodeMirror = {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration}; </script> </head> @@ -21,6 +21,6 @@ <iframe id="preview"></iframe> </div> - <script type="module" src="main.js"></script> + <script type="module" src="resources/js/main.js"></script> </body> </html> diff --git a/main.js b/main.js @@ -1,609 +0,0 @@ -let updateTimer; -let editorView; -const preview = document.getElementById('preview'); - -// Stock images loaded from manifest - can be referenced by bare filename -let stockImages = new Map(); // lowercase -> original filename - -// Load stock image list from manifest -const stockImagesReady = fetch('resource-manifest.json') - .then(r => r.json()) - .then(manifest => { - manifest.images.forEach(path => { - const filename = path.split('/').pop(); - stockImages.set(filename.toLowerCase(), filename); - }); - }) - .catch(() => console.log('Could not load resource manifest')); - -// Rewrite bare image filenames to use images/ prefix (case-insensitive, uses correct case) -function rewriteBareImageSrcs(html) { - // Rewrite <img src="filename.ext"> - html = html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi, - (match, before, filename, ext, after) => { - const original = stockImages.get(filename.toLowerCase()); - if (original) { - return before + 'images/' + original + after; - } - return match; - }); - // Rewrite url(filename.ext) in CSS - html = html.replace(/(url\(\s*["']?)([^"')/:]+\.(gif|png|jpg|jpeg|svg|webp))(["']?\s*\))/gi, - (match, before, filename, ext, after) => { - const original = stockImages.get(filename.toLowerCase()); - if (original) { - return before + 'images/' + original + after; - } - return match; - }); - return html; -} - -// Expand <img src="?"> tags into gallery table -function expandImagesTag(html) { - return html.replace(/<img\s+src\s*=\s*["']?\?["']?\s*\/?>/gi, () => { - const images = Array.from(stockImages.values()).sort(); - if (images.length === 0) return '<p>No images available</p>'; - const rows = images.map(filename => - `<tr class="stock-image-row" data-filename="${filename}" style="cursor:pointer;user-select:none;"><td>${filename}</td><td style="text-align:center;"><img src="images/${filename}" style="max-width:100%;height:auto;pointer-events:none;"></td></tr>` - ).join(''); - return `<table class="stock-image-table" border="1" cellpadding="8" cellspacing="0" style="max-width:100%;box-sizing:border-box;table-layout:fixed;"><colgroup><col style="width:50%"><col style="width:50%"></colgroup>${rows}</table>`; - }); -} -const editorPane = document.querySelector('.editor-pane'); -const previewPane = document.querySelector('.preview-pane'); -const storageKey = 'html-lab-content'; -let isFullscreen = false; -let showLineNumbers = false; -let enableLineWrapping = false; -let lineNumbersCompartment; -let lineWrappingCompartment; - -function toggleFullscreen() { - isFullscreen = !isFullscreen; - - if (isFullscreen) { - previewPane.classList.add('fullscreen'); - editorPane.classList.add('hidden'); - // Set iframe height to actual visible height (fixes iOS Safari toolbar issue) - preview.style.height = window.innerHeight + 'px'; - } else { - previewPane.classList.remove('fullscreen'); - editorPane.classList.remove('hidden'); - preview.style.height = ''; - } - - // Update button title in iframe - try { - const toggleButton = preview.contentDocument.getElementById('fullscreenToggle'); - if (toggleButton) { - toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen'; - } - } catch (e) { - // Ignore cross-origin errors - } -} - - -// Listen for messages from iframe -window.addEventListener('message', function(event) { - if (event.data === 'toggleFullscreen') { - toggleFullscreen(); - } -}); - -function extractTitleAndFavicon(htmlCode) { - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlCode, 'text/html'); - - // Extract title - const titleElement = doc.querySelector('title'); - const title = titleElement ? titleElement.textContent.trim() : null; - - // Extract favicon - const faviconSelectors = [ - 'link[rel="icon"]', - 'link[rel="shortcut icon"]', - 'link[rel="apple-touch-icon"]', - 'link[rel="mask-icon"]' - ]; - - let favicon = null; - for (const selector of faviconSelectors) { - const faviconElement = doc.querySelector(selector); - if (faviconElement && faviconElement.href) { - favicon = faviconElement.href; - break; - } - } - - return { title, favicon }; -} - -function updateMainPageTitleAndFavicon(title, favicon) { - // Update title - if (title) { - document.title = title; - } else { - document.title = 'Web Workshop'; - } - - // Update favicon - let faviconLink = document.querySelector('link[rel="icon"]'); - if (!faviconLink) { - faviconLink = document.createElement('link'); - faviconLink.rel = 'icon'; - document.head.appendChild(faviconLink); - } - - if (favicon) { - faviconLink.href = favicon; - } else { - faviconLink.href = 'resources/construction.png'; - } -} - -function updatePreview() { - // Skip preview updates while mobile keyboard is open and editor is focused - // This prevents keyboard layer resets on Android - if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) { - return; - } - - const code = editorView.state.doc.toString(); - - // Extract and update title and favicon from user's HTML - const { title, favicon } = extractTitleAndFavicon(code); - updateMainPageTitleAndFavicon(title, favicon); - - // Store scroll position before updating - let scrollX = 0, scrollY = 0; - try { - if (preview.contentWindow?.scrollX !== undefined) { - scrollX = preview.contentWindow.scrollX; - scrollY = preview.contentWindow.scrollY; - } - } catch (e) { - // Ignore cross-origin errors - } - - // Use srcdoc to create a completely fresh document context - // Expand <images> tag and rewrite bare stock image filenames - const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>'; - preview.srcdoc = processedCode; - - // Add our functionality after the iframe loads - const onLoad = () => { - try { - const doc = preview.contentDocument; - if (!doc) return; - - // Add CSS for overscroll and button - const style = doc.createElement('style'); - style.textContent = '* { overscroll-behavior: none !important; } .iframe-fullscreen-toggle { position: fixed; top: 5px; right: 5px; z-index: 10000; background: rgba(0, 0, 0, 0.2); color: white; border: none; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -webkit-tap-highlight-color: transparent; outline: none; user-select: none; } .iframe-fullscreen-toggle svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)); opacity: 0.8; transition: opacity 0.2s; } @media (hover: hover) and (pointer: fine) { .iframe-fullscreen-toggle:hover { background: rgba(0, 0, 0, 0.35); } .iframe-fullscreen-toggle:hover svg { opacity: 1; } }'; - doc.head.appendChild(style); - - // Create fullscreen button - const existingButton = doc.getElementById('fullscreenToggle'); - if (existingButton) existingButton.remove(); - - const button = doc.createElement('button'); - button.id = 'fullscreenToggle'; - button.className = 'iframe-fullscreen-toggle'; - button.title = 'Toggle fullscreen'; - - // Use inline SVG to avoid being affected by user's img styles - button.innerHTML = '<svg width="20" height="20" viewBox="0 0 14 14" fill="white"><path d="M 7,14 H 5 v 5 h 5 V 17 H 7 Z M 5,10 H 7 V 7 h 3 V 5 H 5 Z m 12,7 h -3 v 2 h 5 V 14 H 17 Z M 14,5 v 2 h 3 v 3 h 2 V 5 Z" transform="translate(-5,-5)"/></svg>'; - - button.addEventListener('click', function() { - parent.postMessage('toggleFullscreen', '*'); - }); - - if (doc.body) { - doc.body.appendChild(button); - } - - // Add click handlers for stock image table rows - const stockImageRows = doc.querySelectorAll('.stock-image-row'); - stockImageRows.forEach(row => { - row.addEventListener('click', () => { - const filename = row.getAttribute('data-filename'); - if (!filename) return; - - // Find and replace <img src="?"> in the editor - const editorContent = editorView.state.doc.toString(); - const imgPattern = /<img\s+src\s*=\s*["']?\?["']?\s*\/?>/i; - const match = editorContent.match(imgPattern); - - if (match) { - const start = editorContent.indexOf(match[0]); - const end = start + match[0].length; - const replacement = `<img src="${filename}">`; - - editorView.dispatch({ - changes: { from: start, to: end, insert: replacement } - }); - saveToStorage(); - updatePreview(); - } - }); - }); - - // Restore scroll position - setTimeout(() => { - try { - preview.contentWindow?.scrollTo(scrollX, scrollY); - } catch (e) { - // Ignore cross-origin errors - } - }, 10); - } catch (e) { - // Ignore cross-origin errors - } - - preview.removeEventListener('load', onLoad); - }; - preview.addEventListener('load', onLoad); -} - -function saveToStorage() { - try { - localStorage.setItem(storageKey, editorView.state.doc.toString()); - } catch (e) { - console.warn('Could not save to localStorage:', e); - } -} - -function loadFromStorage() { - try { - return localStorage.getItem(storageKey) || ''; - } catch (e) { - console.warn('Could not load from localStorage:', e); - return ''; - } -} - -function loadEditorSettings() { - try { - showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true'; - enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false'; - } catch (e) { - console.warn('Could not load editor settings from localStorage:', e); - showLineNumbers = false; - enableLineWrapping = true; - } -} - -function saveEditorSetting(key, value) { - try { - localStorage.setItem(key, value.toString()); - } catch (e) { - console.warn('Could not save editor setting to localStorage:', e); - } -} - -function toggleLineNumbers() { - const {lineNumbers} = window.CodeMirror; - showLineNumbers = !showLineNumbers; - saveEditorSetting('editor-line-numbers', showLineNumbers); - - editorView.dispatch({ - effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : []) - }); -} - -function createLineWrappingExtension() { - const {EditorView, Decoration} = window.CodeMirror; - - return [ - EditorView.lineWrapping, - EditorView.decorations.of((view) => { - const decorations = []; - - for (let {from, to} of view.visibleRanges) { - for (let pos = from; pos <= to;) { - const line = view.state.doc.lineAt(pos); - const lineText = line.text; - - // Calculate indentation level (count leading whitespace) - let indentChars = 0; - for (let i = 0; i < lineText.length; i++) { - if (lineText[i] === '\t') { - indentChars += 2; // Convert tab to 2 spaces for calculation - } else if (lineText[i] === ' ') { - indentChars += 1; - } else { - break; - } - } - - // Apply hanging indent if line has indentation - if (indentChars > 0) { - const indentDecoration = Decoration.line({ - attributes: { - style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);` - } - }); - decorations.push(indentDecoration.range(line.from)); - } - - pos = line.to + 1; - } - } - - return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none; - }), - ]; -} - -function toggleLineWrapping() { - enableLineWrapping = !enableLineWrapping; - saveEditorSetting('editor-line-wrapping', enableLineWrapping); - - const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; - - editorView.dispatch({ - effects: lineWrappingCompartment.reconfigure(lineWrappingExtension) - }); -} - -// File operations -window.saveFile = function() { - const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'index.html'; - a.click(); - URL.revokeObjectURL(url); -}; - -window.loadFile = function() { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.html,.htm'; - input.onchange = function(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function(e) { - editorView.dispatch({ - changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result } - }); - saveToStorage(); - updatePreview(); - }; - reader.readAsText(file); - }; - input.click(); -}; - -// Wait for CodeMirror to be available -function initializeCodeMirror() { - if (!window.CodeMirror) { - setTimeout(initializeCodeMirror, 100); - return; - } - - const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, history, closeBrackets, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror; - - // Custom phrase overrides for CodeMirror UI (search panel) - const customPhrases = EditorState.phrases.of({ - "Find": "Find..." - }); - - // Load saved content and editor settings - const savedContent = loadFromStorage(); - loadEditorSettings(); - - // Create compartments for dynamic extensions - lineNumbersCompartment = new Compartment(); - lineWrappingCompartment = new Compartment(); - - const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; - - // Create CodeMirror editor - editorView = new EditorView({ - state: EditorState.create({ - doc: savedContent, - extensions: [ - customPhrases, - history(), - search(), - closeBrackets(), - keymap.of([ - {key: "Mod-z", run: undo}, - {key: "Mod-y", run: redo}, - {key: "Mod-Shift-z", run: redo}, - {key: "Mod-o", run: () => { window.loadFile(); return true; }}, - {key: "Mod-s", run: () => { window.saveFile(); return true; }}, - {key: "F1", run: () => { toggleLineNumbers(); return true; }}, - {key: "F2", run: () => { toggleLineWrapping(); return true; }}, - indentWithTab, - ...searchKeymap.filter(binding => binding.key !== "Mod-f"), - ...defaultKeymap - ]), - html(), - // Auto-close <style> and <script> tags (not handled by default html() extension) - // Also expand <!> into HTML boilerplate - EditorView.inputHandler.of((view, from, to, text) => { - if (text !== '>') return false; - const before = view.state.doc.sliceString(Math.max(0, from - 20), from); - // Check for <!> boilerplate trigger - if (before.endsWith('<!')) { - const boilerplate = `<!DOCTYPE html> -<html> -\t<head> -\t\t<style> -\t\t\t -\t\t</style> -\t</head> -\t<body> -\t\t -\t</body> -</html>`; - const startPos = from - 2; - const cursorPos = startPos + boilerplate.indexOf('<body>') + 9; - view.dispatch({ - changes: { from: startPos, to: from, insert: boilerplate }, - selection: { anchor: cursorPos } - }); - return true; - } - // Check for <style> or <script> - const match = before.match(/<(style|script)(\s[^>]*)?$/i); - if (!match) return false; - const tagName = match[1].toLowerCase(); - const closingTag = `</${tagName}>`; - view.dispatch({ - changes: { from, to, insert: '>' + closingTag }, - selection: { anchor: from + 1 } - }); - return true; - }), - githubDark, - indentUnit.of("\t"), - placeholder("Build something with HTML..."), - EditorView.updateListener.of((update) => { - if (update.docChanged) { - clearTimeout(updateTimer); - updateTimer = setTimeout(updatePreview, 600); - saveToStorage(); - } - }), - // Disable text correction and autocomplete - EditorView.contentAttributes.of({ - 'autocomplete': 'off', - 'autocorrect': 'off', - 'autocapitalize': 'off', - 'spellcheck': 'false' - }), - lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []), - lineWrappingCompartment.of(initialLineWrappingExtension) - ] - }), - parent: document.getElementById('editor') - }); - - // Initial render (wait for stock images manifest to load first) - stockImagesReady.then(() => updatePreview()); - - // Track editor focus and handle keyboard dismissal - editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; }); - - editorView.contentDOM.addEventListener('blur', () => { - isEditorFocused = false; - - // If we are on mobile and the keyboard mode is active, - // exit immediately. Do not wait for visualViewport resize. - if (isMobileDevice()) { - exitMobileKeyboardMode(); - } - }); - - // Focus the editor - editorView.focus(); - - // Global keydown handler for Cmd+F toggle - document.addEventListener('keydown', function(e) { - if ((e.metaKey || e.ctrlKey) && e.key === 'f') { - e.preventDefault(); - closeSearchPanel(editorView) || openSearchPanel(editorView); - } - }); - - // Disable browser autocomplete on search panel inputs when they appear - const editorElement = document.getElementById('editor'); - const searchInputObserver = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - const searchInputs = node.querySelectorAll?.('.cm-search input[name="search"], .cm-search input[name="replace"]'); - searchInputs?.forEach(input => input.setAttribute('autocomplete', 'off')); - } - } - } - }); - searchInputObserver.observe(editorElement, { childList: true, subtree: true }); -} - -// Mobile keyboard detection -function isMobileDevice() { - return window.matchMedia("(pointer: coarse), (pointer: none)").matches; -} - -let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; -let isEditorFocused = false; - -// Handle transition from fullscreen keyboard mode back to split view -function exitMobileKeyboardMode() { - if (!document.body.classList.contains('mobile-keyboard-open')) return; - - // Hide editor immediately to prevent visual stutter/jump - editorPane.style.opacity = '0'; - document.body.classList.remove('mobile-keyboard-open'); - - // Wait for layout to settle, then restore scroll and opacity - requestAnimationFrame(() => requestAnimationFrame(() => { - if (editorView) { - // Scroll cursor into view in the new 50% layout - const pos = editorView.state.selection.main.head; - const lineBlock = editorView.lineBlockAt(pos); - const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2); - editorView.scrollDOM.scrollTop = Math.max(0, targetScroll); - } - editorPane.style.opacity = ''; - - // Update preview with any changes made while keyboard was open - updatePreview(); - })); -} - -function updateViewportVariables() { - const vv = window.visualViewport; - if (vv) { - document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`); - document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`); - } -} - -function handleViewportChange() { - if (!isMobileDevice()) return; - - const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; - const heightDifference = initialViewportHeight - currentHeight; - const isKeyboardOpen = heightDifference > 150; - - updateViewportVariables(); - - if (isKeyboardOpen && isEditorFocused) { - document.body.classList.add('mobile-keyboard-open'); - } else if (!isKeyboardOpen) { - // Fallback for keyboard dismissal that doesn't trigger blur (e.g. Android) - exitMobileKeyboardMode(); - } -} - -if (window.visualViewport) { - window.visualViewport.addEventListener('resize', handleViewportChange); - window.visualViewport.addEventListener('scroll', updateViewportVariables); -} else { - window.addEventListener('resize', handleViewportChange); -} - -// Initialize when page loads -initializeCodeMirror(); - -// Register service worker -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('./sw.js') - .then(registration => { - console.log('SW registered: ', registration); - }) - .catch(registrationError => { - console.log('SW registration failed: ', registrationError); - }); - }); -} diff --git a/manifest.json b/manifest.json @@ -9,12 +9,12 @@ "orientation": "any", "icons": [ { - "src": "resources/construction_padded.png", + "src": "resources/icons/construction_padded.png", "sizes": "200x200", "type": "image/png" }, { - "src": "resources/construction_padded.png", + "src": "resources/icons/construction_padded.png", "sizes": "192x192", "type": "image/png" } diff --git a/pages/avocado_warehouse/index.html b/pages/avocado_warehouse/index.html @@ -36,7 +36,7 @@ </ul> <center> <h1 style="color:goldenrod">Try it Now!! Our yummy avocado coffee beverage</h1> - <img src="https://hunterirving.github.io/web_workshop/images/coffee.png" style="width:200px; height:200px;"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/coffee.png" style="width:200px; height:200px;"> <i><p>"What the? Eww..." - Jen Lu</p></i> <hr> <p>Avocados in the courtroom?<br>This vid made us L-O-L<br>(Laff Out Loud)</p> @@ -44,7 +44,7 @@ <br><br> <hr> <br> - <img src="https://hunterirving.github.io/web_workshop/images/sun.png" style="background-color:LemonChiffon; max-width:400px"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/sun.png" style="background-color:LemonChiffon; max-width:400px"> <p>Anyway, thanks for checking out the site! Stay tuned for our family's prized avocado growing tips! 🌞</p> </center> </body> diff --git a/pages/my_homepage/index.html b/pages/my_homepage/index.html @@ -29,21 +29,21 @@ Welcome to my World Wide Web Site 🌞 </h1> <center> - <img src="https://hunterirving.github.io/web_workshop/images/welcome.gif" width=400px> + <img src="https://hunterirving.github.io/web_workshop/resources/images/welcome.gif" width=400px> <p> brace yourself. you are in for a wild ride!! </p> - <a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/images/windows.gif" style="padding:10px;"></a> + <a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/resources/images/windows.gif" style="padding:10px;"></a> <h3>enter by "clicking" on the window above....<br>if you <i><span style="color:red">dare</span></i>.....!</h3> - <img src="https://hunterirving.github.io/web_workshop/images/scared_mouse.gif" style="transform: scaleX(-1); margin-bottom:25px"><img src="https://hunterirving.github.io/web_workshop/images/mac.png" width=200px> + <img src="https://hunterirving.github.io/web_workshop/resources/images/scared_mouse.gif" style="transform: scaleX(-1); margin-bottom:25px"><img src="https://hunterirving.github.io/web_workshop/resources/images/mac.png" width=200px> </center> <h1 class="heading"> Here are a few of my favorite images... 🍀 </h1> <center> - <img src="https://hunterirving.github.io/web_workshop/images/cd.gif" style="width:150px"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/cd.gif" style="width:150px"> <br><br> - <img src="https://hunterirving.github.io/web_workshop/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;"> </center> </body> </html> \ No newline at end of file diff --git a/pages/pizza_heaven/index.html b/pages/pizza_heaven/index.html @@ -46,9 +46,9 @@ <b><span style="color: red; text-shadow: 3px 3px 5px white;">THE BEST</span> <span style="color:green; text-shadow: 3px 3px 5px white;">PIZZA</span> <span style="color:white">IN THE WORLD</span></b> </p> <hr> - <img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"> - <img src="https://hunterirving.github.io/web_workshop/images/gaia.gif" width=200px> - <img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"> + <img src="https://hunterirving.github.io/web_workshop/resources/images/gaia.gif" width=200px> + <img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"> <div class="block"> <marquee scrollamount=20>AYY, I'M MAKIN' PIZZA HERE !</marquee> </div> @@ -92,6 +92,6 @@ But you better not disparage our pizza pies again, capeeshe??? </p> <br><br> - <center><img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"></center><br><br> + <center><img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"></center><br><br> </body> </html> \ No newline at end of file diff --git a/resource-manifest.json b/resource-manifest.json @@ -1,135 +0,0 @@ -{ - "generated_at": "auto-generated by GitHub Actions", - "images": [ - "/web_workshop/images/24.gif", - "/web_workshop/images/bang.gif", - "/web_workshop/images/bg_bricks.jpg", - "/web_workshop/images/bg_checker.gif", - "/web_workshop/images/bg_daisies.jpg", - "/web_workshop/images/bg_desert.gif", - "/web_workshop/images/bg_stars.gif", - "/web_workshop/images/bg_trippy.jpg", - "/web_workshop/images/bg_water.jpg", - "/web_workshop/images/bigN.gif", - "/web_workshop/images/birdbath.gif", - "/web_workshop/images/bizzy_wizzy.gif", - "/web_workshop/images/blahblah.gif", - "/web_workshop/images/bonk.gif", - "/web_workshop/images/bright_idea.gif", - "/web_workshop/images/butterflies.gif", - "/web_workshop/images/butterfly_blue.gif", - "/web_workshop/images/butterfly_monarch.gif", - "/web_workshop/images/cactus.gif", - "/web_workshop/images/cat.gif", - "/web_workshop/images/caterpillar.gif", - "/web_workshop/images/cd.gif", - "/web_workshop/images/christmas_garland.gif", - "/web_workshop/images/christmas_holly.gif", - "/web_workshop/images/christmas_wreath.gif", - "/web_workshop/images/coffee.gif", - "/web_workshop/images/coffee.png", - "/web_workshop/images/computer.gif", - "/web_workshop/images/construct.gif", - "/web_workshop/images/cow.gif", - "/web_workshop/images/daisies.gif", - "/web_workshop/images/dancingman.gif", - "/web_workshop/images/dancingwoman.gif", - "/web_workshop/images/devil.gif", - "/web_workshop/images/disco_ball.gif", - "/web_workshop/images/divider_blissey.gif", - "/web_workshop/images/divider_blue_flowers.gif", - "/web_workshop/images/divider_blue_flowers2.gif", - "/web_workshop/images/divider_rainbow.gif", - "/web_workshop/images/divider_rainbow_dots.gif", - "/web_workshop/images/divider_red_flowers.png", - "/web_workshop/images/divider_wildflowers.gif", - "/web_workshop/images/divider_yellow_flowers.gif", - "/web_workshop/images/dollhouse.gif", - "/web_workshop/images/doraemon.gif", - "/web_workshop/images/doraemon_stamp.gif", - "/web_workshop/images/email.gif", - "/web_workshop/images/email2.gif", - "/web_workshop/images/flowers.gif", - "/web_workshop/images/frog_car.gif", - "/web_workshop/images/furby1.gif", - "/web_workshop/images/furby2.gif", - "/web_workshop/images/furby3.gif", - "/web_workshop/images/furby4.gif", - "/web_workshop/images/furby5.gif", - "/web_workshop/images/furby6.gif", - "/web_workshop/images/furby7.gif", - "/web_workshop/images/furby8.gif", - "/web_workshop/images/furby9.gif", - "/web_workshop/images/furby_banner.gif", - "/web_workshop/images/furby_clouds.gif", - "/web_workshop/images/furby_field.jpg", - "/web_workshop/images/furby_pink.png", - "/web_workshop/images/furby_shocked.gif", - "/web_workshop/images/furby_weird.gif", - "/web_workshop/images/furbyland.gif", - "/web_workshop/images/gaia.gif", - "/web_workshop/images/garf.gif", - "/web_workshop/images/gc_icon.gif", - "/web_workshop/images/hacker_skull.gif", - "/web_workshop/images/hacking.gif", - "/web_workshop/images/hamster.gif", - "/web_workshop/images/hamtaro.gif", - "/web_workshop/images/hellokitty.gif", - "/web_workshop/images/hint.gif", - "/web_workshop/images/home.gif", - "/web_workshop/images/koosh.gif", - "/web_workshop/images/lil_buddha.gif", - "/web_workshop/images/lil_demon.gif", - "/web_workshop/images/linux_power.gif", - "/web_workshop/images/mac.png", - "/web_workshop/images/mailbox.gif", - "/web_workshop/images/mario64.gif", - "/web_workshop/images/modchips.gif", - "/web_workshop/images/neopet.gif", - "/web_workshop/images/ness.gif", - "/web_workshop/images/new_bunny.gif", - "/web_workshop/images/no_java.gif", - "/web_workshop/images/orbit.gif", - "/web_workshop/images/pika_construction.gif", - "/web_workshop/images/pink_clock.gif", - "/web_workshop/images/pochacco.gif", - "/web_workshop/images/providence.gif", - "/web_workshop/images/ratrace.gif", - "/web_workshop/images/ring.gif", - "/web_workshop/images/sailor_moon.gif", - "/web_workshop/images/samus.gif", - "/web_workshop/images/scachis.gif", - "/web_workshop/images/scared_mouse.gif", - "/web_workshop/images/smiley_bar.gif", - "/web_workshop/images/smiley_dancing.gif", - "/web_workshop/images/smiling_pizza.gif", - "/web_workshop/images/spacedog.gif", - "/web_workshop/images/spinninbrain.gif", - "/web_workshop/images/splat.png", - "/web_workshop/images/sun.png", - "/web_workshop/images/surfin.gif", - "/web_workshop/images/tamas.gif", - "/web_workshop/images/telephone.gif", - "/web_workshop/images/underconstruction.gif", - "/web_workshop/images/welcome.gif", - "/web_workshop/images/windows.gif", - "/web_workshop/images/wishingwell.gif", - "/web_workshop/images/wizard.gif", - "/web_workshop/images/ww_9volt.gif", - "/web_workshop/images/ww_ana.gif", - "/web_workshop/images/ww_crygor.gif", - "/web_workshop/images/ww_dribble.gif", - "/web_workshop/images/ww_jimmy.gif", - "/web_workshop/images/ww_kat.gif", - "/web_workshop/images/ww_mona.gif", - "/web_workshop/images/ww_orbulon.gif", - "/web_workshop/images/ww_spitz.gif", - "/web_workshop/images/ww_wario.gif", - "/web_workshop/images/y2k.gif" - ], - "resources": [ - "/web_workshop/resources/construction.png", - "/web_workshop/resources/construction_padded.png", - "/web_workshop/resources/fullscreen.svg" - ] -} -\ No newline at end of file diff --git a/styles.css b/resources/css/styles.css diff --git a/resources/construction.png b/resources/icons/construction.png Binary files differ. diff --git a/resources/construction_padded.png b/resources/icons/construction_padded.png Binary files differ. diff --git a/images/24.gif b/resources/images/24.gif Binary files differ. diff --git a/images/bang.gif b/resources/images/bang.gif Binary files differ. diff --git a/images/bg_bricks.jpg b/resources/images/bg_bricks.jpg Binary files differ. diff --git a/images/bg_checker.gif b/resources/images/bg_checker.gif Binary files differ. diff --git a/images/bg_daisies.jpg b/resources/images/bg_daisies.jpg Binary files differ. diff --git a/images/bg_desert.gif b/resources/images/bg_desert.gif Binary files differ. diff --git a/images/bg_stars.gif b/resources/images/bg_stars.gif Binary files differ. diff --git a/images/bg_trippy.jpg b/resources/images/bg_trippy.jpg Binary files differ. diff --git a/images/bg_water.jpg b/resources/images/bg_water.jpg Binary files differ. diff --git a/images/bigN.gif b/resources/images/bigN.gif Binary files differ. diff --git a/images/birdbath.gif b/resources/images/birdbath.gif Binary files differ. diff --git a/images/bizzy_wizzy.gif b/resources/images/bizzy_wizzy.gif Binary files differ. diff --git a/images/blahblah.gif b/resources/images/blahblah.gif Binary files differ. diff --git a/images/bonk.gif b/resources/images/bonk.gif Binary files differ. diff --git a/images/bright_idea.gif b/resources/images/bright_idea.gif Binary files differ. diff --git a/images/butterflies.gif b/resources/images/butterflies.gif Binary files differ. diff --git a/images/butterfly_blue.gif b/resources/images/butterfly_blue.gif Binary files differ. diff --git a/images/butterfly_monarch.gif b/resources/images/butterfly_monarch.gif Binary files differ. diff --git a/images/cactus.gif b/resources/images/cactus.gif Binary files differ. diff --git a/images/cat.gif b/resources/images/cat.gif Binary files differ. diff --git a/images/caterpillar.gif b/resources/images/caterpillar.gif Binary files differ. diff --git a/images/cd.gif b/resources/images/cd.gif Binary files differ. diff --git a/images/christmas_garland.gif b/resources/images/christmas_garland.gif Binary files differ. diff --git a/images/christmas_holly.gif b/resources/images/christmas_holly.gif Binary files differ. diff --git a/images/christmas_wreath.gif b/resources/images/christmas_wreath.gif Binary files differ. diff --git a/images/coffee.gif b/resources/images/coffee.gif Binary files differ. diff --git a/images/coffee.png b/resources/images/coffee.png Binary files differ. diff --git a/images/computer.gif b/resources/images/computer.gif Binary files differ. diff --git a/images/construct.gif b/resources/images/construct.gif Binary files differ. diff --git a/images/cow.gif b/resources/images/cow.gif Binary files differ. diff --git a/images/daisies.gif b/resources/images/daisies.gif Binary files differ. diff --git a/images/dancingman.gif b/resources/images/dancingman.gif Binary files differ. diff --git a/images/dancingwoman.gif b/resources/images/dancingwoman.gif Binary files differ. diff --git a/images/devil.gif b/resources/images/devil.gif Binary files differ. diff --git a/images/disco_ball.gif b/resources/images/disco_ball.gif Binary files differ. diff --git a/images/divider_blissey.gif b/resources/images/divider_blissey.gif Binary files differ. diff --git a/images/divider_blue_flowers.gif b/resources/images/divider_blue_flowers.gif Binary files differ. diff --git a/images/divider_blue_flowers2.gif b/resources/images/divider_blue_flowers2.gif Binary files differ. diff --git a/images/divider_rainbow.gif b/resources/images/divider_rainbow.gif Binary files differ. diff --git a/images/divider_rainbow_dots.gif b/resources/images/divider_rainbow_dots.gif Binary files differ. diff --git a/images/divider_red_flowers.png b/resources/images/divider_red_flowers.png Binary files differ. diff --git a/images/divider_wildflowers.gif b/resources/images/divider_wildflowers.gif Binary files differ. diff --git a/images/divider_yellow_flowers.gif b/resources/images/divider_yellow_flowers.gif Binary files differ. diff --git a/images/dollhouse.gif b/resources/images/dollhouse.gif Binary files differ. diff --git a/images/doraemon.gif b/resources/images/doraemon.gif Binary files differ. diff --git a/images/doraemon_stamp.gif b/resources/images/doraemon_stamp.gif Binary files differ. diff --git a/images/email.gif b/resources/images/email.gif Binary files differ. diff --git a/images/email2.gif b/resources/images/email2.gif Binary files differ. diff --git a/images/flowers.gif b/resources/images/flowers.gif Binary files differ. diff --git a/images/frog_car.gif b/resources/images/frog_car.gif Binary files differ. diff --git a/images/furby1.gif b/resources/images/furby1.gif Binary files differ. diff --git a/images/furby2.gif b/resources/images/furby2.gif Binary files differ. diff --git a/images/furby3.gif b/resources/images/furby3.gif Binary files differ. diff --git a/images/furby4.gif b/resources/images/furby4.gif Binary files differ. diff --git a/images/furby5.gif b/resources/images/furby5.gif Binary files differ. diff --git a/images/furby6.gif b/resources/images/furby6.gif Binary files differ. diff --git a/images/furby7.gif b/resources/images/furby7.gif Binary files differ. diff --git a/images/furby8.gif b/resources/images/furby8.gif Binary files differ. diff --git a/images/furby9.gif b/resources/images/furby9.gif Binary files differ. diff --git a/images/furby_banner.gif b/resources/images/furby_banner.gif Binary files differ. diff --git a/images/furby_clouds.gif b/resources/images/furby_clouds.gif Binary files differ. diff --git a/images/furby_field.jpg b/resources/images/furby_field.jpg Binary files differ. diff --git a/images/furby_pink.png b/resources/images/furby_pink.png Binary files differ. diff --git a/images/furby_shocked.gif b/resources/images/furby_shocked.gif Binary files differ. diff --git a/images/furby_weird.gif b/resources/images/furby_weird.gif Binary files differ. diff --git a/images/furbyland.gif b/resources/images/furbyland.gif Binary files differ. diff --git a/images/gaia.gif b/resources/images/gaia.gif Binary files differ. diff --git a/images/garf.gif b/resources/images/garf.gif Binary files differ. diff --git a/images/gc_icon.gif b/resources/images/gc_icon.gif Binary files differ. diff --git a/images/hacker_skull.gif b/resources/images/hacker_skull.gif Binary files differ. diff --git a/images/hacking.gif b/resources/images/hacking.gif Binary files differ. diff --git a/images/hamster.gif b/resources/images/hamster.gif Binary files differ. diff --git a/images/hamtaro.gif b/resources/images/hamtaro.gif Binary files differ. diff --git a/images/hellokitty.gif b/resources/images/hellokitty.gif Binary files differ. diff --git a/images/hint.gif b/resources/images/hint.gif Binary files differ. diff --git a/images/home.gif b/resources/images/home.gif Binary files differ. diff --git a/images/koosh.gif b/resources/images/koosh.gif Binary files differ. diff --git a/images/lil_buddha.gif b/resources/images/lil_buddha.gif Binary files differ. diff --git a/images/lil_demon.gif b/resources/images/lil_demon.gif Binary files differ. diff --git a/images/linux_power.gif b/resources/images/linux_power.gif Binary files differ. diff --git a/images/mac.png b/resources/images/mac.png Binary files differ. diff --git a/images/mailbox.gif b/resources/images/mailbox.gif Binary files differ. diff --git a/images/mario64.gif b/resources/images/mario64.gif Binary files differ. diff --git a/images/modchips.gif b/resources/images/modchips.gif Binary files differ. diff --git a/images/neopet.gif b/resources/images/neopet.gif Binary files differ. diff --git a/images/ness.gif b/resources/images/ness.gif Binary files differ. diff --git a/images/new_bunny.gif b/resources/images/new_bunny.gif Binary files differ. diff --git a/images/no_java.gif b/resources/images/no_java.gif Binary files differ. diff --git a/images/orbit.gif b/resources/images/orbit.gif Binary files differ. diff --git a/images/pika_construction.gif b/resources/images/pika_construction.gif Binary files differ. diff --git a/images/pink_clock.gif b/resources/images/pink_clock.gif Binary files differ. diff --git a/images/pochacco.gif b/resources/images/pochacco.gif Binary files differ. diff --git a/images/providence.gif b/resources/images/providence.gif Binary files differ. diff --git a/images/ratrace.gif b/resources/images/ratrace.gif Binary files differ. diff --git a/images/ring.gif b/resources/images/ring.gif Binary files differ. diff --git a/images/sailor_moon.gif b/resources/images/sailor_moon.gif Binary files differ. diff --git a/images/samus.gif b/resources/images/samus.gif Binary files differ. diff --git a/images/scachis.gif b/resources/images/scachis.gif Binary files differ. diff --git a/images/scared_mouse.gif b/resources/images/scared_mouse.gif Binary files differ. diff --git a/images/smiley_bar.gif b/resources/images/smiley_bar.gif Binary files differ. diff --git a/images/smiley_dancing.gif b/resources/images/smiley_dancing.gif Binary files differ. diff --git a/images/smiling_pizza.gif b/resources/images/smiling_pizza.gif Binary files differ. diff --git a/images/spacedog.gif b/resources/images/spacedog.gif Binary files differ. diff --git a/images/spinninbrain.gif b/resources/images/spinninbrain.gif Binary files differ. diff --git a/images/splat.png b/resources/images/splat.png Binary files differ. diff --git a/images/sun.png b/resources/images/sun.png Binary files differ. diff --git a/images/surfin.gif b/resources/images/surfin.gif Binary files differ. diff --git a/images/tamas.gif b/resources/images/tamas.gif Binary files differ. diff --git a/images/telephone.gif b/resources/images/telephone.gif Binary files differ. diff --git a/images/underconstruction.gif b/resources/images/underconstruction.gif Binary files differ. diff --git a/images/welcome.gif b/resources/images/welcome.gif Binary files differ. diff --git a/images/windows.gif b/resources/images/windows.gif Binary files differ. diff --git a/images/wishingwell.gif b/resources/images/wishingwell.gif Binary files differ. diff --git a/images/wizard.gif b/resources/images/wizard.gif Binary files differ. diff --git a/images/ww_9volt.gif b/resources/images/ww_9volt.gif Binary files differ. diff --git a/images/ww_ana.gif b/resources/images/ww_ana.gif Binary files differ. diff --git a/images/ww_crygor.gif b/resources/images/ww_crygor.gif Binary files differ. diff --git a/images/ww_dribble.gif b/resources/images/ww_dribble.gif Binary files differ. diff --git a/images/ww_jimmy.gif b/resources/images/ww_jimmy.gif Binary files differ. diff --git a/images/ww_kat.gif b/resources/images/ww_kat.gif Binary files differ. diff --git a/images/ww_mona.gif b/resources/images/ww_mona.gif Binary files differ. diff --git a/images/ww_orbulon.gif b/resources/images/ww_orbulon.gif Binary files differ. diff --git a/images/ww_spitz.gif b/resources/images/ww_spitz.gif Binary files differ. diff --git a/images/ww_wario.gif b/resources/images/ww_wario.gif Binary files differ. diff --git a/images/y2k.gif b/resources/images/y2k.gif Binary files differ. diff --git a/codemirror-bundle.js b/resources/js/codemirror-bundle.js diff --git a/resources/js/main.js b/resources/js/main.js @@ -0,0 +1,609 @@ +let updateTimer; +let editorView; +const preview = document.getElementById('preview'); + +// Stock images loaded from manifest - can be referenced by bare filename +let stockImages = new Map(); // lowercase -> original filename + +// Load stock image list from manifest +const stockImagesReady = fetch('resources/resource-manifest.json') + .then(r => r.json()) + .then(manifest => { + manifest.images.forEach(path => { + const filename = path.split('/').pop(); + stockImages.set(filename.toLowerCase(), filename); + }); + }) + .catch(() => console.log('Could not load resource manifest')); + +// Rewrite bare image filenames to use resources/images/ prefix (case-insensitive, uses correct case) +function rewriteBareImageSrcs(html) { + // Rewrite <img src="filename.ext"> + html = html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi, + (match, before, filename, ext, after) => { + const original = stockImages.get(filename.toLowerCase()); + if (original) { + return before + 'resources/images/' + original + after; + } + return match; + }); + // Rewrite url(filename.ext) in CSS + html = html.replace(/(url\(\s*["']?)([^"')/:]+\.(gif|png|jpg|jpeg|svg|webp))(["']?\s*\))/gi, + (match, before, filename, ext, after) => { + const original = stockImages.get(filename.toLowerCase()); + if (original) { + return before + 'resources/images/' + original + after; + } + return match; + }); + return html; +} + +// Expand <img src="?"> tags into gallery table +function expandImagesTag(html) { + return html.replace(/<img\s+src\s*=\s*["']?\?["']?\s*\/?>/gi, () => { + const images = Array.from(stockImages.values()).sort(); + if (images.length === 0) return '<p>No images available</p>'; + const rows = images.map(filename => + `<tr class="stock-image-row" data-filename="${filename}" style="cursor:pointer;user-select:none;"><td>${filename}</td><td style="text-align:center;"><img src="resources/images/${filename}" style="max-width:100%;height:auto;pointer-events:none;"></td></tr>` + ).join(''); + return `<table class="stock-image-table" border="1" cellpadding="8" cellspacing="0" style="max-width:100%;box-sizing:border-box;table-layout:fixed;"><colgroup><col style="width:50%"><col style="width:50%"></colgroup>${rows}</table>`; + }); +} +const editorPane = document.querySelector('.editor-pane'); +const previewPane = document.querySelector('.preview-pane'); +const storageKey = 'html-lab-content'; +let isFullscreen = false; +let showLineNumbers = false; +let enableLineWrapping = false; +let lineNumbersCompartment; +let lineWrappingCompartment; + +function toggleFullscreen() { + isFullscreen = !isFullscreen; + + if (isFullscreen) { + previewPane.classList.add('fullscreen'); + editorPane.classList.add('hidden'); + // Set iframe height to actual visible height (fixes iOS Safari toolbar issue) + preview.style.height = window.innerHeight + 'px'; + } else { + previewPane.classList.remove('fullscreen'); + editorPane.classList.remove('hidden'); + preview.style.height = ''; + } + + // Update button title in iframe + try { + const toggleButton = preview.contentDocument.getElementById('fullscreenToggle'); + if (toggleButton) { + toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen'; + } + } catch (e) { + // Ignore cross-origin errors + } +} + + +// Listen for messages from iframe +window.addEventListener('message', function(event) { + if (event.data === 'toggleFullscreen') { + toggleFullscreen(); + } +}); + +function extractTitleAndFavicon(htmlCode) { + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlCode, 'text/html'); + + // Extract title + const titleElement = doc.querySelector('title'); + const title = titleElement ? titleElement.textContent.trim() : null; + + // Extract favicon + const faviconSelectors = [ + 'link[rel="icon"]', + 'link[rel="shortcut icon"]', + 'link[rel="apple-touch-icon"]', + 'link[rel="mask-icon"]' + ]; + + let favicon = null; + for (const selector of faviconSelectors) { + const faviconElement = doc.querySelector(selector); + if (faviconElement && faviconElement.href) { + favicon = faviconElement.href; + break; + } + } + + return { title, favicon }; +} + +function updateMainPageTitleAndFavicon(title, favicon) { + // Update title + if (title) { + document.title = title; + } else { + document.title = 'Web Workshop'; + } + + // Update favicon + let faviconLink = document.querySelector('link[rel="icon"]'); + if (!faviconLink) { + faviconLink = document.createElement('link'); + faviconLink.rel = 'icon'; + document.head.appendChild(faviconLink); + } + + if (favicon) { + faviconLink.href = favicon; + } else { + faviconLink.href = 'resources/icons/construction.png'; + } +} + +function updatePreview() { + // Skip preview updates while mobile keyboard is open and editor is focused + // This prevents keyboard layer resets on Android + if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) { + return; + } + + const code = editorView.state.doc.toString(); + + // Extract and update title and favicon from user's HTML + const { title, favicon } = extractTitleAndFavicon(code); + updateMainPageTitleAndFavicon(title, favicon); + + // Store scroll position before updating + let scrollX = 0, scrollY = 0; + try { + if (preview.contentWindow?.scrollX !== undefined) { + scrollX = preview.contentWindow.scrollX; + scrollY = preview.contentWindow.scrollY; + } + } catch (e) { + // Ignore cross-origin errors + } + + // Use srcdoc to create a completely fresh document context + // Expand <images> tag and rewrite bare stock image filenames + const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>'; + preview.srcdoc = processedCode; + + // Add our functionality after the iframe loads + const onLoad = () => { + try { + const doc = preview.contentDocument; + if (!doc) return; + + // Add CSS for overscroll and button + const style = doc.createElement('style'); + style.textContent = '* { overscroll-behavior: none !important; } .iframe-fullscreen-toggle { position: fixed; top: 5px; right: 5px; z-index: 10000; background: rgba(0, 0, 0, 0.2); color: white; border: none; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -webkit-tap-highlight-color: transparent; outline: none; user-select: none; } .iframe-fullscreen-toggle svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)); opacity: 0.8; transition: opacity 0.2s; } @media (hover: hover) and (pointer: fine) { .iframe-fullscreen-toggle:hover { background: rgba(0, 0, 0, 0.35); } .iframe-fullscreen-toggle:hover svg { opacity: 1; } }'; + doc.head.appendChild(style); + + // Create fullscreen button + const existingButton = doc.getElementById('fullscreenToggle'); + if (existingButton) existingButton.remove(); + + const button = doc.createElement('button'); + button.id = 'fullscreenToggle'; + button.className = 'iframe-fullscreen-toggle'; + button.title = 'Toggle fullscreen'; + + // Use inline SVG to avoid being affected by user's img styles + button.innerHTML = '<svg width="20" height="20" viewBox="0 0 14 14" fill="white"><path d="M 7,14 H 5 v 5 h 5 V 17 H 7 Z M 5,10 H 7 V 7 h 3 V 5 H 5 Z m 12,7 h -3 v 2 h 5 V 14 H 17 Z M 14,5 v 2 h 3 v 3 h 2 V 5 Z" transform="translate(-5,-5)"/></svg>'; + + button.addEventListener('click', function() { + parent.postMessage('toggleFullscreen', '*'); + }); + + if (doc.body) { + doc.body.appendChild(button); + } + + // Add click handlers for stock image table rows + const stockImageRows = doc.querySelectorAll('.stock-image-row'); + stockImageRows.forEach(row => { + row.addEventListener('click', () => { + const filename = row.getAttribute('data-filename'); + if (!filename) return; + + // Find and replace <img src="?"> in the editor + const editorContent = editorView.state.doc.toString(); + const imgPattern = /<img\s+src\s*=\s*["']?\?["']?\s*\/?>/i; + const match = editorContent.match(imgPattern); + + if (match) { + const start = editorContent.indexOf(match[0]); + const end = start + match[0].length; + const replacement = `<img src="${filename}">`; + + editorView.dispatch({ + changes: { from: start, to: end, insert: replacement } + }); + saveToStorage(); + updatePreview(); + } + }); + }); + + // Restore scroll position + setTimeout(() => { + try { + preview.contentWindow?.scrollTo(scrollX, scrollY); + } catch (e) { + // Ignore cross-origin errors + } + }, 10); + } catch (e) { + // Ignore cross-origin errors + } + + preview.removeEventListener('load', onLoad); + }; + preview.addEventListener('load', onLoad); +} + +function saveToStorage() { + try { + localStorage.setItem(storageKey, editorView.state.doc.toString()); + } catch (e) { + console.warn('Could not save to localStorage:', e); + } +} + +function loadFromStorage() { + try { + return localStorage.getItem(storageKey) || ''; + } catch (e) { + console.warn('Could not load from localStorage:', e); + return ''; + } +} + +function loadEditorSettings() { + try { + showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true'; + enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false'; + } catch (e) { + console.warn('Could not load editor settings from localStorage:', e); + showLineNumbers = false; + enableLineWrapping = true; + } +} + +function saveEditorSetting(key, value) { + try { + localStorage.setItem(key, value.toString()); + } catch (e) { + console.warn('Could not save editor setting to localStorage:', e); + } +} + +function toggleLineNumbers() { + const {lineNumbers} = window.CodeMirror; + showLineNumbers = !showLineNumbers; + saveEditorSetting('editor-line-numbers', showLineNumbers); + + editorView.dispatch({ + effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : []) + }); +} + +function createLineWrappingExtension() { + const {EditorView, Decoration} = window.CodeMirror; + + return [ + EditorView.lineWrapping, + EditorView.decorations.of((view) => { + const decorations = []; + + for (let {from, to} of view.visibleRanges) { + for (let pos = from; pos <= to;) { + const line = view.state.doc.lineAt(pos); + const lineText = line.text; + + // Calculate indentation level (count leading whitespace) + let indentChars = 0; + for (let i = 0; i < lineText.length; i++) { + if (lineText[i] === '\t') { + indentChars += 2; // Convert tab to 2 spaces for calculation + } else if (lineText[i] === ' ') { + indentChars += 1; + } else { + break; + } + } + + // Apply hanging indent if line has indentation + if (indentChars > 0) { + const indentDecoration = Decoration.line({ + attributes: { + style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);` + } + }); + decorations.push(indentDecoration.range(line.from)); + } + + pos = line.to + 1; + } + } + + return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none; + }), + ]; +} + +function toggleLineWrapping() { + enableLineWrapping = !enableLineWrapping; + saveEditorSetting('editor-line-wrapping', enableLineWrapping); + + const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; + + editorView.dispatch({ + effects: lineWrappingCompartment.reconfigure(lineWrappingExtension) + }); +} + +// File operations +window.saveFile = function() { + const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'index.html'; + a.click(); + URL.revokeObjectURL(url); +}; + +window.loadFile = function() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.html,.htm'; + input.onchange = function(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + editorView.dispatch({ + changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result } + }); + saveToStorage(); + updatePreview(); + }; + reader.readAsText(file); + }; + input.click(); +}; + +// Wait for CodeMirror to be available +function initializeCodeMirror() { + if (!window.CodeMirror) { + setTimeout(initializeCodeMirror, 100); + return; + } + + const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, history, closeBrackets, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror; + + // Custom phrase overrides for CodeMirror UI (search panel) + const customPhrases = EditorState.phrases.of({ + "Find": "Find..." + }); + + // Load saved content and editor settings + const savedContent = loadFromStorage(); + loadEditorSettings(); + + // Create compartments for dynamic extensions + lineNumbersCompartment = new Compartment(); + lineWrappingCompartment = new Compartment(); + + const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; + + // Create CodeMirror editor + editorView = new EditorView({ + state: EditorState.create({ + doc: savedContent, + extensions: [ + customPhrases, + history(), + search(), + closeBrackets(), + keymap.of([ + {key: "Mod-z", run: undo}, + {key: "Mod-y", run: redo}, + {key: "Mod-Shift-z", run: redo}, + {key: "Mod-o", run: () => { window.loadFile(); return true; }}, + {key: "Mod-s", run: () => { window.saveFile(); return true; }}, + {key: "F1", run: () => { toggleLineNumbers(); return true; }}, + {key: "F2", run: () => { toggleLineWrapping(); return true; }}, + indentWithTab, + ...searchKeymap.filter(binding => binding.key !== "Mod-f"), + ...defaultKeymap + ]), + html(), + // Auto-close <style> and <script> tags (not handled by default html() extension) + // Also expand <!> into HTML boilerplate + EditorView.inputHandler.of((view, from, to, text) => { + if (text !== '>') return false; + const before = view.state.doc.sliceString(Math.max(0, from - 20), from); + // Check for <!> boilerplate trigger + if (before.endsWith('<!')) { + const boilerplate = `<!DOCTYPE html> +<html> +\t<head> +\t\t<style> +\t\t\t +\t\t</style> +\t</head> +\t<body> +\t\t +\t</body> +</html>`; + const startPos = from - 2; + const cursorPos = startPos + boilerplate.indexOf('<body>') + 9; + view.dispatch({ + changes: { from: startPos, to: from, insert: boilerplate }, + selection: { anchor: cursorPos } + }); + return true; + } + // Check for <style> or <script> + const match = before.match(/<(style|script)(\s[^>]*)?$/i); + if (!match) return false; + const tagName = match[1].toLowerCase(); + const closingTag = `</${tagName}>`; + view.dispatch({ + changes: { from, to, insert: '>' + closingTag }, + selection: { anchor: from + 1 } + }); + return true; + }), + githubDark, + indentUnit.of("\t"), + placeholder("Build something with HTML..."), + EditorView.updateListener.of((update) => { + if (update.docChanged) { + clearTimeout(updateTimer); + updateTimer = setTimeout(updatePreview, 600); + saveToStorage(); + } + }), + // Disable text correction and autocomplete + EditorView.contentAttributes.of({ + 'autocomplete': 'off', + 'autocorrect': 'off', + 'autocapitalize': 'off', + 'spellcheck': 'false' + }), + lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []), + lineWrappingCompartment.of(initialLineWrappingExtension) + ] + }), + parent: document.getElementById('editor') + }); + + // Initial render (wait for stock images manifest to load first) + stockImagesReady.then(() => updatePreview()); + + // Track editor focus and handle keyboard dismissal + editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; }); + + editorView.contentDOM.addEventListener('blur', () => { + isEditorFocused = false; + + // If we are on mobile and the keyboard mode is active, + // exit immediately. Do not wait for visualViewport resize. + if (isMobileDevice()) { + exitMobileKeyboardMode(); + } + }); + + // Focus the editor + editorView.focus(); + + // Global keydown handler for Cmd+F toggle + document.addEventListener('keydown', function(e) { + if ((e.metaKey || e.ctrlKey) && e.key === 'f') { + e.preventDefault(); + closeSearchPanel(editorView) || openSearchPanel(editorView); + } + }); + + // Disable browser autocomplete on search panel inputs when they appear + const editorElement = document.getElementById('editor'); + const searchInputObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + const searchInputs = node.querySelectorAll?.('.cm-search input[name="search"], .cm-search input[name="replace"]'); + searchInputs?.forEach(input => input.setAttribute('autocomplete', 'off')); + } + } + } + }); + searchInputObserver.observe(editorElement, { childList: true, subtree: true }); +} + +// Mobile keyboard detection +function isMobileDevice() { + return window.matchMedia("(pointer: coarse), (pointer: none)").matches; +} + +let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; +let isEditorFocused = false; + +// Handle transition from fullscreen keyboard mode back to split view +function exitMobileKeyboardMode() { + if (!document.body.classList.contains('mobile-keyboard-open')) return; + + // Hide editor immediately to prevent visual stutter/jump + editorPane.style.opacity = '0'; + document.body.classList.remove('mobile-keyboard-open'); + + // Wait for layout to settle, then restore scroll and opacity + requestAnimationFrame(() => requestAnimationFrame(() => { + if (editorView) { + // Scroll cursor into view in the new 50% layout + const pos = editorView.state.selection.main.head; + const lineBlock = editorView.lineBlockAt(pos); + const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2); + editorView.scrollDOM.scrollTop = Math.max(0, targetScroll); + } + editorPane.style.opacity = ''; + + // Update preview with any changes made while keyboard was open + updatePreview(); + })); +} + +function updateViewportVariables() { + const vv = window.visualViewport; + if (vv) { + document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`); + document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`); + } +} + +function handleViewportChange() { + if (!isMobileDevice()) return; + + const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; + const heightDifference = initialViewportHeight - currentHeight; + const isKeyboardOpen = heightDifference > 150; + + updateViewportVariables(); + + if (isKeyboardOpen && isEditorFocused) { + document.body.classList.add('mobile-keyboard-open'); + } else if (!isKeyboardOpen) { + // Fallback for keyboard dismissal that doesn't trigger blur (e.g. Android) + exitMobileKeyboardMode(); + } +} + +if (window.visualViewport) { + window.visualViewport.addEventListener('resize', handleViewportChange); + window.visualViewport.addEventListener('scroll', updateViewportVariables); +} else { + window.addEventListener('resize', handleViewportChange); +} + +// Initialize when page loads +initializeCodeMirror(); + +// Register service worker +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('./sw.js') + .then(registration => { + console.log('SW registered: ', registration); + }) + .catch(registrationError => { + console.log('SW registration failed: ', registrationError); + }); + }); +} diff --git a/readme_images/picnic.png b/resources/readme_images/picnic.png Binary files differ. diff --git a/readme_images/stock_images.png b/resources/readme_images/stock_images.png Binary files differ. diff --git a/resources/resource-manifest.json b/resources/resource-manifest.json @@ -0,0 +1,497 @@ +{ + "generated_at": "auto-generated by GitHub Actions", + "images": [ + "/web_workshop/resources/images/24.gif", + "/web_workshop/resources/images/bang.gif", + "/web_workshop/resources/images/bg_bricks.jpg", + "/web_workshop/resources/images/bg_checker.gif", + "/web_workshop/resources/images/bg_daisies.jpg", + "/web_workshop/resources/images/bg_desert.gif", + "/web_workshop/resources/images/bg_stars.gif", + "/web_workshop/resources/images/bg_trippy.jpg", + "/web_workshop/resources/images/bg_water.jpg", + "/web_workshop/resources/images/bigN.gif", + "/web_workshop/resources/images/birdbath.gif", + "/web_workshop/resources/images/bizzy_wizzy.gif", + "/web_workshop/resources/images/blahblah.gif", + "/web_workshop/resources/images/bonk.gif", + "/web_workshop/resources/images/bright_idea.gif", + "/web_workshop/resources/images/butterflies.gif", + "/web_workshop/resources/images/butterfly_blue.gif", + "/web_workshop/resources/images/butterfly_monarch.gif", + "/web_workshop/resources/images/cactus.gif", + "/web_workshop/resources/images/cat.gif", + "/web_workshop/resources/images/caterpillar.gif", + "/web_workshop/resources/images/cd.gif", + "/web_workshop/resources/images/christmas_garland.gif", + "/web_workshop/resources/images/christmas_holly.gif", + "/web_workshop/resources/images/christmas_wreath.gif", + "/web_workshop/resources/images/coffee.gif", + "/web_workshop/resources/images/coffee.png", + "/web_workshop/resources/images/computer.gif", + "/web_workshop/resources/images/construct.gif", + "/web_workshop/resources/images/cow.gif", + "/web_workshop/resources/images/daisies.gif", + "/web_workshop/resources/images/dancingman.gif", + "/web_workshop/resources/images/dancingwoman.gif", + "/web_workshop/resources/images/devil.gif", + "/web_workshop/resources/images/disco_ball.gif", + "/web_workshop/resources/images/divider_blissey.gif", + "/web_workshop/resources/images/divider_blue_flowers.gif", + "/web_workshop/resources/images/divider_blue_flowers2.gif", + "/web_workshop/resources/images/divider_rainbow.gif", + "/web_workshop/resources/images/divider_rainbow_dots.gif", + "/web_workshop/resources/images/divider_red_flowers.png", + "/web_workshop/resources/images/divider_wildflowers.gif", + "/web_workshop/resources/images/divider_yellow_flowers.gif", + "/web_workshop/resources/images/dollhouse.gif", + "/web_workshop/resources/images/doraemon.gif", + "/web_workshop/resources/images/doraemon_stamp.gif", + "/web_workshop/resources/images/email.gif", + "/web_workshop/resources/images/email2.gif", + "/web_workshop/resources/images/flowers.gif", + "/web_workshop/resources/images/frog_car.gif", + "/web_workshop/resources/images/furby1.gif", + "/web_workshop/resources/images/furby2.gif", + "/web_workshop/resources/images/furby3.gif", + "/web_workshop/resources/images/furby4.gif", + "/web_workshop/resources/images/furby5.gif", + "/web_workshop/resources/images/furby6.gif", + "/web_workshop/resources/images/furby7.gif", + "/web_workshop/resources/images/furby8.gif", + "/web_workshop/resources/images/furby9.gif", + "/web_workshop/resources/images/furby_banner.gif", + "/web_workshop/resources/images/furby_clouds.gif", + "/web_workshop/resources/images/furby_field.jpg", + "/web_workshop/resources/images/furby_pink.png", + "/web_workshop/resources/images/furby_shocked.gif", + "/web_workshop/resources/images/furby_weird.gif", + "/web_workshop/resources/images/furbyland.gif", + "/web_workshop/resources/images/gaia.gif", + "/web_workshop/resources/images/garf.gif", + "/web_workshop/resources/images/gc_icon.gif", + "/web_workshop/resources/images/hacker_skull.gif", + "/web_workshop/resources/images/hacking.gif", + "/web_workshop/resources/images/hamster.gif", + "/web_workshop/resources/images/hamtaro.gif", + "/web_workshop/resources/images/hellokitty.gif", + "/web_workshop/resources/images/hint.gif", + "/web_workshop/resources/images/home.gif", + "/web_workshop/resources/images/koosh.gif", + "/web_workshop/resources/images/lil_buddha.gif", + "/web_workshop/resources/images/lil_demon.gif", + "/web_workshop/resources/images/linux_power.gif", + "/web_workshop/resources/images/mac.png", + "/web_workshop/resources/images/mailbox.gif", + "/web_workshop/resources/images/mario64.gif", + "/web_workshop/resources/images/modchips.gif", + "/web_workshop/resources/images/neopet.gif", + "/web_workshop/resources/images/ness.gif", + "/web_workshop/resources/images/new_bunny.gif", + "/web_workshop/resources/images/no_java.gif", + "/web_workshop/resources/images/orbit.gif", + "/web_workshop/resources/images/pika_construction.gif", + "/web_workshop/resources/images/pink_clock.gif", + "/web_workshop/resources/images/pochacco.gif", + "/web_workshop/resources/images/providence.gif", + "/web_workshop/resources/images/ratrace.gif", + "/web_workshop/resources/images/ring.gif", + "/web_workshop/resources/images/sailor_moon.gif", + "/web_workshop/resources/images/samus.gif", + "/web_workshop/resources/images/scachis.gif", + "/web_workshop/resources/images/scared_mouse.gif", + "/web_workshop/resources/images/smiley_bar.gif", + "/web_workshop/resources/images/smiley_dancing.gif", + "/web_workshop/resources/images/smiling_pizza.gif", + "/web_workshop/resources/images/spacedog.gif", + "/web_workshop/resources/images/spinninbrain.gif", + "/web_workshop/resources/images/splat.png", + "/web_workshop/resources/images/sun.png", + "/web_workshop/resources/images/surfin.gif", + "/web_workshop/resources/images/tamas.gif", + "/web_workshop/resources/images/telephone.gif", + "/web_workshop/resources/images/underconstruction.gif", + "/web_workshop/resources/images/welcome.gif", + "/web_workshop/resources/images/windows.gif", + "/web_workshop/resources/images/wishingwell.gif", + "/web_workshop/resources/images/wizard.gif", + "/web_workshop/resources/images/ww_9volt.gif", + "/web_workshop/resources/images/ww_ana.gif", + "/web_workshop/resources/images/ww_crygor.gif", + "/web_workshop/resources/images/ww_dribble.gif", + "/web_workshop/resources/images/ww_jimmy.gif", + "/web_workshop/resources/images/ww_kat.gif", + "/web_workshop/resources/images/ww_mona.gif", + "/web_workshop/resources/images/ww_orbulon.gif", + "/web_workshop/resources/images/ww_spitz.gif", + "/web_workshop/resources/images/ww_wario.gif", + "/web_workshop/resources/images/y2k.gif" + ], + "resources": [ + "/web_workshop/resources/css/styles.css", + "/web_workshop/resources/icons/construction.png", + "/web_workshop/resources/icons/construction_padded.png", + "/web_workshop/resources/images/.DS_Store", + "/web_workshop/resources/images/24.gif", + "/web_workshop/resources/images/bang.gif", + "/web_workshop/resources/images/bg_bricks.jpg", + "/web_workshop/resources/images/bg_checker.gif", + "/web_workshop/resources/images/bg_daisies.jpg", + "/web_workshop/resources/images/bg_desert.gif", + "/web_workshop/resources/images/bg_stars.gif", + "/web_workshop/resources/images/bg_trippy.jpg", + "/web_workshop/resources/images/bg_water.jpg", + "/web_workshop/resources/images/bigN.gif", + "/web_workshop/resources/images/birdbath.gif", + "/web_workshop/resources/images/bizzy_wizzy.gif", + "/web_workshop/resources/images/blahblah.gif", + "/web_workshop/resources/images/bonk.gif", + "/web_workshop/resources/images/bright_idea.gif", + "/web_workshop/resources/images/butterflies.gif", + "/web_workshop/resources/images/butterfly_blue.gif", + "/web_workshop/resources/images/butterfly_monarch.gif", + "/web_workshop/resources/images/cactus.gif", + "/web_workshop/resources/images/cat.gif", + "/web_workshop/resources/images/caterpillar.gif", + "/web_workshop/resources/images/cd.gif", + "/web_workshop/resources/images/christmas_garland.gif", + "/web_workshop/resources/images/christmas_holly.gif", + "/web_workshop/resources/images/christmas_wreath.gif", + "/web_workshop/resources/images/coffee.gif", + "/web_workshop/resources/images/coffee.png", + "/web_workshop/resources/images/computer.gif", + "/web_workshop/resources/images/construct.gif", + "/web_workshop/resources/images/cow.gif", + "/web_workshop/resources/images/daisies.gif", + "/web_workshop/resources/images/dancingman.gif", + "/web_workshop/resources/images/dancingwoman.gif", + "/web_workshop/resources/images/devil.gif", + "/web_workshop/resources/images/disco_ball.gif", + "/web_workshop/resources/images/divider_blissey.gif", + "/web_workshop/resources/images/divider_blue_flowers.gif", + "/web_workshop/resources/images/divider_blue_flowers2.gif", + "/web_workshop/resources/images/divider_rainbow.gif", + "/web_workshop/resources/images/divider_rainbow_dots.gif", + "/web_workshop/resources/images/divider_red_flowers.png", + "/web_workshop/resources/images/divider_wildflowers.gif", + "/web_workshop/resources/images/divider_yellow_flowers.gif", + "/web_workshop/resources/images/dollhouse.gif", + "/web_workshop/resources/images/doraemon.gif", + "/web_workshop/resources/images/doraemon_stamp.gif", + "/web_workshop/resources/images/email.gif", + "/web_workshop/resources/images/email2.gif", + "/web_workshop/resources/images/flowers.gif", + "/web_workshop/resources/images/frog_car.gif", + "/web_workshop/resources/images/furby1.gif", + "/web_workshop/resources/images/furby2.gif", + "/web_workshop/resources/images/furby3.gif", + "/web_workshop/resources/images/furby4.gif", + "/web_workshop/resources/images/furby5.gif", + "/web_workshop/resources/images/furby6.gif", + "/web_workshop/resources/images/furby7.gif", + "/web_workshop/resources/images/furby8.gif", + "/web_workshop/resources/images/furby9.gif", + "/web_workshop/resources/images/furby_banner.gif", + "/web_workshop/resources/images/furby_clouds.gif", + "/web_workshop/resources/images/furby_field.jpg", + "/web_workshop/resources/images/furby_pink.png", + "/web_workshop/resources/images/furby_shocked.gif", + "/web_workshop/resources/images/furby_weird.gif", + "/web_workshop/resources/images/furbyland.gif", + "/web_workshop/resources/images/gaia.gif", + "/web_workshop/resources/images/garf.gif", + "/web_workshop/resources/images/gc_icon.gif", + "/web_workshop/resources/images/hacker_skull.gif", + "/web_workshop/resources/images/hacking.gif", + "/web_workshop/resources/images/hamster.gif", + "/web_workshop/resources/images/hamtaro.gif", + "/web_workshop/resources/images/hellokitty.gif", + "/web_workshop/resources/images/hint.gif", + "/web_workshop/resources/images/home.gif", + "/web_workshop/resources/images/koosh.gif", + "/web_workshop/resources/images/lil_buddha.gif", + "/web_workshop/resources/images/lil_demon.gif", + "/web_workshop/resources/images/linux_power.gif", + "/web_workshop/resources/images/mac.png", + "/web_workshop/resources/images/mailbox.gif", + "/web_workshop/resources/images/mario64.gif", + "/web_workshop/resources/images/modchips.gif", + "/web_workshop/resources/images/neopet.gif", + "/web_workshop/resources/images/ness.gif", + "/web_workshop/resources/images/new_bunny.gif", + "/web_workshop/resources/images/no_java.gif", + "/web_workshop/resources/images/orbit.gif", + "/web_workshop/resources/images/pika_construction.gif", + "/web_workshop/resources/images/pink_clock.gif", + "/web_workshop/resources/images/pochacco.gif", + "/web_workshop/resources/images/providence.gif", + "/web_workshop/resources/images/ratrace.gif", + "/web_workshop/resources/images/ring.gif", + "/web_workshop/resources/images/sailor_moon.gif", + "/web_workshop/resources/images/samus.gif", + "/web_workshop/resources/images/scachis.gif", + "/web_workshop/resources/images/scared_mouse.gif", + "/web_workshop/resources/images/smiley_bar.gif", + "/web_workshop/resources/images/smiley_dancing.gif", + "/web_workshop/resources/images/smiling_pizza.gif", + "/web_workshop/resources/images/spacedog.gif", + "/web_workshop/resources/images/spinninbrain.gif", + "/web_workshop/resources/images/splat.png", + "/web_workshop/resources/images/sun.png", + "/web_workshop/resources/images/surfin.gif", + "/web_workshop/resources/images/tamas.gif", + "/web_workshop/resources/images/telephone.gif", + "/web_workshop/resources/images/underconstruction.gif", + "/web_workshop/resources/images/welcome.gif", + "/web_workshop/resources/images/windows.gif", + "/web_workshop/resources/images/wishingwell.gif", + "/web_workshop/resources/images/wizard.gif", + "/web_workshop/resources/images/ww_9volt.gif", + "/web_workshop/resources/images/ww_ana.gif", + "/web_workshop/resources/images/ww_crygor.gif", + "/web_workshop/resources/images/ww_dribble.gif", + "/web_workshop/resources/images/ww_jimmy.gif", + "/web_workshop/resources/images/ww_kat.gif", + "/web_workshop/resources/images/ww_mona.gif", + "/web_workshop/resources/images/ww_orbulon.gif", + "/web_workshop/resources/images/ww_spitz.gif", + "/web_workshop/resources/images/ww_wario.gif", + "/web_workshop/resources/images/y2k.gif", + "/web_workshop/resources/js/codemirror-bundle.js", + "/web_workshop/resources/js/main.js", + "/web_workshop/resources/readme_images/picnic.png", + "/web_workshop/resources/readme_images/stock_images.png", + "/web_workshop/resources/resource-manifest.json", + "/web_workshop/resources/scripts/generate_resource_manifest.py", + "/web_workshop/resources/ui/fullscreen.svg", + "/web_workshop/resources/vendor/build.mjs", + "/web_workshop/resources/vendor/node_modules/.bin/esbuild", + "/web_workshop/resources/vendor/node_modules/.package-lock.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/language/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/search/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/state/package.json", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/.github/workflows/dispatch.yml", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/LICENSE", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/README.md", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@codemirror/view/package.json", + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/README.md", + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/bin/esbuild", + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/package.json", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/README.md", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/common/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/common/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/common/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/css/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@lezer/css/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/css/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/css/rollup.config.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/css.grammar", + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/highlight.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.terms.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/tokens.js", + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/declarations.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/selector.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/statements.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/test-css.js", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/html/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@lezer/html/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/html/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/html/rollup.config.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/.tern-port", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/content.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/highlight.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/html.grammar", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.terms.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/tokens.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/mixed.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/tags.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-html.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-incremental.js", + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/vue.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/CHANGELOG.md", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/package.json", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/rollup.config.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/highlight.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/javascript.grammar", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.terms.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/tokens.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/decorator.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/expression.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/jsx.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/semicolon.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/statement.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/test-javascript.js", + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/typescript.txt", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/LICENSE", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/README.md", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.js", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.js", + "/web_workshop/resources/vendor/node_modules/@lezer/lr/package.json", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/LICENSE", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/README.md", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/package.json", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/rollup.config.js", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.d.ts", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.js", + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/test/test-cluster.js", + "/web_workshop/resources/vendor/node_modules/crelt/LICENSE", + "/web_workshop/resources/vendor/node_modules/crelt/README.md", + "/web_workshop/resources/vendor/node_modules/crelt/dist/index.cjs", + "/web_workshop/resources/vendor/node_modules/crelt/dist/index.d.cts", + "/web_workshop/resources/vendor/node_modules/crelt/index.d.ts", + "/web_workshop/resources/vendor/node_modules/crelt/index.js", + "/web_workshop/resources/vendor/node_modules/crelt/package.json", + "/web_workshop/resources/vendor/node_modules/crelt/rollup.config.js", + "/web_workshop/resources/vendor/node_modules/esbuild/LICENSE.md", + "/web_workshop/resources/vendor/node_modules/esbuild/README.md", + "/web_workshop/resources/vendor/node_modules/esbuild/bin/esbuild", + "/web_workshop/resources/vendor/node_modules/esbuild/install.js", + "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.d.ts", + "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.js", + "/web_workshop/resources/vendor/node_modules/esbuild/package.json", + "/web_workshop/resources/vendor/node_modules/style-mod/LICENSE", + "/web_workshop/resources/vendor/node_modules/style-mod/README.md", + "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.cjs", + "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.d.cts", + "/web_workshop/resources/vendor/node_modules/style-mod/package.json", + "/web_workshop/resources/vendor/node_modules/style-mod/src/README.md", + "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.d.ts", + "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.js", + "/web_workshop/resources/vendor/node_modules/style-mod/test/test-style-mod.js", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/.tern-port", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/LICENSE", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/README.md", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.cjs", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.cts", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.ts", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.js", + "/web_workshop/resources/vendor/node_modules/w3c-keyname/package.json", + "/web_workshop/resources/vendor/package-lock.json", + "/web_workshop/resources/vendor/package.json" + ] +} +\ No newline at end of file diff --git a/resources/scripts/generate_resource_manifest.py b/resources/scripts/generate_resource_manifest.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Generate resource manifest for PWA caching +Scans the images/ and resources/ directories and creates a JSON manifest file +""" + +import os +import json +from pathlib import Path + +def generate_resource_manifest(): + """Generate a JSON manifest of all resources in the images/ and resources/ directories""" + + # Define supported image extensions + image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'} + + # Get the repo root (two levels up from resources/scripts/) + repo_root = Path(__file__).parent.parent.parent + resources_dir = repo_root / 'resources' + images_dir = resources_dir / 'images' + + # Collect all image files + image_files = [] + if images_dir.exists(): + for file_path in images_dir.iterdir(): + if file_path.is_file() and file_path.suffix.lower() in image_extensions: + # Create relative path from web root (GitHub Pages subdirectory) + relative_path = f"/web_workshop/resources/images/{file_path.name}" + image_files.append(relative_path) + + # Collect ALL resource files (no extension filtering) + resource_files = [] + if resources_dir.exists(): + for file_path in resources_dir.rglob('*'): + if file_path.is_file(): + # Create relative path from web root (GitHub Pages subdirectory) + relative_path = f"/web_workshop/resources/{file_path.relative_to(resources_dir).as_posix()}" + resource_files.append(relative_path) + + # Sort for consistent output + image_files.sort() + resource_files.sort() + + # Create manifest object + manifest = { + "images": image_files, + "resources": resource_files, + "generated_at": "auto-generated by GitHub Actions" + } + + # Write manifest file + manifest_path = resources_dir / 'resource-manifest.json' + with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest, f, indent=2, sort_keys=True) + + print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:") + for img in image_files: + print(f" - {img}") + for res in resource_files: + print(f" - {res}") + + return manifest + +if __name__ == "__main__": + manifest = generate_resource_manifest() + print(f"\nManifest saved to: resources/resource-manifest.json") +\ No newline at end of file diff --git a/resources/fullscreen.svg b/resources/ui/fullscreen.svg diff --git a/resources/vendor/build.mjs b/resources/vendor/build.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +// Build script to bundle CodeMirror for offline use +// +// Usage: +// cd vendor +// npm install +// npm run build +// +// This will regenerate ../js/codemirror-bundle.js + +import * as esbuild from 'esbuild'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +await esbuild.build({ + stdin: { + contents: ` + export {EditorView, keymap, placeholder, lineNumbers, Decoration} from "@codemirror/view"; + export {EditorState, Compartment} from "@codemirror/state"; + export {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "@codemirror/commands"; + export {closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"; + export {html} from "@codemirror/lang-html"; + export {githubDark} from "@fsegurai/codemirror-theme-github-dark"; + export {indentUnit} from "@codemirror/language"; + export {search, searchKeymap, closeSearchPanel, openSearchPanel} from "@codemirror/search"; + `, + resolveDir: __dirname, + loader: 'js', + }, + bundle: true, + format: 'esm', + outfile: join(__dirname, '..', 'js', 'codemirror-bundle.js'), + minify: true, + target: ['es2020'], +}); + +console.log('✓ CodeMirror bundle created: ../js/codemirror-bundle.js'); diff --git a/vendor/package.json b/resources/vendor/package.json diff --git a/sw.js b/sw.js @@ -3,10 +3,10 @@ const urlsToCache = [ '/web_workshop/', '/web_workshop/index.html', '/web_workshop/manifest.json', - '/web_workshop/resource-manifest.json', - '/web_workshop/styles.css', - '/web_workshop/main.js', - '/web_workshop/codemirror-bundle.js' + '/web_workshop/resources/resource-manifest.json', + '/web_workshop/resources/css/styles.css', + '/web_workshop/resources/js/main.js', + '/web_workshop/resources/js/codemirror-bundle.js' ]; // Function to discover and cache all files in directories @@ -44,7 +44,7 @@ async function cacheDirectoryFiles(cache, directories) { // Function to cache all resources using the generated manifest async function cacheResources(cache) { try { - const response = await fetch('/web_workshop/resource-manifest.json'); + const response = await fetch('/web_workshop/resources/resource-manifest.json'); if (response.ok) { const manifest = await response.json(); console.log(`Caching ${manifest.images.length} images and ${manifest.resources.length} resources from manifest`); diff --git a/vendor/build.mjs b/vendor/build.mjs @@ -1,39 +0,0 @@ -#!/usr/bin/env node -// Build script to bundle CodeMirror for offline use -// -// Usage: -// cd vendor -// npm install -// npm run build -// -// This will regenerate ../codemirror-bundle.js - -import * as esbuild from 'esbuild'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -await esbuild.build({ - stdin: { - contents: ` - export {EditorView, keymap, placeholder, lineNumbers, Decoration} from "@codemirror/view"; - export {EditorState, Compartment} from "@codemirror/state"; - export {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "@codemirror/commands"; - export {closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"; - export {html} from "@codemirror/lang-html"; - export {githubDark} from "@fsegurai/codemirror-theme-github-dark"; - export {indentUnit} from "@codemirror/language"; - export {search, searchKeymap, closeSearchPanel, openSearchPanel} from "@codemirror/search"; - `, - resolveDir: __dirname, - loader: 'js', - }, - bundle: true, - format: 'esm', - outfile: join(__dirname, '..', 'codemirror-bundle.js'), - minify: true, - target: ['es2020'], -}); - -console.log('✓ CodeMirror bundle created: ../codemirror-bundle.js');