about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--html/svg-viewer/index.html522
1 files changed, 522 insertions, 0 deletions
diff --git a/html/svg-viewer/index.html b/html/svg-viewer/index.html
new file mode 100644
index 0000000..232e96b
--- /dev/null
+++ b/html/svg-viewer/index.html
@@ -0,0 +1,522 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
+    <title>SVG viewer</title>
+    <style>
+        body {
+            margin: 0;
+            padding: 10px;
+            font-family: system-ui, -apple-system, sans-serif;
+            min-height: 100vh;
+            /* Prevent content shift on mobile when keyboard appears */
+            min-height: -webkit-fill-available;
+        }
+
+        .container {
+            display: flex;
+            gap: 10px;
+            height: calc(100vh - 20px);
+            /* Support for mobile browsers */
+            height: calc(100vh - 20px - env(safe-area-inset-bottom));
+        }
+
+        .editor, .preview {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            min-height: 200px; /* Ensure minimum height on mobile */
+        }
+
+        textarea {
+            flex: 1;
+            resize: none;
+            padding: 10px;
+            font-family: monospace;
+            font-size: 14px;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            /* Improve mobile experience */
+            -webkit-overflow-scrolling: touch;
+            /* Prevent zoom on mobile devices */
+            font-size: 16px;
+        }
+
+        canvas {
+            flex: 1;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            background: white;
+        }
+
+        h2 {
+            margin-top: 0;
+            margin-bottom: 10px;
+            font-size: 16px;
+        }
+
+        .toolbar {
+            display: flex;
+            gap: 8px;
+            margin-bottom: 16px; /* Increased margin for more space below */
+            flex-wrap: wrap;
+            justify-content: space-between; /* Align error message to the right */
+        }
+
+        .toolbar button {
+            /* Improve touch targets */
+            min-height: 44px;
+            padding: 8px 16px;
+            /* Prevent text wrapping inside button */
+            white-space: nowrap;
+            /* Add visual feedback for touch */
+            transition: background-color 0.2s;
+            -webkit-touch-callout: none;
+            user-select: none;
+            border: 1px solid #ccc;
+            border-radius: 6px;
+            background: #f8f8f8;
+        }
+
+        .toolbar button:active {
+            background-color: #e0e0e0;
+        }
+
+        @media (min-width: 1024px) {
+            .toolbar {
+                flex-wrap: nowrap;
+                overflow-x: auto;
+                -webkit-overflow-scrolling: touch;
+                scrollbar-width: none;
+                -ms-overflow-style: none;
+                scroll-snap-type: x mandatory;
+            }
+
+            .toolbar::-webkit-scrollbar {
+                display: none;
+            }
+
+            .toolbar button {
+                scroll-snap-align: start;
+            }
+        }
+
+        /* Medium and small screens - stacked buttons */
+        @media (max-width: 1023px) {
+            .toolbar {
+                flex-direction: column;
+                width: 100%;
+                gap: 4px;
+            }
+
+            .toolbar button {
+                width: 100%;
+                text-align: left;
+                justify-content: flex-start;
+                padding: 12px 16px;
+            }
+        }
+
+        /* Small mobile optimization */
+        @media (max-width: 480px) {
+            .toolbar button {
+                padding: 10px 14px;
+                font-size: 14px;
+            }
+        }
+
+        /* Landscape mode optimization */
+        @media (orientation: landscape) and (max-height: 500px) {
+            .toolbar {
+                /* Revert to horizontal scroll for landscape */
+                flex-direction: row;
+                flex-wrap: nowrap;
+                overflow-x: auto;
+            }
+
+            .toolbar button {
+                width: auto;
+                padding: 8px 16px;
+                background-image: none;
+            }
+        }
+
+        /* Media query for mobile devices */
+        @media (max-width: 768px) {
+            .container {
+                flex-direction: column;
+                height: auto;
+                min-height: 100vh;
+            }
+
+            .editor, .preview {
+                flex: none;
+                height: calc(50vh - 30px); /* Half viewport minus some spacing */
+                overflow: hidden; /* Contain scrolling children */
+                display: flex;
+                flex-direction: column;
+            }
+
+            .toolbar {
+                flex-direction: column;
+                width: 100%;
+                gap: 12px; /* Increased gap for more space between buttons */
+            }
+
+            textarea, canvas {
+                min-height: 200px; /* Minimum height */
+                height: 100%;
+                overflow-y: auto; /* Enable vertical scrolling */
+            }
+
+            h2 {
+                font-size: 14px;
+                margin-bottom: 8px;
+                flex-shrink: 0; /* Prevent header from shrinking */
+            }
+        }
+
+        /* Landscape mode optimization */
+        @media (orientation: landscape) {
+            .container {
+                flex-direction: row;
+                height: calc(100vh - 20px);
+            }
+
+            .editor, .preview {
+                width: 50%;
+                height: 100%;
+                overflow: hidden; /* Contain scrolling children */
+            }
+
+            .toolbar {
+                flex-direction: row;
+                flex-wrap: wrap;
+                flex-shrink: 0; /* Prevent toolbar from shrinking */
+            }
+        }
+
+        /* Support for notched phones */
+        @supports (padding: max(0px)) {
+            body {
+                padding-left: max(10px, env(safe-area-inset-left));
+                padding-right: max(10px, env(safe-area-inset-right));
+                padding-bottom: max(10px, env(safe-area-inset-bottom));
+            }
+        }
+
+        /* Focus styles for better keyboard navigation */
+        .toolbar button:focus {
+            outline: 2px solid #0066cc;
+            outline-offset: 2px;
+        }
+
+        /* High contrast focus indicator for canvas */
+        canvas:focus {
+            outline: 3px solid #0066cc;
+        }
+
+        /* Error message styling */
+        .error-message {
+            color: #cc0000;
+            font-size: 14px;
+            margin-top: 4px;
+            display: none;
+            flex-grow: 1; /* Allow error message to take available space */
+            text-align: right; /* Align text to the right */
+        }
+
+        /* Enhance focus visibility for WCAG 2.4.7 */
+        *:focus {
+            outline: 3px solid #0066cc;
+            outline-offset: 2px;
+        }
+
+        /* Add focus-visible for modern browsers */
+        *:focus:not(:focus-visible) {
+            outline: none;
+        }
+        
+        *:focus-visible {
+            outline: 3px solid #0066cc;
+            outline-offset: 2px;
+        }
+
+        /* High contrast mode support */
+        @media (forced-colors: active) {
+            .toolbar button {
+                border: 1px solid ButtonBorder;
+                background: ButtonFace;
+                color: ButtonText;
+            }
+            
+            canvas, textarea {
+                border: 1px solid ButtonBorder;
+            }
+        }
+
+        .export-buttons {
+            display: flex;
+            gap: 8px;
+            margin-top: 8px;
+        }
+
+        .export-buttons button {
+            min-height: 44px;
+            padding: 8px 16px;
+            white-space: nowrap;
+            transition: background-color 0.2s;
+            -webkit-touch-callout: none;
+            user-select: none;
+            border: 1px solid #ccc;
+            border-radius: 6px;
+            background: #f8f8f8;
+        }
+
+        .export-buttons button:active {
+            background-color: #e0e0e0;
+        }
+    </style>
+</head>
+<body>
+    <main>
+        <div class="container" role="application" aria-label="SVG Editor Application">
+            <div class="editor">
+                <h2 id="editor-label">SVG Editor</h2>
+                <div class="toolbar" role="toolbar" aria-label="Error message">
+                    <div id="error-message" class="error-message" role="alert"></div>
+                </div>
+                <textarea 
+                    id="svgInput" 
+                    spellcheck="false"
+                    aria-labelledby="editor-label"
+                    aria-describedby="editor-desc error-message"
+                    role="textbox"
+                >&lt;svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"&gt;
+    &lt;circle cx="100" cy="100" r="50" fill="blue" /&gt;
+&lt;/svg&gt;</textarea>
+                <div class="export-buttons">
+                    <button onclick="exportSVG()" aria-label="Export as SVG file">Export SVG</button>
+                    <button onclick="exportPNG()" aria-label="Export as PNG image">Export PNG</button>
+                </div>
+            </div>
+            <div class="preview">
+                <h2 id="preview-label">Preview</h2>
+                <canvas 
+                    id="preview" 
+                    role="img" 
+                    aria-labelledby="preview-label"
+                    tabindex="0"
+                ></canvas>
+            </div>
+        </div>
+    </main>
+
+    <script>
+        const textarea = document.getElementById('svgInput');
+        const canvas = document.getElementById('preview');
+        const ctx = canvas.getContext('2d');
+
+        // Function to convert SVG string to image data
+        const svgToImage = (svgString) => {
+            const blob = new Blob([svgString], { type: 'image/svg+xml' });
+            const url = URL.createObjectURL(blob);
+            return new Promise((resolve, reject) => {
+                const img = new Image();
+                img.onload = () => {
+                    URL.revokeObjectURL(url);
+                    resolve(img);
+                };
+                img.onerror = () => {
+                    URL.revokeObjectURL(url);
+                    reject(new Error('Failed to load SVG'));
+                };
+                img.src = url;
+            });
+        };
+
+        // Function to parse SVG and check for syntax errors
+        const checkSVG = (svgString) => {
+            const parser = new DOMParser();
+            const doc = parser.parseFromString(svgString, 'image/svg+xml');
+            const errorNode = doc.querySelector('parsererror');
+            return errorNode ? errorNode.textContent : null;
+        };
+
+        // Function to update the preview
+        const updatePreview = async () => {
+            const errorMessage = document.getElementById('error-message');
+            const svgString = textarea.value;
+            const error = checkSVG(svgString);
+
+            if (error) {
+                errorMessage.style.display = 'block';
+                errorMessage.textContent = `SVG Syntax Error: ${error}`;
+                canvas.setAttribute('aria-label', 'Preview unavailable due to SVG error');
+                return;
+            }
+
+            try {
+                const img = await svgToImage(svgString);
+                
+                // Set canvas size to match its display size
+                const rect = canvas.getBoundingClientRect();
+                canvas.width = rect.width;
+                canvas.height = rect.height;
+                
+                // Clear canvas
+                ctx.clearRect(0, 0, canvas.width, canvas.height);
+                
+                // Calculate scaling to fit SVG while maintaining aspect ratio
+                const scale = Math.min(
+                    canvas.width / img.width,
+                    canvas.height / img.height
+                );
+                
+                // Center the image
+                const x = (canvas.width - img.width * scale) / 2;
+                const y = (canvas.height - img.height * scale) / 2;
+                
+                // Draw scaled image
+                ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
+
+                errorMessage.style.display = 'none';
+                errorMessage.textContent = '';
+                canvas.setAttribute('aria-label', 'SVG preview showing current editor content');
+            } catch (error) {
+                console.error('Error updating preview:', error);
+                errorMessage.style.display = 'block';
+                errorMessage.textContent = 'Error in SVG syntax. Please check your markup.';
+                canvas.setAttribute('aria-label', 'Preview unavailable due to SVG error');
+            }
+        };
+
+        // Update preview when text changes
+        textarea.addEventListener('input', () => {
+            updatePreview();
+        });
+
+        // Handle window resize
+        window.addEventListener('resize', () => {
+            updatePreview();
+        });
+
+        // Initial preview
+        updatePreview();
+
+        // Template functions for SVG shapes
+        const shapeTemplates = {
+            rect: '    <rect x="10" y="10" width="100" height="80" fill="red" />\n',
+            circle: '    <circle cx="100" cy="100" r="50" fill="blue" />\n',
+            ellipse: '    <ellipse cx="100" cy="100" rx="80" ry="50" fill="green" />\n',
+            line: '    <line x1="10" y1="10" x2="190" y2="190" stroke="black" stroke-width="2" />\n',
+            path: '    <path d="M 10,10 L 100,10 L 100,100 Z" fill="purple" />\n',
+            text: '    <text x="100" y="100" text-anchor="middle">Hello SVG</text>\n'
+        };
+
+        // Function to insert template at cursor position
+        const insertShape = (shapeType) => {
+            const template = shapeTemplates[shapeType];
+            const start = textarea.selectionStart;
+            const end = textarea.selectionEnd;
+            const text = textarea.value;
+            
+            // Find the closing </svg> tag
+            const closingTag = '</svg>';
+            const closingTagIndex = text.lastIndexOf(closingTag);
+            
+            if (closingTagIndex !== -1) {
+                // Insert the new shape before the closing tag
+                const newText = 
+                    text.substring(0, closingTagIndex) +
+                    template +
+                    text.substring(closingTagIndex);
+                
+                textarea.value = newText;
+                updatePreview();
+                
+                // Set cursor after inserted template
+                const newPosition = closingTagIndex + template.length;
+                textarea.setSelectionRange(newPosition, newPosition);
+                textarea.focus();
+            }
+        };
+
+        // Keyboard handling for toolbar
+        document.querySelector('.toolbar').addEventListener('keydown', (e) => {
+            const buttons = Array.from(e.currentTarget.querySelectorAll('button'));
+            const currentIndex = buttons.indexOf(document.activeElement);
+            
+            switch(e.key) {
+                case 'ArrowRight':
+                case 'ArrowDown':
+                    e.preventDefault();
+                    buttons[(currentIndex + 1) % buttons.length].focus();
+                    break;
+                case 'ArrowLeft':
+                case 'ArrowUp':
+                    e.preventDefault();
+                    buttons[(currentIndex - 1 + buttons.length) % buttons.length].focus();
+                    break;
+            }
+        });
+
+        // Announce successful exports
+        const announceExport = (format) => {
+            const message = `${format} file exported successfully`;
+            const announcement = document.createElement('div');
+            announcement.setAttribute('role', 'status');
+            announcement.setAttribute('aria-live', 'polite');
+            announcement.style.position = 'absolute';
+            announcement.style.width = '1px';
+            announcement.style.height = '1px';
+            announcement.style.overflow = 'hidden';
+            announcement.textContent = message;
+            document.body.appendChild(announcement);
+            setTimeout(() => document.body.removeChild(announcement), 1000);
+        };
+
+        // Update export functions
+        const exportSVG = () => {
+            const svgBlob = new Blob([textarea.value], { type: 'image/svg+xml' });
+            const url = URL.createObjectURL(svgBlob);
+            const link = document.createElement('a');
+            link.href = url;
+            link.download = 'drawing.svg';
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+            URL.revokeObjectURL(url);
+            announceExport('SVG');
+        };
+
+        const exportPNG = () => {
+            const tempCanvas = document.createElement('canvas');
+            const tempCtx = tempCanvas.getContext('2d');
+            
+            // Create image from SVG
+            svgToImage(textarea.value).then(img => {
+                // Set canvas to SVG's native size
+                tempCanvas.width = img.width;
+                tempCanvas.height = img.height;
+                
+                // Draw with white background
+                tempCtx.fillStyle = 'white';
+                tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
+                tempCtx.drawImage(img, 0, 0);
+                
+                // Export
+                const url = tempCanvas.toDataURL('image/png');
+                const link = document.createElement('a');
+                link.href = url;
+                link.download = 'drawing.png';
+                document.body.appendChild(link);
+                link.click();
+                document.body.removeChild(link);
+                announceExport('PNG');
+            });
+        };
+    </script>
+</body>
+</html>
\ No newline at end of file