restructure

Hunter
Wednesday, March 18th, 2026 at 5:42 PM

.github/workflows/generate-manifest.yml+3-3
.gitignore+2-2
README.md+7-7
generate_resource_manifest.pydeleted
index.html+4-4
main.jsdeleted
manifest.json+2-2
pages/avocado_warehouse/index.html+2-2
pages/my_homepage/index.html+5-5
pages/pizza_heaven/index.html+4-4
resource-manifest.jsondeleted
styles.css -> resources/css/styles.css  
resources/construction.png -> resources/icons/construction.png  
resources/construction_padded.png -> resources/icons/construction_padded.png  
images/24.gif -> resources/images/24.gif  
images/bang.gif -> resources/images/bang.gif  
images/bg_bricks.jpg -> resources/images/bg_bricks.jpg  
images/bg_checker.gif -> resources/images/bg_checker.gif  
images/bg_daisies.jpg -> resources/images/bg_daisies.jpg  
images/bg_desert.gif -> resources/images/bg_desert.gif  
images/bg_stars.gif -> resources/images/bg_stars.gif  
images/bg_trippy.jpg -> resources/images/bg_trippy.jpg  
images/bg_water.jpg -> resources/images/bg_water.jpg  
images/bigN.gif -> resources/images/bigN.gif  
images/birdbath.gif -> resources/images/birdbath.gif  
images/bizzy_wizzy.gif -> resources/images/bizzy_wizzy.gif  
images/blahblah.gif -> resources/images/blahblah.gif  
images/bonk.gif -> resources/images/bonk.gif  
images/bright_idea.gif -> resources/images/bright_idea.gif  
images/butterflies.gif -> resources/images/butterflies.gif  
images/butterfly_blue.gif -> resources/images/butterfly_blue.gif  
images/butterfly_monarch.gif -> resources/images/butterfly_monarch.gif  
images/cactus.gif -> resources/images/cactus.gif  
images/cat.gif -> resources/images/cat.gif  
images/caterpillar.gif -> resources/images/caterpillar.gif  
images/cd.gif -> resources/images/cd.gif  
images/christmas_garland.gif -> resources/images/christmas_garland.gif  
images/christmas_holly.gif -> resources/images/christmas_holly.gif  
images/christmas_wreath.gif -> resources/images/christmas_wreath.gif  
images/coffee.gif -> resources/images/coffee.gif  
images/coffee.png -> resources/images/coffee.png  
images/computer.gif -> resources/images/computer.gif  
images/construct.gif -> resources/images/construct.gif  
images/cow.gif -> resources/images/cow.gif  
images/daisies.gif -> resources/images/daisies.gif  
images/dancingman.gif -> resources/images/dancingman.gif  
images/dancingwoman.gif -> resources/images/dancingwoman.gif  
images/devil.gif -> resources/images/devil.gif  
images/disco_ball.gif -> resources/images/disco_ball.gif  
images/divider_blissey.gif -> resources/images/divider_blissey.gif  
images/divider_blue_flowers.gif -> resources/images/divider_blue_flowers.gif  
images/divider_blue_flowers2.gif -> resources/images/divider_blue_flowers2.gif  
images/divider_rainbow.gif -> resources/images/divider_rainbow.gif  
images/divider_rainbow_dots.gif -> resources/images/divider_rainbow_dots.gif  
images/divider_red_flowers.png -> resources/images/divider_red_flowers.png  
images/divider_wildflowers.gif -> resources/images/divider_wildflowers.gif  
images/divider_yellow_flowers.gif -> resources/images/divider_yellow_flowers.gif  
images/dollhouse.gif -> resources/images/dollhouse.gif  
images/doraemon.gif -> resources/images/doraemon.gif  
images/doraemon_stamp.gif -> resources/images/doraemon_stamp.gif  
images/email.gif -> resources/images/email.gif  
images/email2.gif -> resources/images/email2.gif  
images/flowers.gif -> resources/images/flowers.gif  
images/frog_car.gif -> resources/images/frog_car.gif  
images/furby1.gif -> resources/images/furby1.gif  
images/furby2.gif -> resources/images/furby2.gif  
images/furby3.gif -> resources/images/furby3.gif  
images/furby4.gif -> resources/images/furby4.gif  
images/furby5.gif -> resources/images/furby5.gif  
images/furby6.gif -> resources/images/furby6.gif  
images/furby7.gif -> resources/images/furby7.gif  
images/furby8.gif -> resources/images/furby8.gif  
images/furby9.gif -> resources/images/furby9.gif  
images/furby_banner.gif -> resources/images/furby_banner.gif  
images/furby_clouds.gif -> resources/images/furby_clouds.gif  
images/furby_field.jpg -> resources/images/furby_field.jpg  
images/furby_pink.png -> resources/images/furby_pink.png  
images/furby_shocked.gif -> resources/images/furby_shocked.gif  
images/furby_weird.gif -> resources/images/furby_weird.gif  
images/furbyland.gif -> resources/images/furbyland.gif  
images/gaia.gif -> resources/images/gaia.gif  
images/garf.gif -> resources/images/garf.gif  
images/gc_icon.gif -> resources/images/gc_icon.gif  
images/hacker_skull.gif -> resources/images/hacker_skull.gif  
images/hacking.gif -> resources/images/hacking.gif  
images/hamster.gif -> resources/images/hamster.gif  
images/hamtaro.gif -> resources/images/hamtaro.gif  
images/hellokitty.gif -> resources/images/hellokitty.gif  
images/hint.gif -> resources/images/hint.gif  
images/home.gif -> resources/images/home.gif  
images/koosh.gif -> resources/images/koosh.gif  
images/lil_buddha.gif -> resources/images/lil_buddha.gif  
images/lil_demon.gif -> resources/images/lil_demon.gif  
images/linux_power.gif -> resources/images/linux_power.gif  
images/mac.png -> resources/images/mac.png  
images/mailbox.gif -> resources/images/mailbox.gif  
images/mario64.gif -> resources/images/mario64.gif  
images/modchips.gif -> resources/images/modchips.gif  
images/neopet.gif -> resources/images/neopet.gif  
images/ness.gif -> resources/images/ness.gif  
images/new_bunny.gif -> resources/images/new_bunny.gif  
images/no_java.gif -> resources/images/no_java.gif  
images/orbit.gif -> resources/images/orbit.gif  
images/pika_construction.gif -> resources/images/pika_construction.gif  
images/pink_clock.gif -> resources/images/pink_clock.gif  
images/pochacco.gif -> resources/images/pochacco.gif  
images/providence.gif -> resources/images/providence.gif  
images/ratrace.gif -> resources/images/ratrace.gif  
images/ring.gif -> resources/images/ring.gif  
images/sailor_moon.gif -> resources/images/sailor_moon.gif  
images/samus.gif -> resources/images/samus.gif  
images/scachis.gif -> resources/images/scachis.gif  
images/scared_mouse.gif -> resources/images/scared_mouse.gif  
images/smiley_bar.gif -> resources/images/smiley_bar.gif  
images/smiley_dancing.gif -> resources/images/smiley_dancing.gif  
images/smiling_pizza.gif -> resources/images/smiling_pizza.gif  
images/spacedog.gif -> resources/images/spacedog.gif  
images/spinninbrain.gif -> resources/images/spinninbrain.gif  
images/splat.png -> resources/images/splat.png  
images/sun.png -> resources/images/sun.png  
images/surfin.gif -> resources/images/surfin.gif  
images/tamas.gif -> resources/images/tamas.gif  
images/telephone.gif -> resources/images/telephone.gif  
images/underconstruction.gif -> resources/images/underconstruction.gif  
images/welcome.gif -> resources/images/welcome.gif  
images/windows.gif -> resources/images/windows.gif  
images/wishingwell.gif -> resources/images/wishingwell.gif  
images/wizard.gif -> resources/images/wizard.gif  
images/ww_9volt.gif -> resources/images/ww_9volt.gif  
images/ww_ana.gif -> resources/images/ww_ana.gif  
images/ww_crygor.gif -> resources/images/ww_crygor.gif  
images/ww_dribble.gif -> resources/images/ww_dribble.gif  
images/ww_jimmy.gif -> resources/images/ww_jimmy.gif  
images/ww_kat.gif -> resources/images/ww_kat.gif  
images/ww_mona.gif -> resources/images/ww_mona.gif  
images/ww_orbulon.gif -> resources/images/ww_orbulon.gif  
images/ww_spitz.gif -> resources/images/ww_spitz.gif  
images/ww_wario.gif -> resources/images/ww_wario.gif  
images/y2k.gif -> resources/images/y2k.gif  
codemirror-bundle.js -> resources/js/codemirror-bundle.js  
resources/js/main.jsnew
readme_images/picnic.png -> resources/readme_images/picnic.png  
readme_images/stock_images.png -> resources/readme_images/stock_images.png  
resources/resource-manifest.jsonnew
resources/scripts/generate_resource_manifest.pynew
resources/fullscreen.svg -> resources/ui/fullscreen.svg  
resources/vendor/build.mjsnew
vendor/package.json -> resources/vendor/package.json  
sw.js+5-5
vendor/build.mjsdeleted

.github/workflows/generate-manifest.yml
 3  on:
 4    push:
 5      branches: [ main ]
   -    paths: [ 'images/**', 'resources/**' ]  # Run when images or resources are added/changed
 6 +    paths: [ 'resources/**' ]  # Run when resources (including images) are added/changed
 7    workflow_dispatch:  # Allow manual trigger
 8  
 9  jobs:

24          python-version: '3.x'
25      
26      - name: Generate resource manifest
   -      run: python generate_resource_manifest.py
27 +      run: python resources/scripts/generate_resource_manifest.py
28      
29      - name: Check for changes
30        id: verify-changed-files

40        run: |
41          git config --local user.email "action@github.com"
42          git config --local user.name "GitHub Action"
   -        git add resource-manifest.json
43 +        git add resources/resource-manifest.json
44          git commit -m "Auto-update resource manifest [skip ci]"
45          git push
.gitignore
2  
3  jen-only/
4  
  -vendor/node_modules/
  -vendor/package-lock.json
5 +resources/vendor/node_modules/
6 +resources/vendor/package-lock.json
README.md
 2  
 3  Craft handmade websites in their natural habitat (your web browser).
 4  
   -<img src="images/pika_construction.gif">
 5 +<img src="resources/images/pika_construction.gif">
 6  
 7  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.
 8  
 9  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.
10  
11  <a href="https://hunterirving.github.io/web_workshop/">
   -<img src="readme_images/picnic.png" width="650"></a>
12 +<img src="resources/readme_images/picnic.png" width="650"></a>
13  <br><br>
14  
15  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  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.
33  
34  <a href="https://hunterirving.github.io/web_workshop/">
   -<img src="readme_images/stock_images.png" width="650"></a>
35 +<img src="resources/readme_images/stock_images.png" width="650"></a>
36  <br><br>
37  
38  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  - to toggle <b>line numbers</b>, press <kbd>F1</kbd> (disabled by default)
59  - to toggle <b>line wrapping</b>, press <kbd>F2</kbd> (enabled by default)
60  
   -<img src="images/hint.gif" width=60px>
61 +<img src="resources/images/hint.gif" width=60px>
62  <i>
63  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>
64  

67  - [CodeMirror 6](https://codemirror.net/)
68  - [GitHub Dark Theme for CodeMirror](https://github.com/fsegurai/codemirror-themes)
69  
   -<img src="images/bright_idea.gif">
70 +<img src="resources/images/bright_idea.gif">
71  
72  ## License
73  
74  GPLv3 (see <a href="LICENSE">LICENSE</a> for details).
75  
76  <br>
   -<img src="images/bigN.gif" width=110px>
77 +<img src="resources/images/bigN.gif" width=110px>
78  
79  ## Contributing
80  
81  Feel free to open issues or submit pull requests for improvements!
82  
   -<img src="images/gaia.gif" width=180px>
83 +<img src="resources/images/gaia.gif" width=180px>
generate_resource_manifest.py
  -#!/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")
index.html
 4  	<meta charset="UTF-8">
 5  	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
 6  	<title>Web Workshop</title>
   -	<link rel="icon" href="resources/construction.png">
 7 +	<link rel="icon" href="resources/icons/construction.png">
 8  	<link rel="manifest" href="manifest.json">
   -	<link rel="stylesheet" href="styles.css">
 9 +	<link rel="stylesheet" href="resources/css/styles.css">
10  	<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"
11 +		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"
12  		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};
13  	</script>
14  </head>

21  		<iframe id="preview"></iframe>
22  	</div>
23  
   -	<script type="module" src="main.js"></script>
24 +	<script type="module" src="resources/js/main.js"></script>
25  </body>
26  </html>
main.js
  -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);
  -			});
  -	});
  -}
manifest.json
 9    "orientation": "any",
10    "icons": [
11      {
   -      "src": "resources/construction_padded.png",
12 +      "src": "resources/icons/construction_padded.png",
13        "sizes": "200x200",
14        "type": "image/png"
15      },
16      {
   -      "src": "resources/construction_padded.png",
17 +      "src": "resources/icons/construction_padded.png",
18        "sizes": "192x192",
19        "type": "image/png"
20      }
pages/avocado_warehouse/index.html
36  		</ul>
37  		<center>
38  			<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;">
39 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/coffee.png" style="width:200px; height:200px;">
40  			<i><p>"What the? Eww..." - Jen Lu</p></i>
41  			<hr>
42  			<p>Avocados in the courtroom?<br>This vid made us L-O-L<br>(Laff Out Loud)</p>

44  			<br><br>
45  			<hr>
46  			<br>
   -			<img src="https://hunterirving.github.io/web_workshop/images/sun.png" style="background-color:LemonChiffon; max-width:400px">
47 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/sun.png" style="background-color:LemonChiffon; max-width:400px">
48  			<p>Anyway, thanks for checking out the site! Stay tuned for our family's prized avocado growing tips! 🌞</p>
49  		</center>
50  	</body>
pages/my_homepage/index.html
29  			Welcome to my World Wide Web Site 🌞
30  		</h1>
31  		<center>
   -			<img src="https://hunterirving.github.io/web_workshop/images/welcome.gif" width=400px>
32 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/welcome.gif" width=400px>
33  			<p>
34  				brace yourself. you are in for a wild ride!!
35  			</p>
   -			<a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/images/windows.gif" style="padding:10px;"></a>
36 +			<a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/resources/images/windows.gif" style="padding:10px;"></a>
37  			<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>
38 +			<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>
39  		</center>
40  		<h1 class="heading">
41  			Here are a few of my favorite images... 🍀
42  		</h1>
43  		<center>
   -			<img src="https://hunterirving.github.io/web_workshop/images/cd.gif" style="width:150px">
44 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/cd.gif" style="width:150px">
45  			<br><br>
   -			<img src="https://hunterirving.github.io/web_workshop/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;">
46 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;">
47  		</center>
48  	</body>
49  </html>
pages/pizza_heaven/index.html
46  				<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>
47  			</p>
48  				<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">
49 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif">
50 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/gaia.gif" width=200px>
51 +			<img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif">
52  			<div class="block">
53  				<marquee scrollamount=20>AYY, I'M MAKIN' PIZZA HERE !</marquee>
54  			</div>

92  		But you better not disparage our pizza pies again, capeeshe???
93  		</p>
94  		<br><br>
   -		<center><img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"></center><br><br>
95 +		<center><img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"></center><br><br>
96  	</body>
97  </html>
resource-manifest.json
  -{
  -  "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"
  -  ]
  -}
styles.css -> resources/css/styles.css
resources/construction.png -> resources/icons/construction.png
Binary files differ.
resources/construction_padded.png -> resources/icons/construction_padded.png
Binary files differ.
images/24.gif -> resources/images/24.gif
Binary files differ.
images/bang.gif -> resources/images/bang.gif
Binary files differ.
images/bg_bricks.jpg -> resources/images/bg_bricks.jpg
Binary files differ.
images/bg_checker.gif -> resources/images/bg_checker.gif
Binary files differ.
images/bg_daisies.jpg -> resources/images/bg_daisies.jpg
Binary files differ.
images/bg_desert.gif -> resources/images/bg_desert.gif
Binary files differ.
images/bg_stars.gif -> resources/images/bg_stars.gif
Binary files differ.
images/bg_trippy.jpg -> resources/images/bg_trippy.jpg
Binary files differ.
images/bg_water.jpg -> resources/images/bg_water.jpg
Binary files differ.
images/bigN.gif -> resources/images/bigN.gif
Binary files differ.
images/birdbath.gif -> resources/images/birdbath.gif
Binary files differ.
images/bizzy_wizzy.gif -> resources/images/bizzy_wizzy.gif
Binary files differ.
images/blahblah.gif -> resources/images/blahblah.gif
Binary files differ.
images/bonk.gif -> resources/images/bonk.gif
Binary files differ.
images/bright_idea.gif -> resources/images/bright_idea.gif
Binary files differ.
images/butterflies.gif -> resources/images/butterflies.gif
Binary files differ.
images/butterfly_blue.gif -> resources/images/butterfly_blue.gif
Binary files differ.
images/butterfly_monarch.gif -> resources/images/butterfly_monarch.gif
Binary files differ.
images/cactus.gif -> resources/images/cactus.gif
Binary files differ.
images/cat.gif -> resources/images/cat.gif
Binary files differ.
images/caterpillar.gif -> resources/images/caterpillar.gif
Binary files differ.
images/cd.gif -> resources/images/cd.gif
Binary files differ.
images/christmas_garland.gif -> resources/images/christmas_garland.gif
Binary files differ.
images/christmas_holly.gif -> resources/images/christmas_holly.gif
Binary files differ.
images/christmas_wreath.gif -> resources/images/christmas_wreath.gif
Binary files differ.
images/coffee.gif -> resources/images/coffee.gif
Binary files differ.
images/coffee.png -> resources/images/coffee.png
Binary files differ.
images/computer.gif -> resources/images/computer.gif
Binary files differ.
images/construct.gif -> resources/images/construct.gif
Binary files differ.
images/cow.gif -> resources/images/cow.gif
Binary files differ.
images/daisies.gif -> resources/images/daisies.gif
Binary files differ.
images/dancingman.gif -> resources/images/dancingman.gif
Binary files differ.
images/dancingwoman.gif -> resources/images/dancingwoman.gif
Binary files differ.
images/devil.gif -> resources/images/devil.gif
Binary files differ.
images/disco_ball.gif -> resources/images/disco_ball.gif
Binary files differ.
images/divider_blissey.gif -> resources/images/divider_blissey.gif
Binary files differ.
images/divider_blue_flowers.gif -> resources/images/divider_blue_flowers.gif
Binary files differ.
images/divider_blue_flowers2.gif -> resources/images/divider_blue_flowers2.gif
Binary files differ.
images/divider_rainbow.gif -> resources/images/divider_rainbow.gif
Binary files differ.
images/divider_rainbow_dots.gif -> resources/images/divider_rainbow_dots.gif
Binary files differ.
images/divider_red_flowers.png -> resources/images/divider_red_flowers.png
Binary files differ.
images/divider_wildflowers.gif -> resources/images/divider_wildflowers.gif
Binary files differ.
images/divider_yellow_flowers.gif -> resources/images/divider_yellow_flowers.gif
Binary files differ.
images/dollhouse.gif -> resources/images/dollhouse.gif
Binary files differ.
images/doraemon.gif -> resources/images/doraemon.gif
Binary files differ.
images/doraemon_stamp.gif -> resources/images/doraemon_stamp.gif
Binary files differ.
images/email.gif -> resources/images/email.gif
Binary files differ.
images/email2.gif -> resources/images/email2.gif
Binary files differ.
images/flowers.gif -> resources/images/flowers.gif
Binary files differ.
images/frog_car.gif -> resources/images/frog_car.gif
Binary files differ.
images/furby1.gif -> resources/images/furby1.gif
Binary files differ.
images/furby2.gif -> resources/images/furby2.gif
Binary files differ.
images/furby3.gif -> resources/images/furby3.gif
Binary files differ.
images/furby4.gif -> resources/images/furby4.gif
Binary files differ.
images/furby5.gif -> resources/images/furby5.gif
Binary files differ.
images/furby6.gif -> resources/images/furby6.gif
Binary files differ.
images/furby7.gif -> resources/images/furby7.gif
Binary files differ.
images/furby8.gif -> resources/images/furby8.gif
Binary files differ.
images/furby9.gif -> resources/images/furby9.gif
Binary files differ.
images/furby_banner.gif -> resources/images/furby_banner.gif
Binary files differ.
images/furby_clouds.gif -> resources/images/furby_clouds.gif
Binary files differ.
images/furby_field.jpg -> resources/images/furby_field.jpg
Binary files differ.
images/furby_pink.png -> resources/images/furby_pink.png
Binary files differ.
images/furby_shocked.gif -> resources/images/furby_shocked.gif
Binary files differ.
images/furby_weird.gif -> resources/images/furby_weird.gif
Binary files differ.
images/furbyland.gif -> resources/images/furbyland.gif
Binary files differ.
images/gaia.gif -> resources/images/gaia.gif
Binary files differ.
images/garf.gif -> resources/images/garf.gif
Binary files differ.
images/gc_icon.gif -> resources/images/gc_icon.gif
Binary files differ.
images/hacker_skull.gif -> resources/images/hacker_skull.gif
Binary files differ.
images/hacking.gif -> resources/images/hacking.gif
Binary files differ.
images/hamster.gif -> resources/images/hamster.gif
Binary files differ.
images/hamtaro.gif -> resources/images/hamtaro.gif
Binary files differ.
images/hellokitty.gif -> resources/images/hellokitty.gif
Binary files differ.
images/hint.gif -> resources/images/hint.gif
Binary files differ.
images/home.gif -> resources/images/home.gif
Binary files differ.
images/koosh.gif -> resources/images/koosh.gif
Binary files differ.
images/lil_buddha.gif -> resources/images/lil_buddha.gif
Binary files differ.
images/lil_demon.gif -> resources/images/lil_demon.gif
Binary files differ.
images/linux_power.gif -> resources/images/linux_power.gif
Binary files differ.
images/mac.png -> resources/images/mac.png
Binary files differ.
images/mailbox.gif -> resources/images/mailbox.gif
Binary files differ.
images/mario64.gif -> resources/images/mario64.gif
Binary files differ.
images/modchips.gif -> resources/images/modchips.gif
Binary files differ.
images/neopet.gif -> resources/images/neopet.gif
Binary files differ.
images/ness.gif -> resources/images/ness.gif
Binary files differ.
images/new_bunny.gif -> resources/images/new_bunny.gif
Binary files differ.
images/no_java.gif -> resources/images/no_java.gif
Binary files differ.
images/orbit.gif -> resources/images/orbit.gif
Binary files differ.
images/pika_construction.gif -> resources/images/pika_construction.gif
Binary files differ.
images/pink_clock.gif -> resources/images/pink_clock.gif
Binary files differ.
images/pochacco.gif -> resources/images/pochacco.gif
Binary files differ.
images/providence.gif -> resources/images/providence.gif
Binary files differ.
images/ratrace.gif -> resources/images/ratrace.gif
Binary files differ.
images/ring.gif -> resources/images/ring.gif
Binary files differ.
images/sailor_moon.gif -> resources/images/sailor_moon.gif
Binary files differ.
images/samus.gif -> resources/images/samus.gif
Binary files differ.
images/scachis.gif -> resources/images/scachis.gif
Binary files differ.
images/scared_mouse.gif -> resources/images/scared_mouse.gif
Binary files differ.
images/smiley_bar.gif -> resources/images/smiley_bar.gif
Binary files differ.
images/smiley_dancing.gif -> resources/images/smiley_dancing.gif
Binary files differ.
images/smiling_pizza.gif -> resources/images/smiling_pizza.gif
Binary files differ.
images/spacedog.gif -> resources/images/spacedog.gif
Binary files differ.
images/spinninbrain.gif -> resources/images/spinninbrain.gif
Binary files differ.
images/splat.png -> resources/images/splat.png
Binary files differ.
images/sun.png -> resources/images/sun.png
Binary files differ.
images/surfin.gif -> resources/images/surfin.gif
Binary files differ.
images/tamas.gif -> resources/images/tamas.gif
Binary files differ.
images/telephone.gif -> resources/images/telephone.gif
Binary files differ.
images/underconstruction.gif -> resources/images/underconstruction.gif
Binary files differ.
images/welcome.gif -> resources/images/welcome.gif
Binary files differ.
images/windows.gif -> resources/images/windows.gif
Binary files differ.
images/wishingwell.gif -> resources/images/wishingwell.gif
Binary files differ.
images/wizard.gif -> resources/images/wizard.gif
Binary files differ.
images/ww_9volt.gif -> resources/images/ww_9volt.gif
Binary files differ.
images/ww_ana.gif -> resources/images/ww_ana.gif
Binary files differ.
images/ww_crygor.gif -> resources/images/ww_crygor.gif
Binary files differ.
images/ww_dribble.gif -> resources/images/ww_dribble.gif
Binary files differ.
images/ww_jimmy.gif -> resources/images/ww_jimmy.gif
Binary files differ.
images/ww_kat.gif -> resources/images/ww_kat.gif
Binary files differ.
images/ww_mona.gif -> resources/images/ww_mona.gif
Binary files differ.
images/ww_orbulon.gif -> resources/images/ww_orbulon.gif
Binary files differ.
images/ww_spitz.gif -> resources/images/ww_spitz.gif
Binary files differ.
images/ww_wario.gif -> resources/images/ww_wario.gif
Binary files differ.
images/y2k.gif -> resources/images/y2k.gif
Binary files differ.
codemirror-bundle.js -> resources/js/codemirror-bundle.js
resources/js/main.js
  1 +let updateTimer;
  2 +let editorView;
  3 +const preview = document.getElementById('preview');
  4 +
  5 +// Stock images loaded from manifest - can be referenced by bare filename
  6 +let stockImages = new Map(); // lowercase -> original filename
  7 +
  8 +// Load stock image list from manifest
  9 +const stockImagesReady = fetch('resources/resource-manifest.json')
 10 +	.then(r => r.json())
 11 +	.then(manifest => {
 12 +		manifest.images.forEach(path => {
 13 +			const filename = path.split('/').pop();
 14 +			stockImages.set(filename.toLowerCase(), filename);
 15 +		});
 16 +	})
 17 +	.catch(() => console.log('Could not load resource manifest'));
 18 +
 19 +// Rewrite bare image filenames to use resources/images/ prefix (case-insensitive, uses correct case)
 20 +function rewriteBareImageSrcs(html) {
 21 +	// Rewrite <img src="filename.ext">
 22 +	html = html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi,
 23 +		(match, before, filename, ext, after) => {
 24 +			const original = stockImages.get(filename.toLowerCase());
 25 +			if (original) {
 26 +				return before + 'resources/images/' + original + after;
 27 +			}
 28 +			return match;
 29 +		});
 30 +	// Rewrite url(filename.ext) in CSS
 31 +	html = html.replace(/(url\(\s*["']?)([^"')/:]+\.(gif|png|jpg|jpeg|svg|webp))(["']?\s*\))/gi,
 32 +		(match, before, filename, ext, after) => {
 33 +			const original = stockImages.get(filename.toLowerCase());
 34 +			if (original) {
 35 +				return before + 'resources/images/' + original + after;
 36 +			}
 37 +			return match;
 38 +		});
 39 +	return html;
 40 +}
 41 +
 42 +// Expand <img src="?"> tags into gallery table
 43 +function expandImagesTag(html) {
 44 +	return html.replace(/<img\s+src\s*=\s*["']?\?["']?\s*\/?>/gi, () => {
 45 +		const images = Array.from(stockImages.values()).sort();
 46 +		if (images.length === 0) return '<p>No images available</p>';
 47 +		const rows = images.map(filename =>
 48 +			`<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>`
 49 +		).join('');
 50 +		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>`;
 51 +	});
 52 +}
 53 +const editorPane = document.querySelector('.editor-pane');
 54 +const previewPane = document.querySelector('.preview-pane');
 55 +const storageKey = 'html-lab-content';
 56 +let isFullscreen = false;
 57 +let showLineNumbers = false;
 58 +let enableLineWrapping = false;
 59 +let lineNumbersCompartment;
 60 +let lineWrappingCompartment;
 61 +
 62 +function toggleFullscreen() {
 63 +	isFullscreen = !isFullscreen;
 64 +
 65 +	if (isFullscreen) {
 66 +		previewPane.classList.add('fullscreen');
 67 +		editorPane.classList.add('hidden');
 68 +		// Set iframe height to actual visible height (fixes iOS Safari toolbar issue)
 69 +		preview.style.height = window.innerHeight + 'px';
 70 +	} else {
 71 +		previewPane.classList.remove('fullscreen');
 72 +		editorPane.classList.remove('hidden');
 73 +		preview.style.height = '';
 74 +	}
 75 +
 76 +	// Update button title in iframe
 77 +	try {
 78 +		const toggleButton = preview.contentDocument.getElementById('fullscreenToggle');
 79 +		if (toggleButton) {
 80 +			toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen';
 81 +		}
 82 +	} catch (e) {
 83 +		// Ignore cross-origin errors
 84 +	}
 85 +}
 86 +
 87 +
 88 +// Listen for messages from iframe
 89 +window.addEventListener('message', function(event) {
 90 +	if (event.data === 'toggleFullscreen') {
 91 +		toggleFullscreen();
 92 +	}
 93 +});
 94 +
 95 +function extractTitleAndFavicon(htmlCode) {
 96 +	const parser = new DOMParser();
 97 +	const doc = parser.parseFromString(htmlCode, 'text/html');
 98 +
 99 +	// Extract title
100 +	const titleElement = doc.querySelector('title');
101 +	const title = titleElement ? titleElement.textContent.trim() : null;
102 +
103 +	// Extract favicon
104 +	const faviconSelectors = [
105 +		'link[rel="icon"]',
106 +		'link[rel="shortcut icon"]',
107 +		'link[rel="apple-touch-icon"]',
108 +		'link[rel="mask-icon"]'
109 +	];
110 +
111 +	let favicon = null;
112 +	for (const selector of faviconSelectors) {
113 +		const faviconElement = doc.querySelector(selector);
114 +		if (faviconElement && faviconElement.href) {
115 +			favicon = faviconElement.href;
116 +			break;
117 +		}
118 +	}
119 +
120 +	return { title, favicon };
121 +}
122 +
123 +function updateMainPageTitleAndFavicon(title, favicon) {
124 +	// Update title
125 +	if (title) {
126 +		document.title = title;
127 +	} else {
128 +		document.title = 'Web Workshop';
129 +	}
130 +
131 +	// Update favicon
132 +	let faviconLink = document.querySelector('link[rel="icon"]');
133 +	if (!faviconLink) {
134 +		faviconLink = document.createElement('link');
135 +		faviconLink.rel = 'icon';
136 +		document.head.appendChild(faviconLink);
137 +	}
138 +
139 +	if (favicon) {
140 +		faviconLink.href = favicon;
141 +	} else {
142 +		faviconLink.href = 'resources/icons/construction.png';
143 +	}
144 +}
145 +
146 +function updatePreview() {
147 +	// Skip preview updates while mobile keyboard is open and editor is focused
148 +	// This prevents keyboard layer resets on Android
149 +	if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) {
150 +		return;
151 +	}
152 +
153 +	const code = editorView.state.doc.toString();
154 +
155 +	// Extract and update title and favicon from user's HTML
156 +	const { title, favicon } = extractTitleAndFavicon(code);
157 +	updateMainPageTitleAndFavicon(title, favicon);
158 +
159 +	// Store scroll position before updating
160 +	let scrollX = 0, scrollY = 0;
161 +	try {
162 +		if (preview.contentWindow?.scrollX !== undefined) {
163 +			scrollX = preview.contentWindow.scrollX;
164 +			scrollY = preview.contentWindow.scrollY;
165 +		}
166 +	} catch (e) {
167 +		// Ignore cross-origin errors
168 +	}
169 +
170 +	// Use srcdoc to create a completely fresh document context
171 +	// Expand <images> tag and rewrite bare stock image filenames
172 +	const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>';
173 +	preview.srcdoc = processedCode;
174 +
175 +	// Add our functionality after the iframe loads
176 +	const onLoad = () => {
177 +		try {
178 +			const doc = preview.contentDocument;
179 +			if (!doc) return;
180 +
181 +			// Add CSS for overscroll and button
182 +			const style = doc.createElement('style');
183 +			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; } }';
184 +			doc.head.appendChild(style);
185 +
186 +			// Create fullscreen button
187 +			const existingButton = doc.getElementById('fullscreenToggle');
188 +			if (existingButton) existingButton.remove();
189 +
190 +			const button = doc.createElement('button');
191 +			button.id = 'fullscreenToggle';
192 +			button.className = 'iframe-fullscreen-toggle';
193 +			button.title = 'Toggle fullscreen';
194 +
195 +			// Use inline SVG to avoid being affected by user's img styles
196 +			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>';
197 +
198 +			button.addEventListener('click', function() {
199 +				parent.postMessage('toggleFullscreen', '*');
200 +			});
201 +
202 +			if (doc.body) {
203 +				doc.body.appendChild(button);
204 +			}
205 +
206 +			// Add click handlers for stock image table rows
207 +			const stockImageRows = doc.querySelectorAll('.stock-image-row');
208 +			stockImageRows.forEach(row => {
209 +				row.addEventListener('click', () => {
210 +					const filename = row.getAttribute('data-filename');
211 +					if (!filename) return;
212 +
213 +					// Find and replace <img src="?"> in the editor
214 +					const editorContent = editorView.state.doc.toString();
215 +					const imgPattern = /<img\s+src\s*=\s*["']?\?["']?\s*\/?>/i;
216 +					const match = editorContent.match(imgPattern);
217 +
218 +					if (match) {
219 +						const start = editorContent.indexOf(match[0]);
220 +						const end = start + match[0].length;
221 +						const replacement = `<img src="${filename}">`;
222 +
223 +						editorView.dispatch({
224 +							changes: { from: start, to: end, insert: replacement }
225 +						});
226 +						saveToStorage();
227 +						updatePreview();
228 +					}
229 +				});
230 +			});
231 +
232 +			// Restore scroll position
233 +			setTimeout(() => {
234 +				try {
235 +					preview.contentWindow?.scrollTo(scrollX, scrollY);
236 +				} catch (e) {
237 +					// Ignore cross-origin errors
238 +				}
239 +			}, 10);
240 +		} catch (e) {
241 +			// Ignore cross-origin errors
242 +		}
243 +
244 +		preview.removeEventListener('load', onLoad);
245 +	};
246 +	preview.addEventListener('load', onLoad);
247 +}
248 +
249 +function saveToStorage() {
250 +	try {
251 +		localStorage.setItem(storageKey, editorView.state.doc.toString());
252 +	} catch (e) {
253 +		console.warn('Could not save to localStorage:', e);
254 +	}
255 +}
256 +
257 +function loadFromStorage() {
258 +	try {
259 +		return localStorage.getItem(storageKey) || '';
260 +	} catch (e) {
261 +		console.warn('Could not load from localStorage:', e);
262 +		return '';
263 +	}
264 +}
265 +
266 +function loadEditorSettings() {
267 +	try {
268 +		showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true';
269 +		enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false';
270 +	} catch (e) {
271 +		console.warn('Could not load editor settings from localStorage:', e);
272 +		showLineNumbers = false;
273 +		enableLineWrapping = true;
274 +	}
275 +}
276 +
277 +function saveEditorSetting(key, value) {
278 +	try {
279 +		localStorage.setItem(key, value.toString());
280 +	} catch (e) {
281 +		console.warn('Could not save editor setting to localStorage:', e);
282 +	}
283 +}
284 +
285 +function toggleLineNumbers() {
286 +	const {lineNumbers} = window.CodeMirror;
287 +	showLineNumbers = !showLineNumbers;
288 +	saveEditorSetting('editor-line-numbers', showLineNumbers);
289 +
290 +	editorView.dispatch({
291 +		effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : [])
292 +	});
293 +}
294 +
295 +function createLineWrappingExtension() {
296 +	const {EditorView, Decoration} = window.CodeMirror;
297 +
298 +	return [
299 +		EditorView.lineWrapping,
300 +		EditorView.decorations.of((view) => {
301 +			const decorations = [];
302 +
303 +			for (let {from, to} of view.visibleRanges) {
304 +				for (let pos = from; pos <= to;) {
305 +					const line = view.state.doc.lineAt(pos);
306 +					const lineText = line.text;
307 +
308 +					// Calculate indentation level (count leading whitespace)
309 +					let indentChars = 0;
310 +					for (let i = 0; i < lineText.length; i++) {
311 +						if (lineText[i] === '\t') {
312 +							indentChars += 2; // Convert tab to 2 spaces for calculation
313 +						} else if (lineText[i] === ' ') {
314 +							indentChars += 1;
315 +						} else {
316 +							break;
317 +						}
318 +					}
319 +
320 +					// Apply hanging indent if line has indentation
321 +					if (indentChars > 0) {
322 +						const indentDecoration = Decoration.line({
323 +							attributes: {
324 +								style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);`
325 +							}
326 +						});
327 +						decorations.push(indentDecoration.range(line.from));
328 +					}
329 +
330 +					pos = line.to + 1;
331 +				}
332 +			}
333 +
334 +			return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none;
335 +		}),
336 +	];
337 +}
338 +
339 +function toggleLineWrapping() {
340 +	enableLineWrapping = !enableLineWrapping;
341 +	saveEditorSetting('editor-line-wrapping', enableLineWrapping);
342 +
343 +	const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
344 +
345 +	editorView.dispatch({
346 +		effects: lineWrappingCompartment.reconfigure(lineWrappingExtension)
347 +	});
348 +}
349 +
350 +// File operations
351 +window.saveFile = function() {
352 +	const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' });
353 +	const url = URL.createObjectURL(blob);
354 +	const a = document.createElement('a');
355 +	a.href = url;
356 +	a.download = 'index.html';
357 +	a.click();
358 +	URL.revokeObjectURL(url);
359 +};
360 +
361 +window.loadFile = function() {
362 +	const input = document.createElement('input');
363 +	input.type = 'file';
364 +	input.accept = '.html,.htm';
365 +	input.onchange = function(event) {
366 +		const file = event.target.files[0];
367 +		if (!file) return;
368 +
369 +		const reader = new FileReader();
370 +		reader.onload = function(e) {
371 +			editorView.dispatch({
372 +				changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result }
373 +			});
374 +			saveToStorage();
375 +			updatePreview();
376 +		};
377 +		reader.readAsText(file);
378 +	};
379 +	input.click();
380 +};
381 +
382 +// Wait for CodeMirror to be available
383 +function initializeCodeMirror() {
384 +	if (!window.CodeMirror) {
385 +		setTimeout(initializeCodeMirror, 100);
386 +		return;
387 +	}
388 +
389 +	const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, history, closeBrackets, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror;
390 +
391 +	// Custom phrase overrides for CodeMirror UI (search panel)
392 +	const customPhrases = EditorState.phrases.of({
393 +		"Find": "Find..."
394 +	});
395 +
396 +	// Load saved content and editor settings
397 +	const savedContent = loadFromStorage();
398 +	loadEditorSettings();
399 +
400 +	// Create compartments for dynamic extensions
401 +	lineNumbersCompartment = new Compartment();
402 +	lineWrappingCompartment = new Compartment();
403 +
404 +	const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
405 +
406 +	// Create CodeMirror editor
407 +	editorView = new EditorView({
408 +		state: EditorState.create({
409 +			doc: savedContent,
410 +			extensions: [
411 +				customPhrases,
412 +				history(),
413 +				search(),
414 +				closeBrackets(),
415 +				keymap.of([
416 +					{key: "Mod-z", run: undo},
417 +					{key: "Mod-y", run: redo},
418 +					{key: "Mod-Shift-z", run: redo},
419 +					{key: "Mod-o", run: () => { window.loadFile(); return true; }},
420 +					{key: "Mod-s", run: () => { window.saveFile(); return true; }},
421 +					{key: "F1", run: () => { toggleLineNumbers(); return true; }},
422 +					{key: "F2", run: () => { toggleLineWrapping(); return true; }},
423 +					indentWithTab,
424 +					...searchKeymap.filter(binding => binding.key !== "Mod-f"),
425 +					...defaultKeymap
426 +				]),
427 +				html(),
428 +				// Auto-close <style> and <script> tags (not handled by default html() extension)
429 +				// Also expand <!> into HTML boilerplate
430 +				EditorView.inputHandler.of((view, from, to, text) => {
431 +					if (text !== '>') return false;
432 +					const before = view.state.doc.sliceString(Math.max(0, from - 20), from);
433 +					// Check for <!> boilerplate trigger
434 +					if (before.endsWith('<!')) {
435 +						const boilerplate = `<!DOCTYPE html>
436 +<html>
437 +\t<head>
438 +\t\t<style>
439 +\t\t\t
440 +\t\t</style>
441 +\t</head>
442 +\t<body>
443 +\t\t
444 +\t</body>
445 +</html>`;
446 +						const startPos = from - 2;
447 +						const cursorPos = startPos + boilerplate.indexOf('<body>') + 9;
448 +						view.dispatch({
449 +							changes: { from: startPos, to: from, insert: boilerplate },
450 +							selection: { anchor: cursorPos }
451 +						});
452 +						return true;
453 +					}
454 +					// Check for <style> or <script>
455 +					const match = before.match(/<(style|script)(\s[^>]*)?$/i);
456 +					if (!match) return false;
457 +					const tagName = match[1].toLowerCase();
458 +					const closingTag = `</${tagName}>`;
459 +					view.dispatch({
460 +						changes: { from, to, insert: '>' + closingTag },
461 +						selection: { anchor: from + 1 }
462 +					});
463 +					return true;
464 +				}),
465 +				githubDark,
466 +				indentUnit.of("\t"),
467 +				placeholder("Build something with HTML..."),
468 +				EditorView.updateListener.of((update) => {
469 +					if (update.docChanged) {
470 +						clearTimeout(updateTimer);
471 +						updateTimer = setTimeout(updatePreview, 600);
472 +						saveToStorage();
473 +					}
474 +				}),
475 +				// Disable text correction and autocomplete
476 +				EditorView.contentAttributes.of({
477 +					'autocomplete': 'off',
478 +					'autocorrect': 'off',
479 +					'autocapitalize': 'off',
480 +					'spellcheck': 'false'
481 +				}),
482 +				lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []),
483 +				lineWrappingCompartment.of(initialLineWrappingExtension)
484 +			]
485 +		}),
486 +		parent: document.getElementById('editor')
487 +	});
488 +
489 +	// Initial render (wait for stock images manifest to load first)
490 +	stockImagesReady.then(() => updatePreview());
491 +
492 +	// Track editor focus and handle keyboard dismissal
493 +	editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; });
494 +
495 +	editorView.contentDOM.addEventListener('blur', () => {
496 +		isEditorFocused = false;
497 +
498 +		// If we are on mobile and the keyboard mode is active,
499 +		// exit immediately. Do not wait for visualViewport resize.
500 +		if (isMobileDevice()) {
501 +			exitMobileKeyboardMode();
502 +		}
503 +	});
504 +
505 +	// Focus the editor
506 +	editorView.focus();
507 +
508 +	// Global keydown handler for Cmd+F toggle
509 +	document.addEventListener('keydown', function(e) {
510 +		if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
511 +			e.preventDefault();
512 +			closeSearchPanel(editorView) || openSearchPanel(editorView);
513 +		}
514 +	});
515 +
516 +	// Disable browser autocomplete on search panel inputs when they appear
517 +	const editorElement = document.getElementById('editor');
518 +	const searchInputObserver = new MutationObserver((mutations) => {
519 +		for (const mutation of mutations) {
520 +			for (const node of mutation.addedNodes) {
521 +				if (node.nodeType === Node.ELEMENT_NODE) {
522 +					const searchInputs = node.querySelectorAll?.('.cm-search input[name="search"], .cm-search input[name="replace"]');
523 +					searchInputs?.forEach(input => input.setAttribute('autocomplete', 'off'));
524 +				}
525 +			}
526 +		}
527 +	});
528 +	searchInputObserver.observe(editorElement, { childList: true, subtree: true });
529 +}
530 +
531 +// Mobile keyboard detection
532 +function isMobileDevice() {
533 +	return window.matchMedia("(pointer: coarse), (pointer: none)").matches;
534 +}
535 +
536 +let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
537 +let isEditorFocused = false;
538 +
539 +// Handle transition from fullscreen keyboard mode back to split view
540 +function exitMobileKeyboardMode() {
541 +	if (!document.body.classList.contains('mobile-keyboard-open')) return;
542 +
543 +	// Hide editor immediately to prevent visual stutter/jump
544 +	editorPane.style.opacity = '0';
545 +	document.body.classList.remove('mobile-keyboard-open');
546 +
547 +	// Wait for layout to settle, then restore scroll and opacity
548 +	requestAnimationFrame(() => requestAnimationFrame(() => {
549 +		if (editorView) {
550 +			// Scroll cursor into view in the new 50% layout
551 +			const pos = editorView.state.selection.main.head;
552 +			const lineBlock = editorView.lineBlockAt(pos);
553 +			const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2);
554 +			editorView.scrollDOM.scrollTop = Math.max(0, targetScroll);
555 +		}
556 +		editorPane.style.opacity = '';
557 +
558 +		// Update preview with any changes made while keyboard was open
559 +		updatePreview();
560 +	}));
561 +}
562 +
563 +function updateViewportVariables() {
564 +	const vv = window.visualViewport;
565 +	if (vv) {
566 +		document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`);
567 +		document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`);
568 +	}
569 +}
570 +
571 +function handleViewportChange() {
572 +	if (!isMobileDevice()) return;
573 +
574 +	const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
575 +	const heightDifference = initialViewportHeight - currentHeight;
576 +	const isKeyboardOpen = heightDifference > 150;
577 +
578 +	updateViewportVariables();
579 +
580 +	if (isKeyboardOpen && isEditorFocused) {
581 +		document.body.classList.add('mobile-keyboard-open');
582 +	} else if (!isKeyboardOpen) {
583 +		// Fallback for keyboard dismissal that doesn't trigger blur (e.g. Android)
584 +		exitMobileKeyboardMode();
585 +	}
586 +}
587 +
588 +if (window.visualViewport) {
589 +	window.visualViewport.addEventListener('resize', handleViewportChange);
590 +	window.visualViewport.addEventListener('scroll', updateViewportVariables);
591 +} else {
592 +	window.addEventListener('resize', handleViewportChange);
593 +}
594 +
595 +// Initialize when page loads
596 +initializeCodeMirror();
597 +
598 +// Register service worker
599 +if ('serviceWorker' in navigator) {
600 +	window.addEventListener('load', () => {
601 +		navigator.serviceWorker.register('./sw.js')
602 +			.then(registration => {
603 +				console.log('SW registered: ', registration);
604 +			})
605 +			.catch(registrationError => {
606 +				console.log('SW registration failed: ', registrationError);
607 +			});
608 +	});
609 +}
readme_images/picnic.png -> resources/readme_images/picnic.png
Binary files differ.
readme_images/stock_images.png -> resources/readme_images/stock_images.png
Binary files differ.
resources/resource-manifest.json
  1 +{
  2 +  "generated_at": "auto-generated by GitHub Actions",
  3 +  "images": [
  4 +    "/web_workshop/resources/images/24.gif",
  5 +    "/web_workshop/resources/images/bang.gif",
  6 +    "/web_workshop/resources/images/bg_bricks.jpg",
  7 +    "/web_workshop/resources/images/bg_checker.gif",
  8 +    "/web_workshop/resources/images/bg_daisies.jpg",
  9 +    "/web_workshop/resources/images/bg_desert.gif",
 10 +    "/web_workshop/resources/images/bg_stars.gif",
 11 +    "/web_workshop/resources/images/bg_trippy.jpg",
 12 +    "/web_workshop/resources/images/bg_water.jpg",
 13 +    "/web_workshop/resources/images/bigN.gif",
 14 +    "/web_workshop/resources/images/birdbath.gif",
 15 +    "/web_workshop/resources/images/bizzy_wizzy.gif",
 16 +    "/web_workshop/resources/images/blahblah.gif",
 17 +    "/web_workshop/resources/images/bonk.gif",
 18 +    "/web_workshop/resources/images/bright_idea.gif",
 19 +    "/web_workshop/resources/images/butterflies.gif",
 20 +    "/web_workshop/resources/images/butterfly_blue.gif",
 21 +    "/web_workshop/resources/images/butterfly_monarch.gif",
 22 +    "/web_workshop/resources/images/cactus.gif",
 23 +    "/web_workshop/resources/images/cat.gif",
 24 +    "/web_workshop/resources/images/caterpillar.gif",
 25 +    "/web_workshop/resources/images/cd.gif",
 26 +    "/web_workshop/resources/images/christmas_garland.gif",
 27 +    "/web_workshop/resources/images/christmas_holly.gif",
 28 +    "/web_workshop/resources/images/christmas_wreath.gif",
 29 +    "/web_workshop/resources/images/coffee.gif",
 30 +    "/web_workshop/resources/images/coffee.png",
 31 +    "/web_workshop/resources/images/computer.gif",
 32 +    "/web_workshop/resources/images/construct.gif",
 33 +    "/web_workshop/resources/images/cow.gif",
 34 +    "/web_workshop/resources/images/daisies.gif",
 35 +    "/web_workshop/resources/images/dancingman.gif",
 36 +    "/web_workshop/resources/images/dancingwoman.gif",
 37 +    "/web_workshop/resources/images/devil.gif",
 38 +    "/web_workshop/resources/images/disco_ball.gif",
 39 +    "/web_workshop/resources/images/divider_blissey.gif",
 40 +    "/web_workshop/resources/images/divider_blue_flowers.gif",
 41 +    "/web_workshop/resources/images/divider_blue_flowers2.gif",
 42 +    "/web_workshop/resources/images/divider_rainbow.gif",
 43 +    "/web_workshop/resources/images/divider_rainbow_dots.gif",
 44 +    "/web_workshop/resources/images/divider_red_flowers.png",
 45 +    "/web_workshop/resources/images/divider_wildflowers.gif",
 46 +    "/web_workshop/resources/images/divider_yellow_flowers.gif",
 47 +    "/web_workshop/resources/images/dollhouse.gif",
 48 +    "/web_workshop/resources/images/doraemon.gif",
 49 +    "/web_workshop/resources/images/doraemon_stamp.gif",
 50 +    "/web_workshop/resources/images/email.gif",
 51 +    "/web_workshop/resources/images/email2.gif",
 52 +    "/web_workshop/resources/images/flowers.gif",
 53 +    "/web_workshop/resources/images/frog_car.gif",
 54 +    "/web_workshop/resources/images/furby1.gif",
 55 +    "/web_workshop/resources/images/furby2.gif",
 56 +    "/web_workshop/resources/images/furby3.gif",
 57 +    "/web_workshop/resources/images/furby4.gif",
 58 +    "/web_workshop/resources/images/furby5.gif",
 59 +    "/web_workshop/resources/images/furby6.gif",
 60 +    "/web_workshop/resources/images/furby7.gif",
 61 +    "/web_workshop/resources/images/furby8.gif",
 62 +    "/web_workshop/resources/images/furby9.gif",
 63 +    "/web_workshop/resources/images/furby_banner.gif",
 64 +    "/web_workshop/resources/images/furby_clouds.gif",
 65 +    "/web_workshop/resources/images/furby_field.jpg",
 66 +    "/web_workshop/resources/images/furby_pink.png",
 67 +    "/web_workshop/resources/images/furby_shocked.gif",
 68 +    "/web_workshop/resources/images/furby_weird.gif",
 69 +    "/web_workshop/resources/images/furbyland.gif",
 70 +    "/web_workshop/resources/images/gaia.gif",
 71 +    "/web_workshop/resources/images/garf.gif",
 72 +    "/web_workshop/resources/images/gc_icon.gif",
 73 +    "/web_workshop/resources/images/hacker_skull.gif",
 74 +    "/web_workshop/resources/images/hacking.gif",
 75 +    "/web_workshop/resources/images/hamster.gif",
 76 +    "/web_workshop/resources/images/hamtaro.gif",
 77 +    "/web_workshop/resources/images/hellokitty.gif",
 78 +    "/web_workshop/resources/images/hint.gif",
 79 +    "/web_workshop/resources/images/home.gif",
 80 +    "/web_workshop/resources/images/koosh.gif",
 81 +    "/web_workshop/resources/images/lil_buddha.gif",
 82 +    "/web_workshop/resources/images/lil_demon.gif",
 83 +    "/web_workshop/resources/images/linux_power.gif",
 84 +    "/web_workshop/resources/images/mac.png",
 85 +    "/web_workshop/resources/images/mailbox.gif",
 86 +    "/web_workshop/resources/images/mario64.gif",
 87 +    "/web_workshop/resources/images/modchips.gif",
 88 +    "/web_workshop/resources/images/neopet.gif",
 89 +    "/web_workshop/resources/images/ness.gif",
 90 +    "/web_workshop/resources/images/new_bunny.gif",
 91 +    "/web_workshop/resources/images/no_java.gif",
 92 +    "/web_workshop/resources/images/orbit.gif",
 93 +    "/web_workshop/resources/images/pika_construction.gif",
 94 +    "/web_workshop/resources/images/pink_clock.gif",
 95 +    "/web_workshop/resources/images/pochacco.gif",
 96 +    "/web_workshop/resources/images/providence.gif",
 97 +    "/web_workshop/resources/images/ratrace.gif",
 98 +    "/web_workshop/resources/images/ring.gif",
 99 +    "/web_workshop/resources/images/sailor_moon.gif",
100 +    "/web_workshop/resources/images/samus.gif",
101 +    "/web_workshop/resources/images/scachis.gif",
102 +    "/web_workshop/resources/images/scared_mouse.gif",
103 +    "/web_workshop/resources/images/smiley_bar.gif",
104 +    "/web_workshop/resources/images/smiley_dancing.gif",
105 +    "/web_workshop/resources/images/smiling_pizza.gif",
106 +    "/web_workshop/resources/images/spacedog.gif",
107 +    "/web_workshop/resources/images/spinninbrain.gif",
108 +    "/web_workshop/resources/images/splat.png",
109 +    "/web_workshop/resources/images/sun.png",
110 +    "/web_workshop/resources/images/surfin.gif",
111 +    "/web_workshop/resources/images/tamas.gif",
112 +    "/web_workshop/resources/images/telephone.gif",
113 +    "/web_workshop/resources/images/underconstruction.gif",
114 +    "/web_workshop/resources/images/welcome.gif",
115 +    "/web_workshop/resources/images/windows.gif",
116 +    "/web_workshop/resources/images/wishingwell.gif",
117 +    "/web_workshop/resources/images/wizard.gif",
118 +    "/web_workshop/resources/images/ww_9volt.gif",
119 +    "/web_workshop/resources/images/ww_ana.gif",
120 +    "/web_workshop/resources/images/ww_crygor.gif",
121 +    "/web_workshop/resources/images/ww_dribble.gif",
122 +    "/web_workshop/resources/images/ww_jimmy.gif",
123 +    "/web_workshop/resources/images/ww_kat.gif",
124 +    "/web_workshop/resources/images/ww_mona.gif",
125 +    "/web_workshop/resources/images/ww_orbulon.gif",
126 +    "/web_workshop/resources/images/ww_spitz.gif",
127 +    "/web_workshop/resources/images/ww_wario.gif",
128 +    "/web_workshop/resources/images/y2k.gif"
129 +  ],
130 +  "resources": [
131 +    "/web_workshop/resources/css/styles.css",
132 +    "/web_workshop/resources/icons/construction.png",
133 +    "/web_workshop/resources/icons/construction_padded.png",
134 +    "/web_workshop/resources/images/.DS_Store",
135 +    "/web_workshop/resources/images/24.gif",
136 +    "/web_workshop/resources/images/bang.gif",
137 +    "/web_workshop/resources/images/bg_bricks.jpg",
138 +    "/web_workshop/resources/images/bg_checker.gif",
139 +    "/web_workshop/resources/images/bg_daisies.jpg",
140 +    "/web_workshop/resources/images/bg_desert.gif",
141 +    "/web_workshop/resources/images/bg_stars.gif",
142 +    "/web_workshop/resources/images/bg_trippy.jpg",
143 +    "/web_workshop/resources/images/bg_water.jpg",
144 +    "/web_workshop/resources/images/bigN.gif",
145 +    "/web_workshop/resources/images/birdbath.gif",
146 +    "/web_workshop/resources/images/bizzy_wizzy.gif",
147 +    "/web_workshop/resources/images/blahblah.gif",
148 +    "/web_workshop/resources/images/bonk.gif",
149 +    "/web_workshop/resources/images/bright_idea.gif",
150 +    "/web_workshop/resources/images/butterflies.gif",
151 +    "/web_workshop/resources/images/butterfly_blue.gif",
152 +    "/web_workshop/resources/images/butterfly_monarch.gif",
153 +    "/web_workshop/resources/images/cactus.gif",
154 +    "/web_workshop/resources/images/cat.gif",
155 +    "/web_workshop/resources/images/caterpillar.gif",
156 +    "/web_workshop/resources/images/cd.gif",
157 +    "/web_workshop/resources/images/christmas_garland.gif",
158 +    "/web_workshop/resources/images/christmas_holly.gif",
159 +    "/web_workshop/resources/images/christmas_wreath.gif",
160 +    "/web_workshop/resources/images/coffee.gif",
161 +    "/web_workshop/resources/images/coffee.png",
162 +    "/web_workshop/resources/images/computer.gif",
163 +    "/web_workshop/resources/images/construct.gif",
164 +    "/web_workshop/resources/images/cow.gif",
165 +    "/web_workshop/resources/images/daisies.gif",
166 +    "/web_workshop/resources/images/dancingman.gif",
167 +    "/web_workshop/resources/images/dancingwoman.gif",
168 +    "/web_workshop/resources/images/devil.gif",
169 +    "/web_workshop/resources/images/disco_ball.gif",
170 +    "/web_workshop/resources/images/divider_blissey.gif",
171 +    "/web_workshop/resources/images/divider_blue_flowers.gif",
172 +    "/web_workshop/resources/images/divider_blue_flowers2.gif",
173 +    "/web_workshop/resources/images/divider_rainbow.gif",
174 +    "/web_workshop/resources/images/divider_rainbow_dots.gif",
175 +    "/web_workshop/resources/images/divider_red_flowers.png",
176 +    "/web_workshop/resources/images/divider_wildflowers.gif",
177 +    "/web_workshop/resources/images/divider_yellow_flowers.gif",
178 +    "/web_workshop/resources/images/dollhouse.gif",
179 +    "/web_workshop/resources/images/doraemon.gif",
180 +    "/web_workshop/resources/images/doraemon_stamp.gif",
181 +    "/web_workshop/resources/images/email.gif",
182 +    "/web_workshop/resources/images/email2.gif",
183 +    "/web_workshop/resources/images/flowers.gif",
184 +    "/web_workshop/resources/images/frog_car.gif",
185 +    "/web_workshop/resources/images/furby1.gif",
186 +    "/web_workshop/resources/images/furby2.gif",
187 +    "/web_workshop/resources/images/furby3.gif",
188 +    "/web_workshop/resources/images/furby4.gif",
189 +    "/web_workshop/resources/images/furby5.gif",
190 +    "/web_workshop/resources/images/furby6.gif",
191 +    "/web_workshop/resources/images/furby7.gif",
192 +    "/web_workshop/resources/images/furby8.gif",
193 +    "/web_workshop/resources/images/furby9.gif",
194 +    "/web_workshop/resources/images/furby_banner.gif",
195 +    "/web_workshop/resources/images/furby_clouds.gif",
196 +    "/web_workshop/resources/images/furby_field.jpg",
197 +    "/web_workshop/resources/images/furby_pink.png",
198 +    "/web_workshop/resources/images/furby_shocked.gif",
199 +    "/web_workshop/resources/images/furby_weird.gif",
200 +    "/web_workshop/resources/images/furbyland.gif",
201 +    "/web_workshop/resources/images/gaia.gif",
202 +    "/web_workshop/resources/images/garf.gif",
203 +    "/web_workshop/resources/images/gc_icon.gif",
204 +    "/web_workshop/resources/images/hacker_skull.gif",
205 +    "/web_workshop/resources/images/hacking.gif",
206 +    "/web_workshop/resources/images/hamster.gif",
207 +    "/web_workshop/resources/images/hamtaro.gif",
208 +    "/web_workshop/resources/images/hellokitty.gif",
209 +    "/web_workshop/resources/images/hint.gif",
210 +    "/web_workshop/resources/images/home.gif",
211 +    "/web_workshop/resources/images/koosh.gif",
212 +    "/web_workshop/resources/images/lil_buddha.gif",
213 +    "/web_workshop/resources/images/lil_demon.gif",
214 +    "/web_workshop/resources/images/linux_power.gif",
215 +    "/web_workshop/resources/images/mac.png",
216 +    "/web_workshop/resources/images/mailbox.gif",
217 +    "/web_workshop/resources/images/mario64.gif",
218 +    "/web_workshop/resources/images/modchips.gif",
219 +    "/web_workshop/resources/images/neopet.gif",
220 +    "/web_workshop/resources/images/ness.gif",
221 +    "/web_workshop/resources/images/new_bunny.gif",
222 +    "/web_workshop/resources/images/no_java.gif",
223 +    "/web_workshop/resources/images/orbit.gif",
224 +    "/web_workshop/resources/images/pika_construction.gif",
225 +    "/web_workshop/resources/images/pink_clock.gif",
226 +    "/web_workshop/resources/images/pochacco.gif",
227 +    "/web_workshop/resources/images/providence.gif",
228 +    "/web_workshop/resources/images/ratrace.gif",
229 +    "/web_workshop/resources/images/ring.gif",
230 +    "/web_workshop/resources/images/sailor_moon.gif",
231 +    "/web_workshop/resources/images/samus.gif",
232 +    "/web_workshop/resources/images/scachis.gif",
233 +    "/web_workshop/resources/images/scared_mouse.gif",
234 +    "/web_workshop/resources/images/smiley_bar.gif",
235 +    "/web_workshop/resources/images/smiley_dancing.gif",
236 +    "/web_workshop/resources/images/smiling_pizza.gif",
237 +    "/web_workshop/resources/images/spacedog.gif",
238 +    "/web_workshop/resources/images/spinninbrain.gif",
239 +    "/web_workshop/resources/images/splat.png",
240 +    "/web_workshop/resources/images/sun.png",
241 +    "/web_workshop/resources/images/surfin.gif",
242 +    "/web_workshop/resources/images/tamas.gif",
243 +    "/web_workshop/resources/images/telephone.gif",
244 +    "/web_workshop/resources/images/underconstruction.gif",
245 +    "/web_workshop/resources/images/welcome.gif",
246 +    "/web_workshop/resources/images/windows.gif",
247 +    "/web_workshop/resources/images/wishingwell.gif",
248 +    "/web_workshop/resources/images/wizard.gif",
249 +    "/web_workshop/resources/images/ww_9volt.gif",
250 +    "/web_workshop/resources/images/ww_ana.gif",
251 +    "/web_workshop/resources/images/ww_crygor.gif",
252 +    "/web_workshop/resources/images/ww_dribble.gif",
253 +    "/web_workshop/resources/images/ww_jimmy.gif",
254 +    "/web_workshop/resources/images/ww_kat.gif",
255 +    "/web_workshop/resources/images/ww_mona.gif",
256 +    "/web_workshop/resources/images/ww_orbulon.gif",
257 +    "/web_workshop/resources/images/ww_spitz.gif",
258 +    "/web_workshop/resources/images/ww_wario.gif",
259 +    "/web_workshop/resources/images/y2k.gif",
260 +    "/web_workshop/resources/js/codemirror-bundle.js",
261 +    "/web_workshop/resources/js/main.js",
262 +    "/web_workshop/resources/readme_images/picnic.png",
263 +    "/web_workshop/resources/readme_images/stock_images.png",
264 +    "/web_workshop/resources/resource-manifest.json",
265 +    "/web_workshop/resources/scripts/generate_resource_manifest.py",
266 +    "/web_workshop/resources/ui/fullscreen.svg",
267 +    "/web_workshop/resources/vendor/build.mjs",
268 +    "/web_workshop/resources/vendor/node_modules/.bin/esbuild",
269 +    "/web_workshop/resources/vendor/node_modules/.package-lock.json",
270 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/.github/workflows/dispatch.yml",
271 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/CHANGELOG.md",
272 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/LICENSE",
273 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/README.md",
274 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.cjs",
275 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.cts",
276 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.ts",
277 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.js",
278 +    "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/package.json",
279 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/.github/workflows/dispatch.yml",
280 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/CHANGELOG.md",
281 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/LICENSE",
282 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/README.md",
283 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.cjs",
284 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.cts",
285 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.ts",
286 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.js",
287 +    "/web_workshop/resources/vendor/node_modules/@codemirror/commands/package.json",
288 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/.github/workflows/dispatch.yml",
289 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/CHANGELOG.md",
290 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/LICENSE",
291 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/README.md",
292 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.cjs",
293 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.cts",
294 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.ts",
295 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.js",
296 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/package.json",
297 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/.github/workflows/dispatch.yml",
298 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/CHANGELOG.md",
299 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/LICENSE",
300 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/README.md",
301 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.cjs",
302 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.cts",
303 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.ts",
304 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.js",
305 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/package.json",
306 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/.github/workflows/dispatch.yml",
307 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/CHANGELOG.md",
308 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/LICENSE",
309 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/README.md",
310 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.cjs",
311 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.cts",
312 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.ts",
313 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.js",
314 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/package.json",
315 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/.github/workflows/dispatch.yml",
316 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/CHANGELOG.md",
317 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/LICENSE",
318 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/README.md",
319 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.cjs",
320 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.cts",
321 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.ts",
322 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.js",
323 +    "/web_workshop/resources/vendor/node_modules/@codemirror/language/package.json",
324 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/.github/workflows/dispatch.yml",
325 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/CHANGELOG.md",
326 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/LICENSE",
327 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/README.md",
328 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.cjs",
329 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.cts",
330 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.ts",
331 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.js",
332 +    "/web_workshop/resources/vendor/node_modules/@codemirror/lint/package.json",
333 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/.github/workflows/dispatch.yml",
334 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/CHANGELOG.md",
335 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/LICENSE",
336 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/README.md",
337 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.cjs",
338 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.cts",
339 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.ts",
340 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.js",
341 +    "/web_workshop/resources/vendor/node_modules/@codemirror/search/package.json",
342 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/.github/workflows/dispatch.yml",
343 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/CHANGELOG.md",
344 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/LICENSE",
345 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/README.md",
346 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.cjs",
347 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.cts",
348 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.ts",
349 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.js",
350 +    "/web_workshop/resources/vendor/node_modules/@codemirror/state/package.json",
351 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/.github/workflows/dispatch.yml",
352 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/CHANGELOG.md",
353 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/LICENSE",
354 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/README.md",
355 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.cjs",
356 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.cts",
357 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.ts",
358 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.js",
359 +    "/web_workshop/resources/vendor/node_modules/@codemirror/view/package.json",
360 +    "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/README.md",
361 +    "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/bin/esbuild",
362 +    "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/package.json",
363 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/README.md",
364 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.cjs",
365 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.cts",
366 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.ts",
367 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.js",
368 +    "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/package.json",
369 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/LICENSE",
370 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/README.md",
371 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.cjs",
372 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.cts",
373 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.ts",
374 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.js",
375 +    "/web_workshop/resources/vendor/node_modules/@lezer/common/package.json",
376 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/CHANGELOG.md",
377 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/LICENSE",
378 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/README.md",
379 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.cjs",
380 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.cts",
381 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.ts",
382 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.js",
383 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/package.json",
384 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/rollup.config.js",
385 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/src/css.grammar",
386 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/src/highlight.js",
387 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.js",
388 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.terms.js",
389 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/src/tokens.js",
390 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/test/declarations.txt",
391 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/test/selector.txt",
392 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/test/statements.txt",
393 +    "/web_workshop/resources/vendor/node_modules/@lezer/css/test/test-css.js",
394 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/LICENSE",
395 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/README.md",
396 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.cjs",
397 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.cts",
398 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.ts",
399 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.js",
400 +    "/web_workshop/resources/vendor/node_modules/@lezer/highlight/package.json",
401 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/CHANGELOG.md",
402 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/LICENSE",
403 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/README.md",
404 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.cjs",
405 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.cts",
406 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.ts",
407 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.js",
408 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/package.json",
409 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/rollup.config.js",
410 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/.tern-port",
411 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/content.js",
412 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/highlight.js",
413 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/html.grammar",
414 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/index.js",
415 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.js",
416 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.terms.js",
417 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/src/tokens.js",
418 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/test/mixed.txt",
419 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/test/tags.txt",
420 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-html.js",
421 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-incremental.js",
422 +    "/web_workshop/resources/vendor/node_modules/@lezer/html/test/vue.txt",
423 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/CHANGELOG.md",
424 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/LICENSE",
425 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/README.md",
426 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.cjs",
427 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.cts",
428 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.ts",
429 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.js",
430 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/package.json",
431 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/rollup.config.js",
432 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/highlight.js",
433 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/javascript.grammar",
434 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.js",
435 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.terms.js",
436 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/tokens.js",
437 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/decorator.txt",
438 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/expression.txt",
439 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/jsx.txt",
440 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/semicolon.txt",
441 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/statement.txt",
442 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/test-javascript.js",
443 +    "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/typescript.txt",
444 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/LICENSE",
445 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/README.md",
446 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.d.ts",
447 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.js",
448 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.cjs",
449 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.cts",
450 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.ts",
451 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.js",
452 +    "/web_workshop/resources/vendor/node_modules/@lezer/lr/package.json",
453 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/LICENSE",
454 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/README.md",
455 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.cjs",
456 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.d.cts",
457 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/package.json",
458 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/rollup.config.js",
459 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.d.ts",
460 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.js",
461 +    "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/test/test-cluster.js",
462 +    "/web_workshop/resources/vendor/node_modules/crelt/LICENSE",
463 +    "/web_workshop/resources/vendor/node_modules/crelt/README.md",
464 +    "/web_workshop/resources/vendor/node_modules/crelt/dist/index.cjs",
465 +    "/web_workshop/resources/vendor/node_modules/crelt/dist/index.d.cts",
466 +    "/web_workshop/resources/vendor/node_modules/crelt/index.d.ts",
467 +    "/web_workshop/resources/vendor/node_modules/crelt/index.js",
468 +    "/web_workshop/resources/vendor/node_modules/crelt/package.json",
469 +    "/web_workshop/resources/vendor/node_modules/crelt/rollup.config.js",
470 +    "/web_workshop/resources/vendor/node_modules/esbuild/LICENSE.md",
471 +    "/web_workshop/resources/vendor/node_modules/esbuild/README.md",
472 +    "/web_workshop/resources/vendor/node_modules/esbuild/bin/esbuild",
473 +    "/web_workshop/resources/vendor/node_modules/esbuild/install.js",
474 +    "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.d.ts",
475 +    "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.js",
476 +    "/web_workshop/resources/vendor/node_modules/esbuild/package.json",
477 +    "/web_workshop/resources/vendor/node_modules/style-mod/LICENSE",
478 +    "/web_workshop/resources/vendor/node_modules/style-mod/README.md",
479 +    "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.cjs",
480 +    "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.d.cts",
481 +    "/web_workshop/resources/vendor/node_modules/style-mod/package.json",
482 +    "/web_workshop/resources/vendor/node_modules/style-mod/src/README.md",
483 +    "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.d.ts",
484 +    "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.js",
485 +    "/web_workshop/resources/vendor/node_modules/style-mod/test/test-style-mod.js",
486 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/.tern-port",
487 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/LICENSE",
488 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/README.md",
489 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.cjs",
490 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.cts",
491 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.ts",
492 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.js",
493 +    "/web_workshop/resources/vendor/node_modules/w3c-keyname/package.json",
494 +    "/web_workshop/resources/vendor/package-lock.json",
495 +    "/web_workshop/resources/vendor/package.json"
496 +  ]
497 +}
resources/scripts/generate_resource_manifest.py
 1 +#!/usr/bin/env python3
 2 +"""
 3 +Generate resource manifest for PWA caching
 4 +Scans the images/ and resources/ directories and creates a JSON manifest file
 5 +"""
 6 +
 7 +import os
 8 +import json
 9 +from pathlib import Path
10 +
11 +def generate_resource_manifest():
12 +    """Generate a JSON manifest of all resources in the images/ and resources/ directories"""
13 +    
14 +    # Define supported image extensions
15 +    image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'}
16 +    
17 +    # Get the repo root (two levels up from resources/scripts/)
18 +    repo_root = Path(__file__).parent.parent.parent
19 +    resources_dir = repo_root / 'resources'
20 +    images_dir = resources_dir / 'images'
21 +    
22 +    # Collect all image files
23 +    image_files = []
24 +    if images_dir.exists():
25 +        for file_path in images_dir.iterdir():
26 +            if file_path.is_file() and file_path.suffix.lower() in image_extensions:
27 +                # Create relative path from web root (GitHub Pages subdirectory)
28 +                relative_path = f"/web_workshop/resources/images/{file_path.name}"
29 +                image_files.append(relative_path)
30 +    
31 +    # Collect ALL resource files (no extension filtering)
32 +    resource_files = []
33 +    if resources_dir.exists():
34 +        for file_path in resources_dir.rglob('*'):
35 +            if file_path.is_file():
36 +                # Create relative path from web root (GitHub Pages subdirectory)
37 +                relative_path = f"/web_workshop/resources/{file_path.relative_to(resources_dir).as_posix()}"
38 +                resource_files.append(relative_path)
39 +    
40 +    # Sort for consistent output
41 +    image_files.sort()
42 +    resource_files.sort()
43 +    
44 +    # Create manifest object
45 +    manifest = {
46 +        "images": image_files,
47 +        "resources": resource_files,
48 +        "generated_at": "auto-generated by GitHub Actions"
49 +    }
50 +    
51 +    # Write manifest file
52 +    manifest_path = resources_dir / 'resource-manifest.json'
53 +    with open(manifest_path, 'w', encoding='utf-8') as f:
54 +        json.dump(manifest, f, indent=2, sort_keys=True)
55 +    
56 +    print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:")
57 +    for img in image_files:
58 +        print(f"  - {img}")
59 +    for res in resource_files:
60 +        print(f"  - {res}")
61 +    
62 +    return manifest
63 +
64 +if __name__ == "__main__":
65 +    manifest = generate_resource_manifest()
66 +    print(f"\nManifest saved to: resources/resource-manifest.json")
resources/fullscreen.svg -> resources/ui/fullscreen.svg
resources/vendor/build.mjs
 1 +#!/usr/bin/env node
 2 +// Build script to bundle CodeMirror for offline use
 3 +//
 4 +// Usage:
 5 +//	cd vendor
 6 +//	npm install
 7 +//	npm run build
 8 +//
 9 +// This will regenerate ../js/codemirror-bundle.js
10 +
11 +import * as esbuild from 'esbuild';
12 +import { fileURLToPath } from 'url';
13 +import { dirname, join } from 'path';
14 +
15 +const __dirname = dirname(fileURLToPath(import.meta.url));
16 +
17 +await esbuild.build({
18 +	stdin: {
19 +		contents: `
20 +			export {EditorView, keymap, placeholder, lineNumbers, Decoration} from "@codemirror/view";
21 +			export {EditorState, Compartment} from "@codemirror/state";
22 +			export {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "@codemirror/commands";
23 +			export {closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete";
24 +			export {html} from "@codemirror/lang-html";
25 +			export {githubDark} from "@fsegurai/codemirror-theme-github-dark";
26 +			export {indentUnit} from "@codemirror/language";
27 +			export {search, searchKeymap, closeSearchPanel, openSearchPanel} from "@codemirror/search";
28 +		`,
29 +		resolveDir: __dirname,
30 +		loader: 'js',
31 +	},
32 +	bundle: true,
33 +	format: 'esm',
34 +	outfile: join(__dirname, '..', 'js', 'codemirror-bundle.js'),
35 +	minify: true,
36 +	target: ['es2020'],
37 +});
38 +
39 +console.log('✓ CodeMirror bundle created: ../js/codemirror-bundle.js');
vendor/package.json -> resources/vendor/package.json
sw.js
 3    '/web_workshop/',
 4    '/web_workshop/index.html',
 5    '/web_workshop/manifest.json',
   -  '/web_workshop/resource-manifest.json',
   -  '/web_workshop/styles.css',
   -  '/web_workshop/main.js',
   -  '/web_workshop/codemirror-bundle.js'
 6 +  '/web_workshop/resources/resource-manifest.json',
 7 +  '/web_workshop/resources/css/styles.css',
 8 +  '/web_workshop/resources/js/main.js',
 9 +  '/web_workshop/resources/js/codemirror-bundle.js'
10  ];
11  
12  // Function to discover and cache all files in directories

44  // Function to cache all resources using the generated manifest
45  async function cacheResources(cache) {
46    try {
   -    const response = await fetch('/web_workshop/resource-manifest.json');
47 +    const response = await fetch('/web_workshop/resources/resource-manifest.json');
48      if (response.ok) {
49        const manifest = await response.json();
50        console.log(`Caching ${manifest.images.length} images and ${manifest.resources.length} resources from manifest`);
vendor/build.mjs
  -#!/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');