Code Refactoring and Responsive Layout

Here are the precise modifications to fulfill your layout requirements using pure CSS, along with code refactoring to extract reusable logic, eliminate redundant comments, and remove unnecessary variables/logs. All code and comments are provided in English.

1. CSS Modifications (Responsive Layout)

Replace the corresponding classes and the @media blocks in your <style> section with the following code. This uses flex properties and min-width/min-height to guarantee the control panel size while allowing the SVG to shrink when necessary.

CSS

/* Base Flex Settings */
.ejceesmain {
   display: flex;
   width: 100vw;
   height: 100vh;
   background-color: #f4f4f9;
   overflow: hidden;
}

.ejceessvg {
   display: flex;
   justify-content: center;
   align-items: center;
   box-sizing: border-box;
   background-color: transparent;
   flex: 1 1 auto;
   min-width: 0;
   min-height: 0;
}

.ejceespb {
   display: block;
   aspect-ratio: 432 / 480;
   background-color: transparent;
   cursor: crosshair;
   max-width: 100%;
   max-height: 100%;
}

.ejceesctrl {
   display: flex;
   flex-direction: column;
   background-color: #333;
   color: #fff;
   flex: 0 0 auto;
   box-sizing: border-box;
   overflow: hidden;
}

/* --- Responsive Layout Rules --- */

/* Mobile (Narrow Screen) */
@media (max-width: 767px) {
   .ejceesmain { flex-direction: column; }
   .ejceesctrl { flex: 1 0 272px; min-height: 272px; }
   .ejceessvg { width: 100%; height: auto; }
   .ejceespb { width: 100%; height: auto; }
   
   .ejceestool, .ejceesstep, .ejceesoutput { padding: 0 7px; }
   .ejceesstep { gap: 7px; }
   .ejceesrecord { width: 200px; font-size: 12px; }
   .ejceesrcdstep .ejceesrcdstepcontent { margin-left: 24px; }
   .branch-marker { margin-left: 1px; }
}

/* Desktop (Wide Screen) */
@media (min-width: 768px) {
   .ejceesmain { flex-direction: row; }
   .ejceesctrl { flex: 1 0 414px; min-width: 414px; }
   .ejceessvg { height: 100%; width: auto; }
   .ejceespb { height: 100%; width: auto; }
   
   .ejceestool, .ejceesstep, .ejceesoutput { padding: 0 12px; }
   .ejceesstep { gap: 12px; }
   .ejceesrecord { width: 268px; font-size: 16px; }
}

2. JavaScript Refactoring & Optimization

Part A: Add Helper Functions

Insert these reusable functions right after the undoStack and redoStack declarations (around line 712).

JavaScript

// --- Reusable Helper Functions ---

function resetRangeMode() {
   isRangeMode = false;
   rangeStart = null;
   rangeEnd = null;
   rangeClicks = 0;
   clearRangeBorders();
}

function isGlobalActionBlocked() {
   return isExportTextMode || isAutoPlaying;
}

function countForksUpToStep(stepIndex) {
   let forkCount = 0;
   let curr = historyFEN;
   for (let i = 0; i < stepIndex; i++) {
       if (curr.v && curr.v.length > 1) forkCount++;
       const choice = (curr.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
       curr = curr.v[choice];
   }
   return forkCount;
}

function clearPieceHighlights(svgRoot = document) {
   svgRoot.querySelectorAll('use').forEach(el => {
       const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
       if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
           el.removeAttribute('currentmove');
           el.setAttribute('stroke', 'none');
           el.removeAttribute('stroke-width');
       }
   });
}

function expandJSONNode(node, parentFen) {
   let dc, simfen, newNode;
   if (node.m) {
       dc = deriveCoordsFromMove(parentFen, node.m);
       simfen = simulateMove(parentFen, dc);
       newNode = { fen: simfen, move: node.m, lastMove: dc, c: node.c || "", v: [] };
   } else {
       newNode = { fen: parentFen, c: node.c || "", v: [] };
   }
   if (node.v) {
       newNode.v = node.v.map(child => expandJSONNode(child, newNode.fen));
   }
   return newNode;
}

function saveToLocalStorage(state) {
   setTimeout(() => {
       try { localStorage.setItem('ejcees_saved_state', state); }
       catch (e) { console.error("Failed to save state to localStorage:", e); }
   }, 0);
}

Part B: Replace Core Duplicate Logic Functions

Replace your existing functions (applyLastMoveVisuals, pushHistory, addMoveToRecordUI, deleteCurrentMoveAndAfter, cloneGameState, saveStateToUndo, restoreState) with these streamlined versions. This leverages the new helpers to drastically reduce code duplication:

JavaScript

function applyLastMoveVisuals(lastMove) {
   clearPieceHighlights(etboard);
   const dot = document.getElementById('ejceesstartdot');

   if (!lastMove) {
       if (dot) {
           dot.setAttribute('cx', initialDotCx);
           dot.setAttribute('cy', initialDotCy);
       }
       return;
   }
   
   const pieceId = tileMap.get(`${lastMove.endX},${lastMove.endY}`);
   if (pieceId) {
       const pieceEl = document.getElementById(pieceId);
       if (pieceEl) {
           pieceEl.setAttribute('currentmove', '1');
           if (!pieceEl.getAttribute('pickup')) {
               pieceEl.setAttribute('stroke', 'lightgreen');
               pieceEl.setAttribute('stroke-width', '2');
           }
       }
   }

   if (dot) {
       const visStart = getVisualCoords(lastMove.startX, lastMove.startY);
       dot.setAttribute('cx', visStart.x * 48 + 24);
       dot.setAttribute('cy', visStart.y * 48 + 24);
   }
}

function pushHistory(lastMoveText = null, lastMoveData = null) {
   if (!lastMoveText) return;
   const newFEN = boardToFEN();
   const parentNode = getNodeAtStep(currentStepIndex);

   if (parentNode) {
       let branchIndex = parentNode.v.findIndex(child => child.move === lastMoveText && child.fen === newFEN);

       if (branchIndex === -1) {
           parentNode.v.push({ fen: newFEN, move: lastMoveText, lastMove: lastMoveData, c: "", v: [] });
           branchIndex = parentNode.v.length - 1;
       }

       if (parentNode.v.length > 1) {
           updateBranchPath(countForksUpToStep(currentStepIndex), branchIndex);
       }
   }

   currentStepIndex++;
   const totalDepth = getPathDepth(historyFEN, currentBranch);
   stepSlider.max = Math.max(0, totalDepth - 1);
   stepSlider.value = currentStepIndex;

   renderRecordUI();
   document.querySelector('.ejceesoutput').textContent += ' | ' + lastMoveText;
   renderNoteUI();
   saveStateToUndo();
}

function addMoveToRecordUI(text, index) {
   const recordContainer = document.querySelector('.ejceesrecord');
   const node = getNodeAtStep(index);
   if (!node) return;
   const turnAfterMove = node.fen.split(' ')[1];
   const whoMoved = (turnAfterMove === 'b') ? 'w' : 'b';

   let lastOuter = recordContainer.lastElementChild;
   if (!lastOuter || !lastOuter.classList.contains('ejceesrcdstepouter')) {
       lastOuter = null;
   }

   let outer = lastOuter;
   if (whoMoved === 'w') {
       outer = document.createElement('div');
       outer.className = 'ejceesrcdstepouter';
       recordContainer.appendChild(outer);
   } else {
       if (!outer || outer.children.length >= 2) {
           outer = document.createElement('div');
           outer.className = 'ejceesrcdstepouter';
           recordContainer.appendChild(outer);
           
           const spacer = document.createElement('div');
           spacer.className = 'ejceesrcdstep';
           spacer.style.pointerEvents = 'none';
           outer.appendChild(spacer);
       }
   }

   const stepDiv = document.createElement('div');
   stepDiv.className = 'ejceesrcdstep';
   stepDiv.id = `step-record-${index}`;
   stepDiv.dataset.index = index;

   if (node.c && node.c.trim() !== '') stepDiv.classList.add('has-comment');

   const parentNode = getNodeAtStep(index - 1);
   if (parentNode && parentNode.v.length > 1) {
       let forkIndex = countForksUpToStep(index - 1);
       const marker = document.createElement('span');
       marker.className = 'branch-marker';
       const currentChoice = currentBranch[forkIndex] !== undefined ? currentBranch[forkIndex] : 0;
       marker.innerText = `${currentChoice + 1}/${parentNode.v.length}`;

       marker.onclick = (e) => {
           e.stopPropagation();
           showBranchMenu(marker, parentNode, forkIndex, index);
       };
       stepDiv.appendChild(marker);
   }

   const textSpan = document.createElement('span');
   textSpan.className = 'ejceesrcdstepcontent';
   textSpan.textContent = NotationConverter.toChinese(text);
   stepDiv.appendChild(textSpan);

   stepDiv.addEventListener('click', () => {
       if (isEditingComment || isExportTextMode || isEditMode || isAutoPlaying) return;
       jumpToStep(index);
       saveStateToUndo();
   });

   outer.appendChild(stepDiv);
   recordContainer.scrollTop = recordContainer.scrollHeight;
   highlightActiveStep(index);
}

function deleteCurrentMoveAndAfter() {
   const targetStep = (isRangeMode && rangeStart !== null && rangeStart !== 0) ? rangeStart : currentStepIndex;
   if (targetStep === 0) return;

   const parentNode = getNodeAtStep(targetStep - 1);
   if (!parentNode) return;

   parentNode.v = [];
   currentStepIndex = Math.max(0, targetStep - 1);
   stepSlider.value = currentStepIndex;

   currentBranch = currentBranch.slice(0, countForksUpToStep(currentStepIndex));
   if (isRangeMode) resetRangeMode();

   renderRecordUI();
   renderNoteUI();
   jumpToStep(currentStepIndex);
   saveStateToUndo();
}

function cloneGameState() {
   return {
       historyFEN: structuredClone(historyFEN),
       currentBranch: [...currentBranch],
       currentStepIndex: currentStepIndex,
       moveInterval: moveInterval,
       moveSpeed: moveSpeed,
       isRotateEnabled: isRotateEnabled,
       isFlipEnabled: isFlipEnabled
   };
}

function saveStateToUndo() {
   const state = JSON.stringify(cloneGameState());
   undoStack.push(state);
   if (undoStack.length > 36) undoStack.shift();
   redoStack = [];
   saveToLocalStorage(state);
}

function restoreState(state) {
   const stateObj = JSON.parse(state);
   historyFEN = structuredClone(stateObj.historyFEN);
   currentBranch = [...stateObj.currentBranch];
   currentStepIndex = stateObj.currentStepIndex;
   moveInterval = parseFloat(stateObj.moveInterval);
   moveSpeed = parseFloat(stateObj.moveSpeed);
   isRotateEnabled = stateObj.isRotateEnabled;
   isFlipEnabled = stateObj.isFlipEnabled;
   renderRecordUI();
   renderNoteUI();
   jumpToStep(currentStepIndex);
   saveToLocalStorage(state);
}

Part C: Optimize Import / Export Blocks and UI Listeners

Update enterEditMode, tool-exp-svg, file parsing, and tool event listeners to utilize the new helpers.

In enterEditMode():

Replace the visual clearance loop with:

JavaScript

clearPieceHighlights(etboard);

In document.getElementById('tool-exp-svg').addEventListener(...):

Replace the visual clearance loop with:

JavaScript

clearPieceHighlights(cloneSvg);

In document.getElementById('file-input').addEventListener(...) and importExportedText():

Replace the duplicate nested expand() functions and variable resets with:

JavaScript

if (isRangeMode) resetRangeMode();
historyFEN = expandJSONNode(data, data.fen);

Replace Tool / Step Listeners (Toolbar Events):

JavaScript

document.querySelector('.ejceesstepminus').addEventListener('click', () => {
   if (isEditingComment || isEditMode || isRangeMode || isGlobalActionBlocked()) return;
   jumpToStep(currentStepIndex - 1);
   saveStateToUndo();
   updateToolHighlights();
});

document.querySelector('.ejceesstepplus').addEventListener('click', () => {
   if (isEditingComment || isEditMode || isRangeMode || isGlobalActionBlocked()) return;
   jumpToStep(currentStepIndex + 1);
   saveStateToUndo();
   updateToolHighlights();
});

function ejceesrcdstartclick() {
   if (isEditingComment || isEditMode || isGlobalActionBlocked()) return;
   jumpToStep(0);
   saveStateToUndo();
   updateToolHighlights();
}

document.getElementById('tool-new').addEventListener('click', () => {
   if (isGlobalActionBlocked()) return;
   if (isRangeMode) {
       historyFEN = getGamePath();
       resetRangeMode();
       stepSlider.max = getPathDepth(historyFEN, currentBranch) - 1;
       loadFEN(historyFEN.fen);
   } else {
       stepSlider.max = 0;
       historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
       loadFEN(INITIAL_FEN);
   }
   currentBranch = [];
   currentStepIndex = 0;
   stepSlider.value = 0;
   renderRecordUI();
   renderNoteUI();
   highlightActiveStep(0);
   applyLastMoveVisuals(null);
   saveStateToUndo();
   updateToolHighlights();
});

document.getElementById('tool-save').addEventListener('click', () => {
   if (isGlobalActionBlocked()) return;

   function simplify(node) {
       let obj = node.move ? { m: node.move } : {};
       if (node.c) obj.c = node.c;
       if (node.v && node.v.length > 0) obj.v = node.v.map(child => simplify(child));
       return obj;
   }

   let copy;
   if (isRangeMode) {
       let rangeHistory = getGamePath();
       copy = simplify(rangeHistory);
       copy.fen = rangeHistory.fen;
       resetRangeMode();
       renderNoteUI();
   } else {
       copy = simplify(historyFEN);
       copy.fen = historyFEN.fen;
   }

   const blob = new Blob([JSON.stringify(copy)], { type: 'text/plain' });
   const a = document.createElement('a');
   const now = new Date();
   const timestamp = now.getFullYear().toString().padStart(4, '0') +
       (now.getMonth() + 1).toString().padStart(2, '0') +
       now.getDate().toString().padStart(2, '0') +
       now.getHours().toString().padStart(2, '0') +
       now.getMinutes().toString().padStart(2, '0') +
       now.getSeconds().toString().padStart(2, '0');
   a.href = URL.createObjectURL(blob);
   a.download = `ejcees_json_${timestamp}.txt`;
   a.click();
});

document.getElementById('tool-open').addEventListener('click', () => {
   if (isGlobalActionBlocked()) return;
   document.getElementById('file-input').click();
});

document.getElementById('tool-rotate').addEventListener('click', () => {
   if (isGlobalActionBlocked()) return;
   isRotateEnabled = !isRotateEnabled;
   reapplyVisualPositions();
   saveStateToUndo();
   updateToolHighlights();
});

document.getElementById('tool-flip').addEventListener('click', () => {
   if (isGlobalActionBlocked()) return;
   isFlipEnabled = !isFlipEnabled;
   reapplyVisualPositions();
   saveStateToUndo();
   updateToolHighlights();
});

document.getElementById('tool-edit').addEventListener('click', () => {
   if (isRangeMode) resetRangeMode();
   if (isEditingComment || isGlobalActionBlocked()) return;
   if (!isEditMode) enterEditMode();
});

document.getElementById('tool-undo').addEventListener('click', () => {
   if (isRangeMode) resetRangeMode();
   if (isGlobalActionBlocked()) return;
   isEditMode ? undoEdit() : undo();
});

document.getElementById('tool-redo').addEventListener('click', () => {
   if (isRangeMode) resetRangeMode();
   if (isGlobalActionBlocked()) return;
   isEditMode ? redoEdit() : redo();
});

document.getElementById('tool-exp-txt').addEventListener('click', () => {
   if (isEditingComment || isEditMode || isGlobalActionBlocked()) return;
   isExportTextMode = true;
   updateToolHighlights();
   renderExportTextUI();
   if (isRangeMode) resetRangeMode();
});

document.addEventListener('keydown', (e) => {
   if (isGlobalActionBlocked()) return;
   if (e.ctrlKey && e.key === 'z') {
       e.preventDefault();
       isEditMode ? undoEdit() : undo();
   }
   if (e.ctrlKey && e.key === 'y') {
       e.preventDefault();
       isEditMode ? redoEdit() : redo();
   }
});

Part D: Cleanup (Logging removal)

Make sure to remove the raw console.log('2moveInterval', moveInterval); at the start of your DOMContentLoaded callback and console.log('initialHistory', initialHistory); near the initialization logic.