diff --git a/docs/public/editor/editor.js b/docs/public/editor/editor.js
index 7727a7995e3..119883fbe9b 100644
--- a/docs/public/editor/editor.js
+++ b/docs/public/editor/editor.js
@@ -2,51 +2,57 @@
// gh-aw Playground - Application Logic
// ================================================================
+import { EditorView, basicSetup } from 'https://esm.sh/codemirror@6.0.2';
+import { EditorState, Compartment } from 'https://esm.sh/@codemirror/state@6.5.4';
+import { keymap } from 'https://esm.sh/@codemirror/view@6.39.14';
+import { yaml } from 'https://esm.sh/@codemirror/lang-yaml@6.1.2';
+import { markdown } from 'https://esm.sh/@codemirror/lang-markdown@6.5.0';
+import { indentUnit } from 'https://esm.sh/@codemirror/language@6.12.1';
+import { oneDark } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3';
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.
-`;
+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.',
+ ''
+].join('\n');
// ---------------------------------------------------------------
// DOM Elements
// ---------------------------------------------------------------
const $ = (id) => document.getElementById(id);
-const editor = $('editor');
-const outputPre = $('outputPre');
+const editorMount = $('editorMount');
const outputPlaceholder = $('outputPlaceholder');
+const outputMount = $('outputMount');
+const outputContainer = $('outputContainer');
const compileBtn = $('compileBtn');
-const copyBtn = $('copyBtn');
const statusBadge = $('statusBadge');
const statusText = $('statusText');
+const statusDot = $('statusDot');
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');
-const tabBar = $('tabBar');
-const tabAdd = $('tabAdd');
// ---------------------------------------------------------------
// State
@@ -58,36 +64,91 @@ let autoCompile = true;
let compileTimer = null;
let currentYaml = '';
-// File tabs state: ordered list of { name, content }
-const MAIN_FILE = 'workflow.md';
-let files = [{ name: MAIN_FILE, content: DEFAULT_CONTENT }];
-let activeTab = MAIN_FILE;
-
// ---------------------------------------------------------------
-// Theme
+// Theme (uses Primer's data-color-mode)
// ---------------------------------------------------------------
+const editorThemeConfig = new Compartment();
+const outputThemeConfig = new Compartment();
+
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 cmThemeFor(theme) {
+ return theme === 'dark' ? oneDark : [];
+}
+
function setTheme(theme) {
- document.documentElement.setAttribute('data-theme', theme);
+ document.documentElement.setAttribute('data-color-mode', 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';
+ if (theme === 'dark') {
+ sunIcon.style.display = 'block';
+ moonIcon.style.display = 'none';
+ } else {
+ sunIcon.style.display = 'none';
+ moonIcon.style.display = 'block';
+ }
+
+ // Update CodeMirror themes
+ const cmTheme = cmThemeFor(theme);
+ editorView.dispatch({ effects: editorThemeConfig.reconfigure(cmTheme) });
+ outputView.dispatch({ effects: outputThemeConfig.reconfigure(cmTheme) });
}
+// ---------------------------------------------------------------
+// CodeMirror: Input Editor (Markdown with YAML frontmatter)
+// ---------------------------------------------------------------
+const editorView = new EditorView({
+ doc: DEFAULT_CONTENT,
+ extensions: [
+ basicSetup,
+ markdown(),
+ EditorState.tabSize.of(2),
+ indentUnit.of(' '),
+ editorThemeConfig.of(cmThemeFor(getPreferredTheme())),
+ keymap.of([{
+ key: 'Mod-Enter',
+ run: () => { doCompile(); return true; }
+ }]),
+ EditorView.updateListener.of(update => {
+ if (update.docChanged && autoCompile && isReady) {
+ scheduleCompile();
+ }
+ }),
+ ],
+ parent: editorMount,
+});
+
+// ---------------------------------------------------------------
+// CodeMirror: Output View (YAML, read-only)
+// ---------------------------------------------------------------
+const outputView = new EditorView({
+ doc: '',
+ extensions: [
+ basicSetup,
+ yaml(),
+ EditorState.readOnly.of(true),
+ EditorView.editable.of(false),
+ outputThemeConfig.of(cmThemeFor(getPreferredTheme())),
+ ],
+ parent: outputMount,
+});
+
+// ---------------------------------------------------------------
+// Apply initial theme + listen for changes
+// ---------------------------------------------------------------
setTheme(getPreferredTheme());
themeToggle.addEventListener('click', () => {
- const current = document.documentElement.getAttribute('data-theme');
+ const current = document.documentElement.getAttribute('data-color-mode');
setTheme(current === 'dark' ? 'light' : 'dark');
});
+// Listen for OS theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('gh-aw-playground-theme')) {
setTheme(e.matches ? 'dark' : 'light');
@@ -102,150 +163,40 @@ document.querySelectorAll('.kbd-hint-mac').forEach(el => el.style.display = isMa
document.querySelectorAll('.kbd-hint-other').forEach(el => el.style.display = isMac ? 'none' : 'inline');
// ---------------------------------------------------------------
-// Status
+// Status (uses Primer Label component)
// ---------------------------------------------------------------
+const STATUS_LABEL_MAP = {
+ loading: 'Label--accent',
+ ready: 'Label--success',
+ compiling: 'Label--accent',
+ error: 'Label--danger'
+};
+
function setStatus(status, text) {
+ // Swap Label modifier class
+ Object.values(STATUS_LABEL_MAP).forEach(cls => statusBadge.classList.remove(cls));
+ statusBadge.classList.add(STATUS_LABEL_MAP[status] || 'Label--secondary');
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;
-}
-
-// ---------------------------------------------------------------
-// File tabs
-// ---------------------------------------------------------------
-function getFile(name) {
- return files.find(f => f.name === name);
-}
-
-function renderTabs() {
- tabBar.querySelectorAll('.tab').forEach(el => el.remove());
-
- for (const file of files) {
- const tab = document.createElement('div');
- tab.className = 'tab' + (file.name === activeTab ? ' active' : '');
- tab.dataset.name = file.name;
-
- const label = document.createElement('span');
- label.textContent = file.name;
- tab.appendChild(label);
-
- if (file.name !== MAIN_FILE) {
- const close = document.createElement('button');
- close.className = 'tab-close';
- close.title = 'Remove file';
- close.innerHTML = '';
- close.addEventListener('click', (e) => {
- e.stopPropagation();
- removeTab(file.name);
- });
- tab.appendChild(close);
- }
-
- tab.addEventListener('click', () => switchTab(file.name));
- tabBar.insertBefore(tab, tabAdd);
- }
-}
-
-function switchTab(name) {
- const current = getFile(activeTab);
- if (current) current.content = editor.value;
-
- activeTab = name;
- const file = getFile(name);
- if (file) {
- editor.value = file.content;
- updateLineNumbers();
- }
- renderTabs();
-}
-
-function addTab() {
- const name = prompt('File path (e.g. shared/my-tools.md):');
- if (!name || !name.trim()) return;
-
- const trimmed = name.trim();
- if (getFile(trimmed)) { switchTab(trimmed); return; }
-
- const defaultImportContent = `---
-# Shared workflow component
-# This file can define: tools, steps, engine, mcp-servers, etc.
-tools:
- - name: example_tool
- description: An example tool
----
-
-# Instructions
-
-Add your shared workflow instructions here.
-`;
- files.push({ name: trimmed, content: defaultImportContent });
- switchTab(trimmed);
-}
-
-function removeTab(name) {
- if (name === MAIN_FILE) return;
- files = files.filter(f => f.name !== name);
- if (activeTab === name) {
- switchTab(MAIN_FILE);
+ // Pulse animation for loading/compiling states
+ if (status === 'loading' || status === 'compiling') {
+ statusDot.style.animation = 'pulse 1.2s ease-in-out infinite';
} else {
- renderTabs();
+ statusDot.style.animation = '';
}
- if (autoCompile && isReady) scheduleCompile();
}
-tabAdd.addEventListener('click', addTab);
-
-// ---------------------------------------------------------------
-// Editor setup
-// ---------------------------------------------------------------
-editor.value = DEFAULT_CONTENT;
-updateLineNumbers();
-renderTabs();
-
-editor.addEventListener('input', () => {
- updateLineNumbers();
- const file = getFile(activeTab);
- if (file) file.content = editor.value;
- 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);
+ if (autoCompile) {
+ toggleTrack.classList.add('active');
+ } else {
+ toggleTrack.classList.remove('active');
+ }
});
// ---------------------------------------------------------------
@@ -256,65 +207,57 @@ function scheduleCompile() {
compileTimer = setTimeout(doCompile, 400);
}
-function getImportFiles() {
- const importFiles = {};
- for (const file of files) {
- if (file.name !== MAIN_FILE) importFiles[file.name] = file.content;
- }
- return Object.keys(importFiles).length > 0 ? importFiles : undefined;
-}
-
async function doCompile() {
if (!isReady || isCompiling) return;
- if (compileTimer) { clearTimeout(compileTimer); compileTimer = null; }
-
- // Save current editor content
- const currentFile = getFile(activeTab);
- if (currentFile) currentFile.content = editor.value;
+ if (compileTimer) {
+ clearTimeout(compileTimer);
+ compileTimer = null;
+ }
- // Get the main workflow content
- const mainFile = getFile(MAIN_FILE);
- const md = mainFile ? mainFile.content : '';
+ const md = editorView.state.doc.toString();
if (!md.trim()) {
- outputPre.style.display = 'none';
+ outputMount.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');
+
+ // Hide old banners
+ errorBanner.classList.add('d-none');
+ warningBanner.classList.add('d-none');
try {
- const importFiles = getImportFiles();
- const result = await compiler.compile(md, importFiles);
+ const result = await compiler.compile(md);
if (result.error) {
setStatus('error', 'Error');
errorText.textContent = result.error;
- errorBanner.classList.add('visible');
+ errorBanner.classList.remove('d-none');
} else {
setStatus('ready', 'Ready');
currentYaml = result.yaml;
- outputPre.textContent = result.yaml;
- outputPre.style.display = 'block';
+
+ // Update output CodeMirror view
+ outputView.dispatch({
+ changes: { from: 0, to: outputView.state.doc.length, insert: result.yaml }
+ });
+ outputMount.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');
+ warningBanner.classList.remove('d-none');
}
}
} catch (err) {
setStatus('error', 'Error');
errorText.textContent = err.message || String(err);
- errorBanner.classList.add('visible');
+ errorBanner.classList.remove('d-none');
} finally {
isCompiling = false;
compileBtn.disabled = !isReady;
@@ -323,62 +266,41 @@ async function doCompile() {
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'));
+$('errorClose').addEventListener('click', () => errorBanner.classList.add('d-none'));
+$('warningClose').addEventListener('click', () => warningBanner.classList.add('d-none'));
// ---------------------------------------------------------------
// 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.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
- if (isDragging) resizePanels(e.clientX, e.clientY);
+ if (!isDragging) return;
+ const rect = panels.getBoundingClientRect();
+ const isMobile = window.innerWidth < 768;
+
+ if (isMobile) {
+ const fraction = (e.clientY - rect.top) / rect.height;
+ const clamped = Math.max(0.2, Math.min(0.8, fraction));
+ panelEditor.style.flex = `0 0 ${clamped * 100}%`;
+ panelOutput.style.flex = `0 0 ${(1 - clamped) * 100}%`;
+ } else {
+ const fraction = (e.clientX - rect.left) / rect.width;
+ const clamped = Math.max(0.2, Math.min(0.8, fraction));
+ panelEditor.style.flex = `0 0 ${clamped * 100}%`;
+ panelOutput.style.flex = `0 0 ${(1 - clamped) * 100}%`;
+ }
});
document.addEventListener('mouseup', () => {
@@ -390,6 +312,7 @@ document.addEventListener('mouseup', () => {
}
});
+// Touch support for mobile divider
divider.addEventListener('touchstart', (e) => {
isDragging = true;
divider.classList.add('dragging');
@@ -397,7 +320,22 @@ divider.addEventListener('touchstart', (e) => {
});
document.addEventListener('touchmove', (e) => {
- if (isDragging) resizePanels(e.touches[0].clientX, e.touches[0].clientY);
+ if (!isDragging) return;
+ const touch = e.touches[0];
+ const rect = panels.getBoundingClientRect();
+ const isMobile = window.innerWidth < 768;
+
+ if (isMobile) {
+ const fraction = (touch.clientY - rect.top) / rect.height;
+ const clamped = Math.max(0.2, Math.min(0.8, fraction));
+ panelEditor.style.flex = `0 0 ${clamped * 100}%`;
+ panelOutput.style.flex = `0 0 ${(1 - clamped) * 100}%`;
+ } else {
+ const fraction = (touch.clientX - rect.left) / rect.width;
+ const clamped = Math.max(0.2, Math.min(0.8, fraction));
+ panelEditor.style.flex = `0 0 ${clamped * 100}%`;
+ panelOutput.style.flex = `0 0 ${(1 - clamped) * 100}%`;
+ }
});
document.addEventListener('touchend', () => {
@@ -422,11 +360,14 @@ async function init() {
compileBtn.disabled = false;
loadingOverlay.classList.add('hidden');
- if (autoCompile) doCompile();
+ // Auto-compile the default content
+ 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('.f4').textContent = 'Failed to load compiler';
+ loadingOverlay.querySelector('.f6').textContent = err.message;
loadingOverlay.querySelector('.loading-spinner').style.display = 'none';
}
}
diff --git a/docs/public/editor/index.html b/docs/public/editor/index.html
index 6ad25d9144b..981e57a6972 100644
--- a/docs/public/editor/index.html
+++ b/docs/public/editor/index.html
@@ -286,382 +286,6 @@
-
+