move fullscreen icon to right of preview pane; use SVG icon

Hunter
Wednesday, July 16th, 2025 at 1:13 PM

.github/workflows/generate-manifest.yml+6-6
generate_image_manifest.pydeleted
generate_resource_manifest.pynew
image-manifest.jsondeleted
index.html+41-14
resource-manifest.jsonnew
resources/fullscreen.svgnew
sw.js+20-9

.github/workflows/generate-manifest.yml
   -name: Generate Image Manifest
 1 +name: Generate Resource Manifest
 2  
 3  on:
 4    push:
 5      branches: [ main ]
   -    paths: [ 'images/**' ]  # Only run when images are added/changed
 6 +    paths: [ 'images/**', 'resources/**' ]  # Run when images or resources are added/changed
 7    workflow_dispatch:  # Allow manual trigger
 8  
 9  jobs:

23        with:
24          python-version: '3.x'
25      
   -    - name: Generate image manifest
   -      run: python generate_image_manifest.py
26 +    - name: Generate resource manifest
27 +      run: python 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 image-manifest.json
   -        git commit -m "Auto-update image manifest [skip ci]"
43 +        git add resource-manifest.json
44 +        git commit -m "Auto-update resource manifest [skip ci]"
45          git push
generate_image_manifest.py
  -#!/usr/bin/env python3
  -"""
  -Generate image manifest for PWA caching
  -Scans the images/ directory and creates a JSON manifest file
  -"""
  -
  -import os
  -import json
  -from pathlib import Path
  -
  -def generate_image_manifest():
  -    """Generate a JSON manifest of all images in the images/ directory"""
  -    
  -    # 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'
  -    
  -    # Check if images directory exists
  -    if not images_dir.exists():
  -        print(f"Images directory not found: {images_dir}")
  -        return
  -    
  -    # Collect all image files
  -    image_files = []
  -    
  -    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)
  -    
  -    # Sort for consistent output
  -    image_files.sort()
  -    
  -    # Create manifest object
  -    manifest = {
  -        "images": image_files,
  -        "generated_at": "auto-generated by GitHub Actions",
  -        "total_images": len(image_files)
  -    }
  -    
  -    # Write manifest file
  -    manifest_path = script_dir / 'image-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:")
  -    for img in image_files:
  -        print(f"  - {img}")
  -    
  -    return manifest
  -
  -if __name__ == "__main__":
  -    manifest = generate_image_manifest()
  -    print(f"\nManifest saved to: image-manifest.json")
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 directory where this script is located
18 +    script_dir = Path(__file__).parent
19 +    images_dir = script_dir / 'images'
20 +    resources_dir = script_dir / 'resources'
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/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 +        "total_images": len(image_files),
50 +        "total_resources": len(resource_files),
51 +        "total_files": len(image_files) + len(resource_files)
52 +    }
53 +    
54 +    # Write manifest file
55 +    manifest_path = script_dir / 'resource-manifest.json'
56 +    with open(manifest_path, 'w', encoding='utf-8') as f:
57 +        json.dump(manifest, f, indent=2, sort_keys=True)
58 +    
59 +    print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:")
60 +    for img in image_files:
61 +        print(f"  - {img}")
62 +    for res in resource_files:
63 +        print(f"  - {res}")
64 +    
65 +    return manifest
66 +
67 +if __name__ == "__main__":
68 +    manifest = generate_resource_manifest()
69 +    print(f"\nManifest saved to: resource-manifest.json")
image-manifest.json
  -{
  -  "generated_at": "auto-generated by GitHub Actions",
  -  "images": [
  -    "/web_workshop/images/bigN.gif",
  -    "/web_workshop/images/bright_idea.gif",
  -    "/web_workshop/images/calamar.gif",
  -    "/web_workshop/images/cd.gif",
  -    "/web_workshop/images/email.gif",
  -    "/web_workshop/images/gaia.gif",
  -    "/web_workshop/images/hint.gif",
  -    "/web_workshop/images/mac.png",
  -    "/web_workshop/images/mario64.gif",
  -    "/web_workshop/images/pastel_square.png",
  -    "/web_workshop/images/pika_construction.gif",
  -    "/web_workshop/images/pink_clock.gif",
  -    "/web_workshop/images/rollerskate.png",
  -    "/web_workshop/images/scared_mouse.gif",
  -    "/web_workshop/images/smiley_dancing.gif",
  -    "/web_workshop/images/smiling_pizza.gif",
  -    "/web_workshop/images/spinninbrain.gif",
  -    "/web_workshop/images/splat.png",
  -    "/web_workshop/images/sun.png",
  -    "/web_workshop/images/underconstruction.gif",
  -    "/web_workshop/images/welcome.gif",
  -    "/web_workshop/images/windows.gif"
  -  ],
  -  "total_images": 22
  -}
index.html
 23  			--editor-bg: #16181b;
 24  		}
 25  
    -
 26  		* {
 27  			margin: 0;
 28  			padding: 0;

 53  		.fullscreen-toggle {
 54  			position: absolute;
 55  			top: 5px;
    -			left: 5px;
    -			z-index: 1000;
 56 +			right: 5px;
 57 +			z-index: 10000;
 58  			background: rgba(0, 0, 0, 0.2);
 59  			color: white;
 60  			border: none;
 61  			border-radius: 4px;
    -			padding: 5px 8px;
 62 +			width: 32px;
 63 +			height: 32px;
 64 +			display: flex;
 65 +			align-items: center;
 66 +			justify-content: center;
 67  			cursor: pointer;
    -			font-size: 16px;
 68  			transition: background-color 0.2s;
 69 +			box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
 70 +		}
 71 +
 72 +		.fullscreen-toggle.has-scrollbar {
 73 +			right: calc(5px + env(scrollbar-width, 15px));
 74  		}
 75  
 76  		.fullscreen-toggle:hover {
    -			background: rgba(0, 0, 0, 0.5);
 77 +			background: rgba(0, 0, 0, 0.35);
 78 +		}
 79 +
 80 +		.fullscreen-toggle img {
 81 +			filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3));
 82 +			opacity: 0.8;
 83 +			transition: opacity 0.2s;
 84 +		}
 85 +
 86 +		.fullscreen-toggle:hover img {
 87 +			opacity: 1;
 88  		}
 89  
 90  		.preview-pane.fullscreen {

 97  			background: white;
 98  		}
 99  
    -		.preview-pane.fullscreen .fullscreen-toggle {
    -			top: 20px;
    -			left: 20px;
    -		}
    -
100  		/* Hide editor when preview is fullscreen */
101  		.editor-pane.hidden {
102  			display: none;

183  	</div>
184  	
185  	<div class="preview-pane">
    -		<button class="fullscreen-toggle" id="fullscreenToggle" title="Toggle fullscreen">⛶</button>
186 +		<button class="fullscreen-toggle" id="fullscreenToggle" title="Toggle fullscreen">
187 +			<img src="resources/fullscreen.svg" width="20" height="20" alt="Fullscreen">
188 +		</button>
189  		<iframe id="preview"></iframe>
190  	</div>
191  

206  			if (isFullscreen) {
207  				previewPane.classList.add('fullscreen');
208  				editorPane.classList.add('hidden');
    -				toggleButton.textContent = '⛶';
209  				toggleButton.title = 'Exit fullscreen';
210  			} else {
211  				previewPane.classList.remove('fullscreen');
212  				editorPane.classList.remove('hidden');
    -				toggleButton.textContent = '⛶';
213  				toggleButton.title = 'Toggle fullscreen';
214  			}
215  		}

219  			toggleButton.addEventListener('click', toggleFullscreen);
220  		});
221  
222 +		function updateScrollbarDetection() {
223 +			const toggleButton = document.getElementById('fullscreenToggle');
224 +			try {
225 +				if (preview.contentDocument && preview.contentDocument.body) {
226 +					const body = preview.contentDocument.body;
227 +					const hasScrollbar = body.scrollHeight > preview.clientHeight;
228 +					toggleButton.classList.toggle('has-scrollbar', hasScrollbar);
229 +				}
230 +			} catch (e) {
231 +				// Ignore cross-origin errors
232 +			}
233 +		}
234 +
235  		function updatePreview() {
236  			const code = editorView.state.doc.toString();
237  			

269  				} catch (e) {
270  					// Ignore cross-origin errors
271  				}
272 +				// Check for scrollbar after content is rendered
273 +				updateScrollbarDetection();
274  			}, 10);
275  		}
276  
resource-manifest.json
 1 +{
 2 +  "generated_at": "auto-generated by GitHub Actions",
 3 +  "images": [
 4 +    "/web_workshop/images/bigN.gif",
 5 +    "/web_workshop/images/bright_idea.gif",
 6 +    "/web_workshop/images/calamar.gif",
 7 +    "/web_workshop/images/cd.gif",
 8 +    "/web_workshop/images/email.gif",
 9 +    "/web_workshop/images/gaia.gif",
10 +    "/web_workshop/images/hint.gif",
11 +    "/web_workshop/images/mac.png",
12 +    "/web_workshop/images/mario64.gif",
13 +    "/web_workshop/images/pastel_square.png",
14 +    "/web_workshop/images/pika_construction.gif",
15 +    "/web_workshop/images/pink_clock.gif",
16 +    "/web_workshop/images/rollerskate.png",
17 +    "/web_workshop/images/scared_mouse.gif",
18 +    "/web_workshop/images/smiley_dancing.gif",
19 +    "/web_workshop/images/smiling_pizza.gif",
20 +    "/web_workshop/images/spinninbrain.gif",
21 +    "/web_workshop/images/splat.png",
22 +    "/web_workshop/images/sun.png",
23 +    "/web_workshop/images/underconstruction.gif",
24 +    "/web_workshop/images/welcome.gif",
25 +    "/web_workshop/images/windows.gif"
26 +  ],
27 +  "resources": [
28 +    "/web_workshop/resources/fullscreen.svg"
29 +  ],
30 +  "total_files": 23,
31 +  "total_images": 22,
32 +  "total_resources": 1
33 +}
resources/fullscreen.svg
 1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 +<svg
 3 +   width="14"
 4 +   height="14"
 5 +   viewBox="0 0 14 14"
 6 +   version="1.1"
 7 +   id="svg11"
 8 +   sodipodi:docname="fullscreen.svg"
 9 +   inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
10 +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11 +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
12 +   xmlns="http://www.w3.org/2000/svg"
13 +   xmlns:svg="http://www.w3.org/2000/svg">
14 +  <defs
15 +     id="defs15" />
16 +  <sodipodi:namedview
17 +     id="namedview13"
18 +     pagecolor="#ffffff"
19 +     bordercolor="#000000"
20 +     borderopacity="0.25"
21 +     inkscape:showpageshadow="2"
22 +     inkscape:pageopacity="0.0"
23 +     inkscape:pagecheckerboard="0"
24 +     inkscape:deskcolor="#d1d1d1"
25 +     showgrid="false"
26 +     inkscape:zoom="9.8333333"
27 +     inkscape:cx="-9.1525424"
28 +     inkscape:cy="7.0169492"
29 +     inkscape:window-width="1309"
30 +     inkscape:window-height="404"
31 +     inkscape:window-x="0"
32 +     inkscape:window-y="38"
33 +     inkscape:window-maximized="0"
34 +     inkscape:current-layer="svg11" />
35 +  <path
36 +     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"
37 +     id="path9"
38 +     style="fill:#ffffff"
39 +     transform="translate(-5,-5)" />
40 +</svg>
sw.js
    -const CACHE_NAME = 'web-workshop-v7';
  1 +const CACHE_NAME = 'web-workshop-v8';
  2  const urlsToCache = [
  3    '/web_workshop/',
  4    '/web_workshop/index.html',

 45    }
 46  }
 47  
    -// Function to cache all images using the generated manifest
    -async function cacheImages(cache) {
 48 +// Function to cache all resources using the generated manifest
 49 +async function cacheResources(cache) {
 50    try {
    -    const response = await fetch('/web_workshop/image-manifest.json');
 51 +    const response = await fetch('/web_workshop/resource-manifest.json');
 52      if (response.ok) {
 53        const manifest = await response.json();
    -      console.log(`Caching ${manifest.total_images} images from manifest`);
 54 +      console.log(`Caching ${manifest.total_images} images and ${manifest.total_resources} resources from manifest`);
 55        
 56 +      // Cache images
 57        for (const imagePath of manifest.images) {
 58          try {
 59            await cache.add(imagePath);

 62            console.log(`Failed to cache image ${imagePath}:`, e);
 63          }
 64        }
 65 +      
 66 +      // Cache resources
 67 +      for (const resourcePath of manifest.resources) {
 68 +        try {
 69 +          await cache.add(resourcePath);
 70 +          console.log(`Cached resource: ${resourcePath}`);
 71 +        } catch (e) {
 72 +          console.log(`Failed to cache resource ${resourcePath}:`, e);
 73 +        }
 74 +      }
 75      } else {
    -      console.log('No image manifest found, skipping image caching');
 76 +      console.log('No resource manifest found, skipping resource caching');
 77      }
 78    } catch (e) {
    -    console.log('Failed to load image manifest:', e);
 79 +    console.log('Failed to load resource manifest:', e);
 80    }
 81  }
 82  

 99            }
100          }
101          
    -        // Cache all images programmatically
    -        await cacheImages(cache);
102 +        // Cache all resources programmatically
103 +        await cacheResources(cache);
104        })
105        .then(() => self.skipWaiting())
106    );