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.
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; }
}
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);
}
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);
}
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();
}
});
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.