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 @@
-
+