diff --git a/docs/public/editor/editor.js b/docs/public/editor/editor.js new file mode 100644 index 00000000000..202e8cded22 --- /dev/null +++ b/docs/public/editor/editor.js @@ -0,0 +1,323 @@ +// ================================================================ +// gh-aw Playground - Application Logic +// ================================================================ + +import { createWorkerCompiler } from '/gh-aw/wasm/compiler-loader.js'; + +// --------------------------------------------------------------- +// Default workflow content +// --------------------------------------------------------------- +const DEFAULT_CONTENT = `--- +name: hello-world +description: A simple hello world workflow +on: + workflow_dispatch: +engine: copilot +--- + +# Mission + +Say hello to the world! Check the current date and time, and greet the user warmly. +`; + +// --------------------------------------------------------------- +// DOM Elements +// --------------------------------------------------------------- +const $ = (id) => document.getElementById(id); + +const editor = $('editor'); +const outputPre = $('outputPre'); +const outputPlaceholder = $('outputPlaceholder'); +const compileBtn = $('compileBtn'); +const copyBtn = $('copyBtn'); +const statusBadge = $('statusBadge'); +const statusText = $('statusText'); +const loadingOverlay = $('loadingOverlay'); +const errorBanner = $('errorBanner'); +const errorText = $('errorText'); +const warningBanner = $('warningBanner'); +const warningText = $('warningText'); +const lineNumbers = $('lineNumbers'); +const lineNumbersInner = $('lineNumbersInner'); +const themeToggle = $('themeToggle'); +const toggleTrack = $('toggleTrack'); +const divider = $('divider'); +const panelEditor = $('panelEditor'); +const panelOutput = $('panelOutput'); +const panels = $('panels'); + +// --------------------------------------------------------------- +// State +// --------------------------------------------------------------- +let compiler = null; +let isReady = false; +let isCompiling = false; +let autoCompile = true; +let compileTimer = null; +let currentYaml = ''; + +// --------------------------------------------------------------- +// Theme +// --------------------------------------------------------------- +function getPreferredTheme() { + const saved = localStorage.getItem('gh-aw-playground-theme'); + if (saved) return saved; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('gh-aw-playground-theme', theme); + const sunIcon = themeToggle.querySelector('.icon-sun'); + const moonIcon = themeToggle.querySelector('.icon-moon'); + sunIcon.style.display = theme === 'dark' ? 'block' : 'none'; + moonIcon.style.display = theme === 'dark' ? 'none' : 'block'; +} + +setTheme(getPreferredTheme()); + +themeToggle.addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme'); + setTheme(current === 'dark' ? 'light' : 'dark'); +}); + +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!localStorage.getItem('gh-aw-playground-theme')) { + setTheme(e.matches ? 'dark' : 'light'); + } +}); + +// --------------------------------------------------------------- +// Keyboard shortcut hint (Mac vs other) +// --------------------------------------------------------------- +const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; +document.querySelectorAll('.kbd-hint-mac').forEach(el => el.style.display = isMac ? 'inline' : 'none'); +document.querySelectorAll('.kbd-hint-other').forEach(el => el.style.display = isMac ? 'none' : 'inline'); + +// --------------------------------------------------------------- +// Status +// --------------------------------------------------------------- +function setStatus(status, text) { + statusBadge.setAttribute('data-status', status); + statusText.textContent = text; +} + +// --------------------------------------------------------------- +// Line numbers +// --------------------------------------------------------------- +function updateLineNumbers() { + const lines = editor.value.split('\n').length; + let html = ''; + for (let i = 1; i <= lines; i++) html += '
' + i + '
'; + lineNumbersInner.innerHTML = html; +} + +function syncLineNumberScroll() { + lineNumbers.scrollTop = editor.scrollTop; +} + +// --------------------------------------------------------------- +// Editor setup +// --------------------------------------------------------------- +editor.value = DEFAULT_CONTENT; +updateLineNumbers(); + +editor.addEventListener('input', () => { + updateLineNumbers(); + if (autoCompile && isReady) scheduleCompile(); +}); + +editor.addEventListener('scroll', syncLineNumberScroll); + +editor.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + const start = editor.selectionStart; + const end = editor.selectionEnd; + editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end); + editor.selectionStart = editor.selectionEnd = start + 2; + editor.dispatchEvent(new Event('input')); + } + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + doCompile(); + } +}); + +// --------------------------------------------------------------- +// Auto-compile toggle +// --------------------------------------------------------------- +$('autoCompileToggle').addEventListener('click', () => { + autoCompile = !autoCompile; + toggleTrack.classList.toggle('active', autoCompile); +}); + +// --------------------------------------------------------------- +// Compile +// --------------------------------------------------------------- +function scheduleCompile() { + if (compileTimer) clearTimeout(compileTimer); + compileTimer = setTimeout(doCompile, 400); +} + +async function doCompile() { + if (!isReady || isCompiling) return; + if (compileTimer) { clearTimeout(compileTimer); compileTimer = null; } + + const md = editor.value; + if (!md.trim()) { + outputPre.style.display = 'none'; + outputPlaceholder.style.display = 'flex'; + outputPlaceholder.textContent = 'Compiled YAML will appear here'; + currentYaml = ''; + copyBtn.disabled = true; + return; + } + + isCompiling = true; + setStatus('compiling', 'Compiling...'); + compileBtn.disabled = true; + errorBanner.classList.remove('visible'); + warningBanner.classList.remove('visible'); + + try { + const result = await compiler.compile(md); + + if (result.error) { + setStatus('error', 'Error'); + errorText.textContent = result.error; + errorBanner.classList.add('visible'); + } else { + setStatus('ready', 'Ready'); + currentYaml = result.yaml; + outputPre.textContent = result.yaml; + outputPre.style.display = 'block'; + outputPlaceholder.style.display = 'none'; + copyBtn.disabled = false; + + if (result.warnings && result.warnings.length > 0) { + warningText.textContent = result.warnings.join('\n'); + warningBanner.classList.add('visible'); + } + } + } catch (err) { + setStatus('error', 'Error'); + errorText.textContent = err.message || String(err); + errorBanner.classList.add('visible'); + } finally { + isCompiling = false; + compileBtn.disabled = !isReady; + } +} + +compileBtn.addEventListener('click', doCompile); + +// --------------------------------------------------------------- +// Copy YAML +// --------------------------------------------------------------- +function showCopyFeedback() { + const feedback = $('copyFeedback'); + feedback.classList.add('show'); + setTimeout(() => feedback.classList.remove('show'), 2000); +} + +copyBtn.addEventListener('click', async () => { + if (!currentYaml) return; + try { + await navigator.clipboard.writeText(currentYaml); + } catch { + const ta = document.createElement('textarea'); + ta.value = currentYaml; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + showCopyFeedback(); +}); + +// --------------------------------------------------------------- +// Banner close +// --------------------------------------------------------------- +$('errorClose').addEventListener('click', () => errorBanner.classList.remove('visible')); +$('warningClose').addEventListener('click', () => warningBanner.classList.remove('visible')); + +// --------------------------------------------------------------- +// Draggable divider +// --------------------------------------------------------------- +let isDragging = false; + +function resizePanels(clientX, clientY) { + const rect = panels.getBoundingClientRect(); + const isMobile = window.innerWidth < 768; + const pos = isMobile ? clientY - rect.top : clientX - rect.left; + const size = isMobile ? rect.height : rect.width; + const clamped = Math.max(0.2, Math.min(0.8, pos / size)); + panelEditor.style.flex = `0 0 ${clamped * 100}%`; + panelOutput.style.flex = `0 0 ${(1 - clamped) * 100}%`; +} + +divider.addEventListener('mousedown', (e) => { + isDragging = true; + divider.classList.add('dragging'); + const isMobile = window.innerWidth < 768; + document.body.style.cursor = isMobile ? 'row-resize' : 'col-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); +}); + +document.addEventListener('mousemove', (e) => { + if (isDragging) resizePanels(e.clientX, e.clientY); +}); + +document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + divider.classList.remove('dragging'); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } +}); + +divider.addEventListener('touchstart', (e) => { + isDragging = true; + divider.classList.add('dragging'); + e.preventDefault(); +}); + +document.addEventListener('touchmove', (e) => { + if (isDragging) resizePanels(e.touches[0].clientX, e.touches[0].clientY); +}); + +document.addEventListener('touchend', () => { + if (isDragging) { + isDragging = false; + divider.classList.remove('dragging'); + } +}); + +// --------------------------------------------------------------- +// Initialize compiler +// --------------------------------------------------------------- +async function init() { + try { + compiler = createWorkerCompiler({ + workerUrl: '/gh-aw/wasm/compiler-worker.js' + }); + + await compiler.ready; + isReady = true; + setStatus('ready', 'Ready'); + compileBtn.disabled = false; + loadingOverlay.classList.add('hidden'); + + if (autoCompile) doCompile(); + } catch (err) { + setStatus('error', 'Failed to load'); + loadingOverlay.querySelector('.loading-text').textContent = 'Failed to load compiler'; + loadingOverlay.querySelector('.loading-subtext').textContent = err.message; + loadingOverlay.querySelector('.loading-spinner').style.display = 'none'; + } +} + +init(); diff --git a/docs/public/editor/index.html b/docs/public/editor/index.html index 7b167199e3d..203360664e4 100644 --- a/docs/public/editor/index.html +++ b/docs/public/editor/index.html @@ -9,7 +9,6 @@ CSS Custom Properties ============================================================ */ :root { - /* Light theme (default) */ --bg-primary: #ffffff; --bg-secondary: #f6f8fa; --bg-tertiary: #eaeef2; @@ -44,7 +43,6 @@ --success-text: #116329; --shadow-sm: 0 1px 2px rgba(27,31,36,0.04); - --shadow-md: 0 3px 6px rgba(140,149,159,0.15); --shadow-lg: 0 8px 24px rgba(140,149,159,0.2); --divider-color: #d1d9e0; @@ -67,8 +65,6 @@ --radius-sm: 6px; --radius-md: 8px; - --radius-lg: 12px; - --transition: 180ms ease; } @@ -107,7 +103,6 @@ --success-text: #3fb950; --shadow-sm: 0 1px 2px rgba(0,0,0,0.2); - --shadow-md: 0 3px 6px rgba(0,0,0,0.3); --shadow-lg: 0 8px 24px rgba(0,0,0,0.4); --divider-color: #30363d; @@ -141,6 +136,10 @@ } body { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; font-family: var(--font-sans); background: var(--bg-primary); color: var(--text-primary); @@ -149,16 +148,6 @@ -moz-osx-font-smoothing: grayscale; } -/* ============================================================ - App Layout - ============================================================ */ -.app { - display: flex; - flex-direction: column; - height: 100vh; - width: 100vw; -} - /* ============================================================ Header ============================================================ */ @@ -187,9 +176,7 @@ text-decoration: none; } -.header-logo svg { - flex-shrink: 0; -} +.header-logo svg { flex-shrink: 0; } .header-separator { width: 1px; @@ -249,9 +236,7 @@ border-color: var(--border-primary); } -.btn-secondary:hover { - background: var(--bg-hover); -} +.btn-secondary:hover { background: var(--bg-hover); } .btn svg { width: 16px; @@ -272,7 +257,8 @@ transition: all var(--transition); } -.status-badge[data-status="loading"] { +.status-badge[data-status="loading"], +.status-badge[data-status="compiling"] { background: var(--accent-subtle); color: var(--accent); } @@ -282,11 +268,6 @@ color: var(--success-text); } -.status-badge[data-status="compiling"] { - background: var(--accent-subtle); - color: var(--accent); -} - .status-badge[data-status="error"] { background: var(--error-bg); color: var(--error-text); @@ -332,9 +313,7 @@ flex-shrink: 0; } -.toggle-track.active { - background: var(--toggle-bg-active); -} +.toggle-track.active { background: var(--toggle-bg-active); } .toggle-knob { position: absolute; @@ -348,9 +327,7 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.2); } -.toggle-track.active .toggle-knob { - transform: translateX(14px); -} +.toggle-track.active .toggle-knob { transform: translateX(14px); } /* Theme toggle */ .theme-toggle { @@ -383,7 +360,7 @@ } /* ============================================================ - Error/Warning Banner + Banners (error/warning) ============================================================ */ .banner { display: none; @@ -396,9 +373,7 @@ transition: all var(--transition); } -.banner.visible { - display: flex; -} +.banner.visible { display: flex; } .banner-error { background: var(--error-bg); @@ -441,9 +416,7 @@ transition: opacity var(--transition); } -.banner-close:hover { - opacity: 1; -} +.banner-close:hover { opacity: 1; } /* ============================================================ Panels @@ -462,10 +435,7 @@ min-height: 0; } -.panel-editor { - flex: 1 1 50%; -} - +.panel-editor, .panel-output { flex: 1 1 50%; } @@ -474,6 +444,7 @@ display: flex; align-items: center; justify-content: space-between; + gap: 6px; padding: 8px 16px; font-size: 12px; font-weight: 600; @@ -487,13 +458,7 @@ transition: background var(--transition), color var(--transition), border-color var(--transition); } -.panel-header-label { - display: flex; - align-items: center; - gap: 6px; -} - -.panel-header-label svg { +.panel-header svg { width: 14px; height: 14px; opacity: 0.6; @@ -513,7 +478,6 @@ justify-content: center; } -/* Larger hit area for easier grabbing */ .divider::before { content: ''; position: absolute; @@ -523,7 +487,6 @@ bottom: 0; } -/* Grip dots */ .divider::after { content: ''; width: 3px; @@ -535,14 +498,10 @@ } .divider:hover::after, -.divider.dragging::after { - opacity: 0.5; -} +.divider.dragging::after { opacity: 0.5; } .divider:hover, -.divider.dragging { - background: var(--divider-hover); -} +.divider.dragging { background: var(--divider-hover); } /* ============================================================ Editor (left panel) @@ -602,27 +561,7 @@ transition: background var(--transition), color var(--transition); } -.editor-textarea::placeholder { - color: var(--text-muted); -} - -.editor-textarea::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -.editor-textarea::-webkit-scrollbar-track { - background: var(--scrollbar-track); -} - -.editor-textarea::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb); - border-radius: 4px; -} - -.editor-textarea::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-hover); -} +.editor-textarea::placeholder { color: var(--text-muted); } /* ============================================================ Output (right panel) @@ -658,20 +597,25 @@ text-align: center; } +/* Scrollbars (shared) */ +.editor-textarea::-webkit-scrollbar, .output-container::-webkit-scrollbar { width: 8px; height: 8px; } +.editor-textarea::-webkit-scrollbar-track, .output-container::-webkit-scrollbar-track { background: var(--scrollbar-track); } +.editor-textarea::-webkit-scrollbar-thumb, .output-container::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 4px; } +.editor-textarea::-webkit-scrollbar-thumb:hover, .output-container::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } @@ -725,7 +669,7 @@ } /* ============================================================ - Copy feedback + Copy feedback toast ============================================================ */ .copy-feedback { position: fixed; @@ -754,9 +698,7 @@ Responsive (<768px) ============================================================ */ @media (max-width: 767px) { - .panels { - flex-direction: column; - } + .panels { flex-direction: column; } .divider { width: 100%; @@ -764,11 +706,6 @@ cursor: row-resize; } - .panel-editor, - .panel-output { - flex: 1 1 50%; - } - .header { gap: 8px; padding: 0 12px; @@ -779,18 +716,11 @@ padding-bottom: 8px; } - .header-separator { - display: none; - } - - .auto-compile-label-text { - display: none; - } + .header-separator { display: none; } + .auto-compile-label-text { display: none; } } -/* ============================================================ - Keyboard hint - ============================================================ */ +/* Keyboard hint */ .kbd { display: inline-block; padding: 1px 5px; @@ -805,7 +735,6 @@ -
@@ -854,11 +783,9 @@
- +