pull js and css out of index.html
Hunter
Monday, December 22nd, 2025 at 9:43 PM
6 <title>Web Workshop</title>
7 <link rel="icon" href="resources/rollerskate.png">
8 <link rel="manifest" href="manifest.json">
9 + <link rel="stylesheet" href="styles.css">
10 <script type="module">
11 import {EditorView, keymap, placeholder, lineNumbers, Decoration} from "https://esm.sh/@codemirror/view@6"
12 import {EditorState, Compartment} from "https://esm.sh/@codemirror/state@6"
16 import {githubDark} from "https://esm.sh/@fsegurai/codemirror-theme-github-dark"
17 import {indentUnit} from "https://esm.sh/@codemirror/language@6"
18 import {search, searchKeymap, closeSearchPanel, openSearchPanel} from "https://esm.sh/@codemirror/search@6"
-
19 +
20 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};
21 </script>
- <style>
- :root {
- --text-color: #f8f8f2;
- --editor-bg: #16181b;
- scrollbar-color: grey var(--editor-bg);
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- html, body {
- overscroll-behavior: none;
- background-color: var(--editor-bg);
- }
-
- html {
- height: 100%;
- }
-
- body {
- font-family: monospace;
- background: var(--bg-color);
- color: var(--text-color);
- height: 100%;
- display: flex;
- overflow: hidden;
- }
-
- .editor-pane {
- width: 50%;
- height: 100%;
- position: relative;
- transition: opacity 0.1s ease-out; /* Added transition for smoother opacity hack */
- }
-
- .preview-pane {
- width: 50%;
- height: 100%;
- position: relative;
- }
-
- .preview-pane.fullscreen {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- z-index: 9999;
- background: white;
- }
-
- /* Hide editor when preview is fullscreen */
- .editor-pane.hidden {
- display: none;
- }
-
- #editor {
- width: 100%;
- height: 100%;
- font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
- font-size: 21px;
- }
-
- /* CodeMirror styling */
- .cm-editor {
- width: 100%;
- height: 100%;
- background: var(--editor-bg);
- color: var(--text-color);
- }
-
- .cm-placeholder {
- color: #666;
- opacity: 0.6;
- }
-
- .cm-content {
- padding: 15px;
- font-size: 21px;
- line-height: 1.6;
- }
-
- .cm-gutters {
- font-size: 21px;
- line-height: 1.6;
- user-select: none;
- }
-
- .cm-lineNumbers .cm-gutterElement {
- line-height: 1.6;
- font-size: 21px;
- }
-
- .ͼ1 .cm-lineNumbers .cm-gutterElement {
- padding-right: 0;
- padding-left: 8px;
- }
-
- .cm-focused {
- outline: none;
- }
-
- /* Force tab size to 2 spaces - override theme defaults */
- .cm-editor .cm-content,
- .cm-editor .cm-line,
- .cm-editor {
- tab-size: 2 !important;
- -moz-tab-size: 2 !important;
- }
-
- /* Disable overscroll on all CodeMirror elements */
- .cm-editor, .cm-content, .cm-scroller, .cm-scrollElement {
- overscroll-behavior: none !important;
- }
-
- /* Simplify search panel - hide all buttons, labels, and replace input */
- .cm-search button {
- display: none !important;
- }
-
- .cm-search {
- background-color: #16181b;
- }
-
- .cm-search label {
- display: none !important;
- }
-
- .cm-search input[name="replace"] {
- display: none !important;
- }
-
- /* Style search input to match editor text size */
- .cm-search input[name="search"] {
- font-size: 21px !important;
- width: 100% !important;
- border-radius: 50px !important;
- padding-left: 0.8em !important;
- padding-right: 0.8em !important;
- padding-bottom: 7px !important;
- }
-
- .cm-panels-bottom {
- border-radius: 0px !important;
- }
-
- .cm-panels-bottom {
- border-top: none !important;
-
- }
-
- #preview {
- width: 100%;
- height: 100%;
- border: none;
- background: white;
- overflow: auto;
- }
-
- /* Vertical layout for narrow screens (height > width) */
- @media (max-aspect-ratio: 1/1) {
- body {
- flex-direction: column;
- }
-
- .editor-pane {
- width: 100%;
- height: 50%;
- order: 2;
- }
-
- .preview-pane {
- width: 100%;
- height: 50%;
- order: 1;
- padding-top: env(safe-area-inset-top);
- }
- }
-
- /* Mobile: fullscreen editor when keyboard is open */
- @media (pointer: coarse), (pointer: none) {
- body.mobile-keyboard-open .preview-pane {
- display: none;
- }
-
- body.mobile-keyboard-open .editor-pane {
- position: fixed;
- top: calc(var(--visual-viewport-offset-top, 0px));
- left: 0;
- width: 100%;
- height: var(--visual-viewport-height, 100%);
- max-height: var(--visual-viewport-height, 100%);
- order: 1;
- overscroll-behavior: none;
- overflow: hidden;
- }
-
- body.mobile-keyboard-open .editor-pane #editor,
- body.mobile-keyboard-open .editor-pane .cm-editor {
- height: 100% !important;
- max-height: 100% !important;
- }
- }
-
- </style>
22 </head>
23 <body>
24 <div class="editor-pane">
25 <div id="editor"></div>
26 </div>
-
27 +
28 <div class="preview-pane">
29 <iframe id="preview"></iframe>
30 </div>
31
- <script type="module">
- 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) {
- return 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;
- });
- }
-
- // Expand <images> tags into gallery table
- function expandImagesTag(html) {
- return html.replace(/<images\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><td>${filename}</td><td style="text-align:center;"><img src="images/${filename}"></td></tr>`
- ).join('');
- return `<table border="1" cellpadding="8" cellspacing="0" style="width:100%;"><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');
- } else {
- previewPane.classList.remove('fullscreen');
- editorPane.classList.remove('hidden');
- }
-
- // 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/rollerskate.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);
- }
-
- // 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, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration, DecorationSet} = window.CodeMirror;
-
- // 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: [
- 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(),
- 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() && document.body.classList.contains('mobile-keyboard-open')) {
-
- // 1. Hide editor immediately to prevent the "stutter/jump" visual
- editorPane.style.opacity = '0';
-
- // 2. Remove the class to trigger layout engine (100% -> 50% height)
- document.body.classList.remove('mobile-keyboard-open');
-
- // 3. 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);
- }
- // Reveal the editor
- editorPane.style.opacity = '';
-
- // Update preview with any changes made while keyboard was open
- updatePreview();
- }));
- }
- });
-
- // 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);
- }
- });
- }
-
- // 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;
-
- 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) {
- // Check if the class is still there (it might have been removed by blur already)
- const wasKeyboardOpen = document.body.classList.contains('mobile-keyboard-open');
-
- if (wasKeyboardOpen) {
- // This is a fallback in case blur didn't catch it
- // (e.g. if the keyboard was dismissed via a gesture that didn't blur immediately)
- if (editorView) {
- const pos = editorView.state.selection.main.head;
- editorPane.style.opacity = '0';
- document.body.classList.remove('mobile-keyboard-open');
-
- requestAnimationFrame(() => requestAnimationFrame(() => {
- const lineBlock = editorView.lineBlockAt(pos);
- const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2);
- editorView.scrollDOM.scrollTop = Math.max(0, targetScroll);
- editorPane.style.opacity = '';
- }));
- } else {
- document.body.classList.remove('mobile-keyboard-open');
- }
- }
- }
- }
-
- 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);
- });
- });
- }
- </script>
32 + <script type="module" src="main.js"></script>
33 </body>
-</html>
34 +</html>
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('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 images/ prefix (case-insensitive, uses correct case)
20 +function rewriteBareImageSrcs(html) {
21 + return html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi,
22 + (match, before, filename, ext, after) => {
23 + const original = stockImages.get(filename.toLowerCase());
24 + if (original) {
25 + return before + 'images/' + original + after;
26 + }
27 + return match;
28 + });
29 +}
30 +
31 +// Expand <images> tags into gallery table
32 +function expandImagesTag(html) {
33 + return html.replace(/<images\s*\/?>/gi, () => {
34 + const images = Array.from(stockImages.values()).sort();
35 + if (images.length === 0) return '<p>No images available</p>';
36 + const rows = images.map(filename =>
37 + `<tr><td>${filename}</td><td style="text-align:center;"><img src="images/${filename}"></td></tr>`
38 + ).join('');
39 + return `<table border="1" cellpadding="8" cellspacing="0" style="width:100%;"><colgroup><col style="width:50%"><col style="width:50%"></colgroup>${rows}</table>`;
40 + });
41 +}
42 +const editorPane = document.querySelector('.editor-pane');
43 +const previewPane = document.querySelector('.preview-pane');
44 +const storageKey = 'html-lab-content';
45 +let isFullscreen = false;
46 +let showLineNumbers = false;
47 +let enableLineWrapping = false;
48 +let lineNumbersCompartment;
49 +let lineWrappingCompartment;
50 +
51 +function toggleFullscreen() {
52 +
53 + isFullscreen = !isFullscreen;
54 +
55 + if (isFullscreen) {
56 + previewPane.classList.add('fullscreen');
57 + editorPane.classList.add('hidden');
58 + } else {
59 + previewPane.classList.remove('fullscreen');
60 + editorPane.classList.remove('hidden');
61 + }
62 +
63 + // Update button title in iframe
64 + try {
65 + const toggleButton = preview.contentDocument.getElementById('fullscreenToggle');
66 + if (toggleButton) {
67 + toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen';
68 + }
69 + } catch (e) {
70 + // Ignore cross-origin errors
71 + }
72 +}
73 +
74 +
75 +// Listen for messages from iframe
76 +window.addEventListener('message', function(event) {
77 + if (event.data === 'toggleFullscreen') {
78 + toggleFullscreen();
79 + }
80 +});
81 +
82 +function extractTitleAndFavicon(htmlCode) {
83 + const parser = new DOMParser();
84 + const doc = parser.parseFromString(htmlCode, 'text/html');
85 +
86 + // Extract title
87 + const titleElement = doc.querySelector('title');
88 + const title = titleElement ? titleElement.textContent.trim() : null;
89 +
90 + // Extract favicon
91 + const faviconSelectors = [
92 + 'link[rel="icon"]',
93 + 'link[rel="shortcut icon"]',
94 + 'link[rel="apple-touch-icon"]',
95 + 'link[rel="mask-icon"]'
96 + ];
97 +
98 + let favicon = null;
99 + for (const selector of faviconSelectors) {
100 + const faviconElement = doc.querySelector(selector);
101 + if (faviconElement && faviconElement.href) {
102 + favicon = faviconElement.href;
103 + break;
104 + }
105 + }
106 +
107 + return { title, favicon };
108 +}
109 +
110 +function updateMainPageTitleAndFavicon(title, favicon) {
111 + // Update title
112 + if (title) {
113 + document.title = title;
114 + } else {
115 + document.title = 'Web Workshop';
116 + }
117 +
118 + // Update favicon
119 + let faviconLink = document.querySelector('link[rel="icon"]');
120 + if (!faviconLink) {
121 + faviconLink = document.createElement('link');
122 + faviconLink.rel = 'icon';
123 + document.head.appendChild(faviconLink);
124 + }
125 +
126 + if (favicon) {
127 + faviconLink.href = favicon;
128 + } else {
129 + faviconLink.href = 'resources/rollerskate.png';
130 + }
131 +}
132 +
133 +function updatePreview() {
134 + // Skip preview updates while mobile keyboard is open and editor is focused
135 + // This prevents keyboard layer resets on Android
136 + if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) {
137 + return;
138 + }
139 +
140 + const code = editorView.state.doc.toString();
141 +
142 + // Extract and update title and favicon from user's HTML
143 + const { title, favicon } = extractTitleAndFavicon(code);
144 + updateMainPageTitleAndFavicon(title, favicon);
145 +
146 + // Store scroll position before updating
147 + let scrollX = 0, scrollY = 0;
148 + try {
149 + if (preview.contentWindow?.scrollX !== undefined) {
150 + scrollX = preview.contentWindow.scrollX;
151 + scrollY = preview.contentWindow.scrollY;
152 + }
153 + } catch (e) {
154 + // Ignore cross-origin errors
155 + }
156 +
157 + // Use srcdoc to create a completely fresh document context
158 + // Expand <images> tag and rewrite bare stock image filenames
159 + const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>';
160 + preview.srcdoc = processedCode;
161 +
162 + // Add our functionality after the iframe loads
163 + const onLoad = () => {
164 + try {
165 + const doc = preview.contentDocument;
166 + if (!doc) return;
167 +
168 + // Add CSS for overscroll and button
169 + const style = doc.createElement('style');
170 + 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; } }';
171 + doc.head.appendChild(style);
172 +
173 + // Create fullscreen button
174 + const existingButton = doc.getElementById('fullscreenToggle');
175 + if (existingButton) existingButton.remove();
176 +
177 + const button = doc.createElement('button');
178 + button.id = 'fullscreenToggle';
179 + button.className = 'iframe-fullscreen-toggle';
180 + button.title = 'Toggle fullscreen';
181 +
182 + // Use inline SVG to avoid being affected by user's img styles
183 + 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>';
184 +
185 + button.addEventListener('click', function() {
186 + parent.postMessage('toggleFullscreen', '*');
187 + });
188 +
189 + if (doc.body) {
190 + doc.body.appendChild(button);
191 + }
192 +
193 + // Restore scroll position
194 + setTimeout(() => {
195 + try {
196 + preview.contentWindow?.scrollTo(scrollX, scrollY);
197 + } catch (e) {
198 + // Ignore cross-origin errors
199 + }
200 + }, 10);
201 + } catch (e) {
202 + // Ignore cross-origin errors
203 + }
204 +
205 + preview.removeEventListener('load', onLoad);
206 + };
207 + preview.addEventListener('load', onLoad);
208 +}
209 +
210 +function saveToStorage() {
211 + try {
212 + localStorage.setItem(storageKey, editorView.state.doc.toString());
213 + } catch (e) {
214 + console.warn('Could not save to localStorage:', e);
215 + }
216 +}
217 +
218 +function loadFromStorage() {
219 + try {
220 + return localStorage.getItem(storageKey) || '';
221 + } catch (e) {
222 + console.warn('Could not load from localStorage:', e);
223 + return '';
224 + }
225 +}
226 +
227 +function loadEditorSettings() {
228 + try {
229 + showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true';
230 + enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false';
231 + } catch (e) {
232 + console.warn('Could not load editor settings from localStorage:', e);
233 + showLineNumbers = false;
234 + enableLineWrapping = true;
235 + }
236 +}
237 +
238 +function saveEditorSetting(key, value) {
239 + try {
240 + localStorage.setItem(key, value.toString());
241 + } catch (e) {
242 + console.warn('Could not save editor setting to localStorage:', e);
243 + }
244 +}
245 +
246 +function toggleLineNumbers() {
247 + const {lineNumbers} = window.CodeMirror;
248 + showLineNumbers = !showLineNumbers;
249 + saveEditorSetting('editor-line-numbers', showLineNumbers);
250 +
251 + editorView.dispatch({
252 + effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : [])
253 + });
254 +}
255 +
256 +function createLineWrappingExtension() {
257 + const {EditorView, Decoration} = window.CodeMirror;
258 +
259 + return [
260 + EditorView.lineWrapping,
261 + EditorView.decorations.of((view) => {
262 + const decorations = [];
263 +
264 + for (let {from, to} of view.visibleRanges) {
265 + for (let pos = from; pos <= to;) {
266 + const line = view.state.doc.lineAt(pos);
267 + const lineText = line.text;
268 +
269 + // Calculate indentation level (count leading whitespace)
270 + let indentChars = 0;
271 + for (let i = 0; i < lineText.length; i++) {
272 + if (lineText[i] === '\t') {
273 + indentChars += 2; // Convert tab to 2 spaces for calculation
274 + } else if (lineText[i] === ' ') {
275 + indentChars += 1;
276 + } else {
277 + break;
278 + }
279 + }
280 +
281 + // Apply hanging indent if line has indentation
282 + if (indentChars > 0) {
283 + const indentDecoration = Decoration.line({
284 + attributes: {
285 + style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);`
286 + }
287 + });
288 + decorations.push(indentDecoration.range(line.from));
289 + }
290 +
291 + pos = line.to + 1;
292 + }
293 + }
294 +
295 + return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none;
296 + }),
297 + ];
298 +}
299 +
300 +function toggleLineWrapping() {
301 + enableLineWrapping = !enableLineWrapping;
302 + saveEditorSetting('editor-line-wrapping', enableLineWrapping);
303 +
304 + const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
305 +
306 + editorView.dispatch({
307 + effects: lineWrappingCompartment.reconfigure(lineWrappingExtension)
308 + });
309 +}
310 +
311 +// File operations
312 +window.saveFile = function() {
313 + const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' });
314 + const url = URL.createObjectURL(blob);
315 + const a = document.createElement('a');
316 + a.href = url;
317 + a.download = 'index.html';
318 + a.click();
319 + URL.revokeObjectURL(url);
320 +};
321 +
322 +window.loadFile = function() {
323 + const input = document.createElement('input');
324 + input.type = 'file';
325 + input.accept = '.html,.htm';
326 + input.onchange = function(event) {
327 + const file = event.target.files[0];
328 + if (!file) return;
329 +
330 + const reader = new FileReader();
331 + reader.onload = function(e) {
332 + editorView.dispatch({
333 + changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result }
334 + });
335 + saveToStorage();
336 + updatePreview();
337 + };
338 + reader.readAsText(file);
339 + };
340 + input.click();
341 +};
342 +
343 +// Wait for CodeMirror to be available
344 +function initializeCodeMirror() {
345 + if (!window.CodeMirror) {
346 + setTimeout(initializeCodeMirror, 100);
347 + return;
348 + }
349 +
350 + const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration, DecorationSet} = window.CodeMirror;
351 +
352 + // Load saved content and editor settings
353 + const savedContent = loadFromStorage();
354 + loadEditorSettings();
355 +
356 + // Create compartments for dynamic extensions
357 + lineNumbersCompartment = new Compartment();
358 + lineWrappingCompartment = new Compartment();
359 +
360 + const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : [];
361 +
362 + // Create CodeMirror editor
363 + editorView = new EditorView({
364 + state: EditorState.create({
365 + doc: savedContent,
366 + extensions: [
367 + history(),
368 + search(),
369 + closeBrackets(),
370 + keymap.of([
371 + {key: "Mod-z", run: undo},
372 + {key: "Mod-y", run: redo},
373 + {key: "Mod-Shift-z", run: redo},
374 + {key: "Mod-o", run: () => { window.loadFile(); return true; }},
375 + {key: "Mod-s", run: () => { window.saveFile(); return true; }},
376 + {key: "F1", run: () => { toggleLineNumbers(); return true; }},
377 + {key: "F2", run: () => { toggleLineWrapping(); return true; }},
378 + indentWithTab,
379 + ...searchKeymap.filter(binding => binding.key !== "Mod-f"),
380 + ...defaultKeymap
381 + ]),
382 + html(),
383 + githubDark,
384 + indentUnit.of("\t"),
385 + placeholder("Build something with HTML..."),
386 + EditorView.updateListener.of((update) => {
387 + if (update.docChanged) {
388 + clearTimeout(updateTimer);
389 + updateTimer = setTimeout(updatePreview, 600);
390 + saveToStorage();
391 + }
392 + }),
393 + // Disable text correction and autocomplete
394 + EditorView.contentAttributes.of({
395 + 'autocomplete': 'off',
396 + 'autocorrect': 'off',
397 + 'autocapitalize': 'off',
398 + 'spellcheck': 'false'
399 + }),
400 + lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []),
401 + lineWrappingCompartment.of(initialLineWrappingExtension)
402 + ]
403 + }),
404 + parent: document.getElementById('editor')
405 + });
406 +
407 + // Initial render (wait for stock images manifest to load first)
408 + stockImagesReady.then(() => updatePreview());
409 +
410 + // Track editor focus and handle keyboard dismissal
411 + editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; });
412 +
413 + editorView.contentDOM.addEventListener('blur', () => {
414 + isEditorFocused = false;
415 +
416 + // If we are on mobile and the keyboard mode is active,
417 + // exit immediately. Do not wait for visualViewport resize.
418 + if (isMobileDevice() && document.body.classList.contains('mobile-keyboard-open')) {
419 +
420 + // 1. Hide editor immediately to prevent the "stutter/jump" visual
421 + editorPane.style.opacity = '0';
422 +
423 + // 2. Remove the class to trigger layout engine (100% -> 50% height)
424 + document.body.classList.remove('mobile-keyboard-open');
425 +
426 + // 3. Wait for layout to settle, then restore scroll and opacity
427 + requestAnimationFrame(() => requestAnimationFrame(() => {
428 + if (editorView) {
429 + // Scroll cursor into view in the new 50% layout
430 + const pos = editorView.state.selection.main.head;
431 + const lineBlock = editorView.lineBlockAt(pos);
432 + const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2);
433 + editorView.scrollDOM.scrollTop = Math.max(0, targetScroll);
434 + }
435 + // Reveal the editor
436 + editorPane.style.opacity = '';
437 +
438 + // Update preview with any changes made while keyboard was open
439 + updatePreview();
440 + }));
441 + }
442 + });
443 +
444 + // Focus the editor
445 + editorView.focus();
446 +
447 + // Global keydown handler for Cmd+F toggle
448 + document.addEventListener('keydown', function(e) {
449 + if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
450 + e.preventDefault();
451 + closeSearchPanel(editorView) || openSearchPanel(editorView);
452 + }
453 + });
454 +}
455 +
456 +// Mobile keyboard detection
457 +function isMobileDevice() {
458 + return window.matchMedia("(pointer: coarse), (pointer: none)").matches;
459 +}
460 +
461 +let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
462 +let isEditorFocused = false;
463 +
464 +function updateViewportVariables() {
465 + const vv = window.visualViewport;
466 + if (vv) {
467 + document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`);
468 + document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`);
469 + }
470 +}
471 +
472 +function handleViewportChange() {
473 + if (!isMobileDevice()) return;
474 +
475 + const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
476 + const heightDifference = initialViewportHeight - currentHeight;
477 + const isKeyboardOpen = heightDifference > 150;
478 +
479 + updateViewportVariables();
480 +
481 + if (isKeyboardOpen && isEditorFocused) {
482 + document.body.classList.add('mobile-keyboard-open');
483 + } else if (!isKeyboardOpen) {
484 + // Check if the class is still there (it might have been removed by blur already)
485 + const wasKeyboardOpen = document.body.classList.contains('mobile-keyboard-open');
486 +
487 + if (wasKeyboardOpen) {
488 + // This is a fallback in case blur didn't catch it
489 + // (e.g. if the keyboard was dismissed via a gesture that didn't blur immediately)
490 + if (editorView) {
491 + const pos = editorView.state.selection.main.head;
492 + editorPane.style.opacity = '0';
493 + document.body.classList.remove('mobile-keyboard-open');
494 +
495 + requestAnimationFrame(() => requestAnimationFrame(() => {
496 + const lineBlock = editorView.lineBlockAt(pos);
497 + const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2);
498 + editorView.scrollDOM.scrollTop = Math.max(0, targetScroll);
499 + editorPane.style.opacity = '';
500 + }));
501 + } else {
502 + document.body.classList.remove('mobile-keyboard-open');
503 + }
504 + }
505 + }
506 +}
507 +
508 +if (window.visualViewport) {
509 + window.visualViewport.addEventListener('resize', handleViewportChange);
510 + window.visualViewport.addEventListener('scroll', updateViewportVariables);
511 +} else {
512 + window.addEventListener('resize', handleViewportChange);
513 +}
514 +
515 +// Initialize when page loads
516 +initializeCodeMirror();
517 +
518 +// Register service worker
519 +if ('serviceWorker' in navigator) {
520 + window.addEventListener('load', () => {
521 + navigator.serviceWorker.register('./sw.js')
522 + .then(registration => {
523 + console.log('SW registered: ', registration);
524 + })
525 + .catch(registrationError => {
526 + console.log('SW registration failed: ', registrationError);
527 + });
528 + });
529 +}
1 +:root {
2 + --text-color: #f8f8f2;
3 + --editor-bg: #16181b;
4 + scrollbar-color: grey var(--editor-bg);
5 +}
6 +
7 +* {
8 + margin: 0;
9 + padding: 0;
10 + box-sizing: border-box;
11 +}
12 +
13 +html, body {
14 + overscroll-behavior: none;
15 + background-color: var(--editor-bg);
16 +}
17 +
18 +html {
19 + height: 100%;
20 +}
21 +
22 +body {
23 + font-family: monospace;
24 + background: var(--bg-color);
25 + color: var(--text-color);
26 + height: 100%;
27 + display: flex;
28 + overflow: hidden;
29 +}
30 +
31 +.editor-pane {
32 + width: 50%;
33 + height: 100%;
34 + position: relative;
35 + transition: opacity 0.1s ease-out; /* Added transition for smoother opacity hack */
36 +}
37 +
38 +.preview-pane {
39 + width: 50%;
40 + height: 100%;
41 + position: relative;
42 +}
43 +
44 +.preview-pane.fullscreen {
45 + position: fixed;
46 + top: 0;
47 + left: 0;
48 + width: 100vw;
49 + height: 100vh;
50 + z-index: 9999;
51 + background: white;
52 +}
53 +
54 +/* Hide editor when preview is fullscreen */
55 +.editor-pane.hidden {
56 + display: none;
57 +}
58 +
59 +#editor {
60 + width: 100%;
61 + height: 100%;
62 + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
63 + font-size: 21px;
64 +}
65 +
66 +/* CodeMirror styling */
67 +.cm-editor {
68 + width: 100%;
69 + height: 100%;
70 + background: var(--editor-bg);
71 + color: var(--text-color);
72 +}
73 +
74 +.cm-placeholder {
75 + color: #666;
76 + opacity: 0.6;
77 +}
78 +
79 +.cm-content {
80 + padding: 15px;
81 + font-size: 21px;
82 + line-height: 1.6;
83 +}
84 +
85 +.cm-gutters {
86 + font-size: 21px;
87 + line-height: 1.6;
88 + user-select: none;
89 +}
90 +
91 +.cm-lineNumbers .cm-gutterElement {
92 + line-height: 1.6;
93 + font-size: 21px;
94 +}
95 +
96 +.ͼ1 .cm-lineNumbers .cm-gutterElement {
97 + padding-right: 0;
98 + padding-left: 8px;
99 +}
100 +
101 +.cm-focused {
102 + outline: none;
103 +}
104 +
105 +/* Force tab size to 2 spaces - override theme defaults */
106 +.cm-editor .cm-content,
107 +.cm-editor .cm-line,
108 +.cm-editor {
109 + tab-size: 2 !important;
110 + -moz-tab-size: 2 !important;
111 +}
112 +
113 +/* Disable overscroll on all CodeMirror elements */
114 +.cm-editor, .cm-content, .cm-scroller, .cm-scrollElement {
115 + overscroll-behavior: none !important;
116 +}
117 +
118 +/* Simplify search panel - hide all buttons, labels, and replace input */
119 +.cm-search button {
120 + display: none !important;
121 +}
122 +
123 +.cm-search {
124 + background-color: #16181b;
125 +}
126 +
127 +.cm-search label {
128 + display: none !important;
129 +}
130 +
131 +.cm-search input[name="replace"] {
132 + display: none !important;
133 +}
134 +
135 +/* Style search input to match editor text size */
136 +.cm-search input[name="search"] {
137 + font-size: 21px !important;
138 + width: 100% !important;
139 + border-radius: 50px !important;
140 + padding-left: 0.8em !important;
141 + padding-right: 0.8em !important;
142 + padding-bottom: 7px !important;
143 +}
144 +
145 +.cm-panels-bottom {
146 + border-radius: 0px !important;
147 +}
148 +
149 +.cm-panels-bottom {
150 + border-top: none !important;
151 +
152 +}
153 +
154 +#preview {
155 + width: 100%;
156 + height: 100%;
157 + border: none;
158 + background: white;
159 + overflow: auto;
160 +}
161 +
162 +/* Vertical layout for narrow screens (height > width) */
163 +@media (max-aspect-ratio: 1/1) {
164 + body {
165 + flex-direction: column;
166 + }
167 +
168 + .editor-pane {
169 + width: 100%;
170 + height: 50%;
171 + order: 2;
172 + }
173 +
174 + .preview-pane {
175 + width: 100%;
176 + height: 50%;
177 + order: 1;
178 + padding-top: env(safe-area-inset-top);
179 + }
180 +}
181 +
182 +/* Mobile: fullscreen editor when keyboard is open */
183 +@media (pointer: coarse), (pointer: none) {
184 + body.mobile-keyboard-open .preview-pane {
185 + display: none;
186 + }
187 +
188 + body.mobile-keyboard-open .editor-pane {
189 + position: fixed;
190 + top: calc(var(--visual-viewport-offset-top, 0px));
191 + left: 0;
192 + width: 100%;
193 + height: var(--visual-viewport-height, 100%);
194 + max-height: var(--visual-viewport-height, 100%);
195 + order: 1;
196 + overscroll-behavior: none;
197 + overflow: hidden;
198 + }
199 +
200 + body.mobile-keyboard-open .editor-pane #editor,
201 + body.mobile-keyboard-open .editor-pane .cm-editor {
202 + height: 100% !important;
203 + max-height: 100% !important;
204 + }
205 +}