Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 123 additions & 2 deletions docs/public/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ const divider = $('divider');
const panelEditor = $('panelEditor');
const panelOutput = $('panelOutput');
const panels = $('panels');
const tabBar = $('tabBar');
const tabStatusDot = $('tabStatusDot');
const fabCompile = $('fabCompile');

// ---------------------------------------------------------------
// State
Expand All @@ -136,6 +139,10 @@ let isCompiling = false;
let compileTimer = null;
let currentYaml = '';
let pendingCompile = false;
let activeTab = 'editor'; // 'editor' | 'output'
let outputIsStale = false; // true when editor changed since last compile
let lastCompileStatus = 'ok'; // 'ok' | 'error'
let isDragging = false; // divider drag state (used by both divider + swipe logic)

// ---------------------------------------------------------------
// Theme — follows browser's prefers-color-scheme automatically.
Expand Down Expand Up @@ -182,6 +189,9 @@ const editorView = new EditorView({
if (update.docChanged) {
try { localStorage.setItem(STORAGE_KEY, update.state.doc.toString()); }
catch (_) { /* localStorage full or unavailable */ }
// Mark output as stale (editor changed since last compile)
outputIsStale = true;
updateTabStatusDot();
if (isReady) {
scheduleCompile();
} else {
Expand Down Expand Up @@ -356,6 +366,7 @@ async function doCompile() {

isCompiling = true;
setStatus('compiling', 'Compiling...');
if (fabCompile) fabCompile.classList.add('compiling');

// Hide old banners
errorBanner.classList.add('d-none');
Expand All @@ -366,10 +377,15 @@ async function doCompile() {

if (result.error) {
setStatus('error', 'Error');
lastCompileStatus = 'error';
updateTabStatusDot();
errorText.textContent = result.error;
errorBanner.classList.remove('d-none');
} else {
setStatus('ready', 'Ready');
lastCompileStatus = 'ok';
outputIsStale = false;
updateTabStatusDot();
currentYaml = result.yaml;

// Update output CodeMirror view
Expand All @@ -387,10 +403,13 @@ async function doCompile() {
}
} catch (err) {
setStatus('error', 'Error');
lastCompileStatus = 'error';
updateTabStatusDot();
errorText.textContent = err.message || String(err);
errorBanner.classList.remove('d-none');
} finally {
isCompiling = false;
if (fabCompile) fabCompile.classList.remove('compiling');
}
}

Expand All @@ -401,10 +420,112 @@ $('errorClose').addEventListener('click', () => errorBanner.classList.add('d-non
$('warningClose').addEventListener('click', () => warningBanner.classList.add('d-none'));

// ---------------------------------------------------------------
// Draggable divider
// Mobile: Tab-based layout
// ---------------------------------------------------------------
let isDragging = false;
const mobileMq = window.matchMedia('(max-width: 767px)');

/** Check if currently in mobile layout */
function isMobileLayout() {
return mobileMq.matches;
}

/** Switch the active mobile tab */
function switchTab(tab) {
activeTab = tab;

// Update tab button states
tabBar.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.panel === tab);
});

// Show/hide panels
if (tab === 'editor') {
panelEditor.style.display = '';
panelOutput.style.display = 'none';
} else {
panelEditor.style.display = 'none';
panelOutput.style.display = '';
}
}

/** Update the status dot on the Output tab */
function updateTabStatusDot() {
if (!tabStatusDot) return;
if (lastCompileStatus === 'error') {
tabStatusDot.setAttribute('data-stale', 'error');
} else if (outputIsStale) {
tabStatusDot.setAttribute('data-stale', 'true');
} else {
tabStatusDot.removeAttribute('data-stale');
}
}

/** Apply or revert mobile layout depending on viewport width */
function applyResponsiveLayout() {
if (isMobileLayout()) {
// Enter mobile mode: show only the active tab's panel
switchTab(activeTab);
} else {
// Exit mobile mode: show both panels, restore flex
panelEditor.style.display = '';
panelOutput.style.display = '';
panelEditor.style.flex = '';
panelOutput.style.flex = '';
}
}

// Tab button click handlers
tabBar.addEventListener('click', (e) => {
const btn = e.target.closest('.tab-btn');
if (!btn || !isMobileLayout()) return;
switchTab(btn.dataset.panel);
});

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab interface lacks keyboard navigation support. Users should be able to switch between tabs using arrow keys (left/right) when focus is on a tab button, following the ARIA Authoring Practices Guide for tabs. Consider adding keydown event handlers for ArrowLeft and ArrowRight keys to improve keyboard accessibility.

Suggested change
// Tab button keyboard navigation (ArrowLeft / ArrowRight)
tabBar.addEventListener('keydown', (e) => {
const currentBtn = e.target.closest('.tab-btn');
if (!currentBtn) return;
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') {
return;
}
e.preventDefault();
const tabs = Array.from(tabBar.querySelectorAll('.tab-btn'));
const currentIndex = tabs.indexOf(currentBtn);
if (currentIndex === -1) return;
let nextIndex;
if (e.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length;
} else {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
const nextBtn = tabs[nextIndex];
if (!nextBtn) return;
nextBtn.focus();
if (isMobileLayout()) {
switchTab(nextBtn.dataset.panel);
}
});

Copilot uses AI. Check for mistakes.
// FAB compile button
fabCompile.addEventListener('click', () => {
doCompile();
});

// Swipe gesture support on panels container
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;

panels.addEventListener('touchstart', (e) => {
// Only handle swipe gestures in mobile tab mode and when not dragging the divider
if (!isMobileLayout() || isDragging) return;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
}, { passive: true });

panels.addEventListener('touchend', (e) => {
if (!isMobileLayout() || isDragging) return;
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
const dt = Date.now() - touchStartTime;

// Require: horizontal distance > 50px, more horizontal than vertical, within 500ms
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5 && dt < 500) {
if (dx < 0 && activeTab === 'editor') {
// Swipe left: go to Output
switchTab('output');
} else if (dx > 0 && activeTab === 'output') {
// Swipe right: go to Editor
switchTab('editor');
}
}
}, { passive: true });
Comment on lines +485 to +518
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FAB compile button event listener is added without checking if the element exists. While the element is guaranteed to exist in the HTML, adding a null check (similar to the pattern used in updateTabStatusDot at line 453 and in doCompile at lines 369 and 412) would make this code more defensive and consistent with the rest of the codebase.

This issue also appears on line 502 of the same file.

Suggested change
fabCompile.addEventListener('click', () => {
doCompile();
});
// Swipe gesture support on panels container
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
panels.addEventListener('touchstart', (e) => {
// Only handle swipe gestures in mobile tab mode and when not dragging the divider
if (!isMobileLayout() || isDragging) return;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
}, { passive: true });
panels.addEventListener('touchend', (e) => {
if (!isMobileLayout() || isDragging) return;
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
const dt = Date.now() - touchStartTime;
// Require: horizontal distance > 50px, more horizontal than vertical, within 500ms
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5 && dt < 500) {
if (dx < 0 && activeTab === 'editor') {
// Swipe left: go to Output
switchTab('output');
} else if (dx > 0 && activeTab === 'output') {
// Swipe right: go to Editor
switchTab('editor');
}
}
}, { passive: true });
if (fabCompile) {
fabCompile.addEventListener('click', () => {
doCompile();
});
}
// Swipe gesture support on panels container
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
if (panels) {
panels.addEventListener('touchstart', (e) => {
// Only handle swipe gestures in mobile tab mode and when not dragging the divider
if (!isMobileLayout() || isDragging) return;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
}, { passive: true });
panels.addEventListener('touchend', (e) => {
if (!isMobileLayout() || isDragging) return;
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
const dt = Date.now() - touchStartTime;
// Require: horizontal distance > 50px, more horizontal than vertical, within 500ms
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5 && dt < 500) {
if (dx < 0 && activeTab === 'editor') {
// Swipe left: go to Output
switchTab('output');
} else if (dx > 0 && activeTab === 'output') {
// Swipe right: go to Editor
switchTab('editor');
}
}
}, { passive: true });
}

Copilot uses AI. Check for mistakes.

// Listen for viewport changes (e.g., device rotation, window resize)
mobileMq.addEventListener('change', () => applyResponsiveLayout());

// Apply on initial load
applyResponsiveLayout();

// ---------------------------------------------------------------
// Draggable divider
// ---------------------------------------------------------------
divider.addEventListener('mousedown', (e) => {
isDragging = true;
divider.classList.add('dragging');
Expand Down
111 changes: 108 additions & 3 deletions docs/public/editor/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,103 @@
50% { opacity: 0.35; }
}

/* Mobile tab bar */
.tab-bar {
border-bottom: 1px solid var(--borderColor-default, var(--color-border-default));
background: var(--bgColor-default, var(--color-canvas-default));
z-index: 9;
}
.tab-btn {
flex: 1;
padding: 8px 12px;
border: none;
background: none;
font-size: 13px;
font-weight: 600;
color: var(--fgColor-muted, var(--color-fg-muted));
cursor: pointer;
position: relative;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 150ms ease;
}
.tab-btn.active {
color: var(--fgColor-accent, var(--color-accent-fg));
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 12px;
right: 12px;
height: 2px;
background: var(--fgColor-accent, var(--color-accent-fg));
border-radius: 2px 2px 0 0;
}
Comment on lines +124 to +150
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab buttons lack visible focus styles for keyboard navigation. When users tab to these buttons using keyboard navigation, there should be a clear visual indicator. Consider adding a :focus-visible style (e.g., outline or box-shadow) to make keyboard focus clear for accessibility.

Copilot uses AI. Check for mistakes.

/* Tab status dot (on Output tab) */
.tab-status-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-left: 6px;
vertical-align: middle;
background: var(--fgColor-success, #1a7f37);
transition: background 200ms ease;
}
.tab-status-dot[data-stale="true"] {
background: var(--fgColor-attention, #9a6700);
}
.tab-status-dot[data-stale="error"] {
background: var(--fgColor-danger, #cf222e);
}

/* Floating Action Button for compile */
.fab-compile {
position: fixed;
bottom: 20px;
right: 20px;
width: 52px;
height: 52px;
border-radius: 50%;
border: none;
background: var(--bgColor-accent-emphasis, var(--color-accent-emphasis, #0969da));
color: var(--fgColor-onEmphasis, #ffffff);
font-size: 20px;
cursor: pointer;
box-shadow: 0 3px 12px rgba(0,0,0,0.28);
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
transition: transform 120ms ease, box-shadow 120ms ease;
-webkit-tap-highlight-color: transparent;
}
.fab-compile:active {
transform: scale(0.92);
box-shadow: 0 1px 6px rgba(0,0,0,0.25);
}
.fab-compile.compiling {
opacity: 0.6;
pointer-events: none;
}

/* Responsive */
@media (max-width: 767px) {
.panels-container { flex-direction: column !important; }
.divider { width: 100%; height: 4px; cursor: row-resize; }
.divider { display: none !important; }
.header-bar { gap: 8px !important; padding: 8px 12px !important; flex-wrap: wrap; height: auto !important; min-height: 48px !important; }
.header-separator { display: none !important; }
.tab-bar { display: flex !important; }
.fab-compile { display: flex !important; }
footer { display: none !important; }
/* Panel headers are redundant when tabs are visible */
.mobile-hidden-header { display: none !important; }
}
@media (min-width: 768px) {
.tab-bar { display: none !important; }
.fab-compile { display: none !important; }
}
</style>
</head>
Expand Down Expand Up @@ -178,11 +269,20 @@
<span class="text-mono f6" id="warningText"></span>
</div>

<!-- Mobile Tab Bar (hidden on desktop via CSS) -->
<div class="tab-bar d-none" id="tabBar">
<button class="tab-btn active" data-panel="editor">Editor</button>
<button class="tab-btn" data-panel="output">
Output
<span class="tab-status-dot" id="tabStatusDot"></span>
</button>
</div>
Comment on lines +273 to +279
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab buttons should include ARIA attributes to properly indicate their role and state to assistive technologies. Consider adding role="tab", aria-selected="true/false", and aria-controls attributes that reference the respective panel IDs. The tab bar container should have role="tablist". This would improve accessibility for screen reader users.

Copilot uses AI. Check for mistakes.

<!-- Main Panels -->
<div class="panels-container d-flex flex-1" style="min-height: 0;" id="panels">
<!-- Editor Panel -->
<div class="d-flex flex-column" style="flex: 1 1 50%; min-width: 0; min-height: 0;" id="panelEditor">
<div class="d-flex flex-items-center flex-justify-between px-3 py-2 color-bg-subtle border-bottom f6 text-bold text-uppercase color-fg-muted" style="letter-spacing: 0.5px; min-height: 36px; user-select: none;">
<div class="mobile-hidden-header d-flex flex-items-center flex-justify-between px-3 py-2 color-bg-subtle border-bottom f6 text-bold text-uppercase color-fg-muted" style="letter-spacing: 0.5px; min-height: 36px; user-select: none;">
<span class="d-flex flex-items-center gap-1">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="opacity: 0.6;">
<path d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75C0 1.784.784 1 1.75 1zm12.5 1.5H1.75a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25zM3 5.5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 013 5.5zm0 4a.75.75 0 01.75-.75h5.5a.75.75 0 010 1.5h-5.5A.75.75 0 013 9.5z"/>
Expand All @@ -197,7 +297,7 @@

<!-- Output Panel -->
<div class="d-flex flex-column" style="flex: 1 1 50%; min-width: 0; min-height: 0;" id="panelOutput">
<div class="d-flex flex-items-center flex-justify-between px-3 py-2 color-bg-subtle border-bottom f6 text-bold text-uppercase color-fg-muted" style="letter-spacing: 0.5px; min-height: 36px; user-select: none;">
<div class="mobile-hidden-header d-flex flex-items-center flex-justify-between px-3 py-2 color-bg-subtle border-bottom f6 text-bold text-uppercase color-fg-muted" style="letter-spacing: 0.5px; min-height: 36px; user-select: none;">
<span class="d-flex flex-items-center gap-1">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" style="opacity: 0.6;">
<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0114.25 16H1.75A1.75 1.75 0 010 14.25Zm1.75-.25a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25Zm7.47 3.97a.75.75 0 011.06 0l2 2a.75.75 0 010 1.06l-2 2a.75.75 0 11-1.06-1.06L10.69 8 9.22 6.53a.75.75 0 010-1.06zm-3.44 0a.75.75 0 010 1.06L4.31 8l1.47 1.47a.75.75 0 01-1.06 1.06l-2-2a.75.75 0 010-1.06l2-2a.75.75 0 011.06 0z"/>
Expand Down Expand Up @@ -234,6 +334,11 @@
<a href="https://github.com/security" target="_blank" rel="noopener noreferrer" class="Link--secondary">Security</a>
</div>
</footer>

<!-- Mobile Floating Action Button for compile (hidden on desktop via CSS) -->
<button class="fab-compile d-none" id="fabCompile" title="Compile" aria-label="Compile workflow">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"></polygon></svg>
</button>
</div>

<script type="module" src="./editor.js"></script>
Expand Down