SVG Export and Import Enhancements

Here are the modifications to address your three requirements. All comments and code are in English.

1. CSS Modifications

Add these new styles to the <style> block in the <head> to handle the SVG preview correctly without white edges.

CSS

        /* --- Add these styles for the SVG Export Preview --- */
       .ejcees-export-modal {
           position: fixed;
           top: 0;
           left: 0;
           width: 100vw;
           height: 100vh;
           background-color: rgba(0,0,0,0.85);
           z-index: 9999;
           display: flex;
           justify-content: center;
           align-items: center;
       }

       .ejcees-export-box {
           position: relative;
           background-color: #333;
           padding: 35px 20px 20px;
           border-radius: 8px;
           display: flex;
           flex-direction: column;
           align-items: center;
           gap: 15px;
           max-height: 90vh;
           max-width: 90vw;
           box-shadow: 0px 0px 25px rgba(0,0,0,0.5);
           box-sizing: border-box;
       }

       .ejcees-export-preview {
           width: auto;
           height: 60vh;
           max-width: 100%;
           background-color: transparent;
           display: flex;
           justify-content: center;
           align-items: center;
           box-sizing: border-box;
       }

       .ejcees-export-preview svg {
           display: block;
           height: 100%;
           aspect-ratio: 432 / 480;
           background-color: #f4f4f9;
       }

2. Base64 Helpers & SVG Import Logic

Add the following functions right after document.addEventListener('DOMContentLoaded', () => { so they are accessible globally within the script.

JavaScript

            // --- Helper functions for Base64 Unicode encoding/decoding ---
           const b64EncodeUnicode = (str) => {
               if (!str) return '';
               return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
                   return String.fromCharCode('0x' + p1);
               }));
           };

           const b64DecodeUnicode = (str) => {
               if (!str) return '';
               return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
                   return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
               }).join(''));
           };

           // --- Function to Import SVG and reconstruct Game History ---
           function importExportedSVG(svgText) {
               const parser = new DOMParser();
               const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
               
               // 1. Get step0 for initial comment
               const step0 = svgDoc.getElementById('step0');
               const initialComment = step0 && step0.getAttribute('data-comment') ? b64DecodeUnicode(step0.getAttribute('data-comment')) : "";
               
               // 2. Reconstruct Initial FEN from SVG DOM positions
               const etboard = svgDoc.querySelector('.etboard');
               const uses = etboard.querySelectorAll('use');
               const boardMap = Array(10).fill(null).map(() => Array(9).fill(''));
               
               let firstMovePieceId = null;
               const step1 = svgDoc.getElementById('step1');
               if (step1 && step1.parentElement) {
                   firstMovePieceId = step1.parentElement.getAttribute('id');
               }
               
               uses.forEach(use => {
                   const href = use.getAttribute('href') || use.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
                   if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
                       const transform = use.getAttribute('transform');
                       const match = transform && transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                       if (match) {
                           const px = parseFloat(match[1]);
                           const py = parseFloat(match[2]);
                           const x = Math.round((px - 24) / 48);
                           const y = Math.round((py - 24) / 48);
                           const id = use.getAttribute('id');
                           // Ignore cloned animation elements (e.g. ones with 'f' suffix)
                           if (id && !id.endsWith('f') && x >= 0 && x < 9 && y >= 0 && y < 10) {
                               boardMap[y][x] = id[0];
                           }
                       }
                   }
               });
               
               let initialFenBoard = "";
               for (let y = 0; y < 10; y++) {
                   let emptyCount = 0;
                   for (let x = 0; x < 9; x++) {
                       if (boardMap[y][x] === '') {
                           emptyCount++;
                       } else {
                           if (emptyCount > 0) { initialFenBoard += emptyCount; emptyCount = 0; }
                           initialFenBoard += boardMap[y][x];
                       }
                   }
                   if (emptyCount > 0) initialFenBoard += emptyCount;
                   if (y < 9) initialFenBoard += "/";
               }
               
               // Determine whose turn it is based on the first move
               let initialTurn = 'w';
               if (firstMovePieceId && firstMovePieceId[0] === firstMovePieceId[0].toLowerCase()) {
                   initialTurn = 'b';
               }
               const baseFen = initialFenBoard + " " + initialTurn;
               
               let newHistoryFEN = { fen: baseFen, move: null, lastMove: null, c: initialComment, v: [] };
               let currentNode = newHistoryFEN;
               let currentFen = baseFen;
               let stepIndex = 1;
               
               // Temporarily load FEN to populate `tileMap` correctly for `getMoveNotation` logic
               loadFEN(baseFen);
               
               // 3. Reconstruct moves by analyzing animateTransform values
               while (true) {
                   const stepAnim = svgDoc.getElementById(`step${stepIndex}`);
                   if (!stepAnim) break;
                   
                   const commentStr = stepAnim.getAttribute('data-comment');
                   const comment = commentStr ? b64DecodeUnicode(commentStr) : "";
                   
                   const fromStr = stepAnim.getAttribute('from');
                   const toStr = stepAnim.getAttribute('to');
                   const pId = stepAnim.parentElement.getAttribute('id');
                   
                   const fParts = fromStr.split(',');
                   const tParts = toStr.split(',');
                   const startX = Math.round((parseFloat(fParts[0]) - 24) / 48);
                   const startY = Math.round((parseFloat(fParts[1]) - 24) / 48);
                   const endX = Math.round((parseFloat(tParts[0]) - 24) / 48);
                   const endY = Math.round((parseFloat(tParts[1]) - 24) / 48);
                   
                   const capturedId = tileMap.get(`${endX},${endY}`) || null;
                   const moveNotation = getMoveNotation(pId, startX, startY, endX, endY, capturedId);
                   const dc = { startX, startY, endX, endY };
                   const nextFen = simulateMove(currentFen, dc);
                   
                   // Update logical board for next loop's `getMoveNotation`
                   tileMap.delete(`${startX},${startY}`);
                   tileMap.set(`${endX},${endY}`, pId);
                   
                   let newNode = { fen: nextFen, move: moveNotation, lastMove: dc, c: comment, v: [] };
                   currentNode.v.push(newNode);
                   currentNode = newNode;
                   currentFen = nextFen;
                   stepIndex++;
               }
               
               historyFEN = newHistoryFEN;
               initBranch();
               currentStepIndex = 0;
               renderRecordUI();
               renderNoteUI();
               jumpToStep(0);
               saveStateToUndo();
           }

3. File Input Listener Modification

Locate document.getElementById('file-input').addEventListener('change', ...) and modify it to handle SVG files:

JavaScript

            document.getElementById('file-input').addEventListener('change', (e) => {
               const file = e.target.files[0];
               if (!file) return;

               const reader = new FileReader();
               reader.onload = (event) => {
                   const result = event.target.result;
                   
                   // Route to SVG Import if it's an SVG file
                   if (file.name.toLowerCase().endsWith('.svg') || result.trim().startsWith('<svg')) {
                       importExportedSVG(result);
                       e.target.value = ''; // Reset input
                       return;
                   }

                   const data = JSON.parse(result);
                   // ... [Keep the rest of your original JSON parsing logic untouched here] ...

4. SVG Export Logic Modification

Locate document.getElementById('tool-exp-svg').addEventListener('click', ...) and modify it to append the base64 encoded comments to step0 and stepN:

JavaScript

            document.getElementById('tool-exp-svg').addEventListener('click', () => {
               if (isExportTextMode || isEditingComment || isEditMode) return;

               let exportStart = 0;
               let exportEnd = getPathDepth(historyFEN, currentBranch) - 1;
               let isAnimation = true;

               // 1. Determine export bounds based on the "Range Selection" (isRangeMode) logic
               if (isRangeMode) {
                   if (rangeStart === null && rangeEnd === null) {
                       isAnimation = false;
                       exportStart = currentStepIndex;
                       exportEnd = currentStepIndex;
                   } else if (rangeStart !== null && rangeEnd !== null && rangeStart === rangeEnd) {
                       isAnimation = false;
                       exportStart = rangeStart;
                       exportEnd = rangeEnd;
                   } else if (rangeStart !== null && rangeEnd === null) {
                       exportStart = rangeStart;
                   } else if (rangeStart !== null && rangeEnd !== null && rangeStart !== rangeEnd) {
                       exportStart = Math.min(rangeStart, rangeEnd);
                       exportEnd = Math.max(rangeStart, rangeEnd);
                   }

                   isRangeMode = false;
                   clearRangeBorders();
                   renderNoteUI();
               }

               const savedStep = currentStepIndex;
               jumpToStep(exportStart);
               const cloneSvg = document.querySelector('.ejceespb').cloneNode(true);
               
               // Get initial state comment
               const startNodeComment = getNodeAtStep(exportStart).c || "";

               if (isAnimation) {
                   const w2t = cloneSvg.querySelector('#whiteToTransparent');
                   if (w2t) w2t.remove();
                   const startDot = cloneSvg.querySelector('#ejceesstartdot');
                   if (startDot) startDot.remove();

                   cloneSvg.querySelectorAll('use').forEach(p => {
                       const href = p.getAttribute('href');
                       if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
                           p.removeAttribute('currentmove');
                           p.setAttribute('stroke', 'none');
                           p.removeAttribute('stroke-width');
                       }
                   });
               } else {
                   // Export Static SVG Snapshot - Append a dummy step0 for the base64 comment
                   const dummyStep0 = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
                   dummyStep0.setAttribute('id', 'step0');
                   dummyStep0.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
                   cloneSvg.querySelector('.etboard').appendChild(dummyStep0);

                   const svgStringXML = new XMLSerializer().serializeToString(cloneSvg);
                   const svgString = svgStringXML.replace(/ xmlns:a.*?=.*?".*?"/g, '').replace(/ a[^t].*?=.*?".*?"/g, '').replace(/^\s+|\s+$/gm, '').replace(/[\r\n]/gm, '');
                   showSvgExportModal(svgString, false);
                   jumpToStep(savedStep);
                   return;
               }

               cloneSvg.setAttribute('class', 'ejceespbanimate');
               const etboardClone = cloneSvg.querySelector('.etboard');
               const etdropClone = cloneSvg.querySelector('.etdrop');

               const movedPieceIds = new Set();
               const initialCoords = {};

               const boardPieces = etboardClone.querySelectorAll('use');
               boardPieces.forEach(p => {
                   const href = p.getAttribute('href');
                   if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
                       const id = p.getAttribute('id');
                       const pf = p.cloneNode(true);
                       pf.setAttribute('id', id + 'f');
                       pf.setAttribute('opacity', '0');
                       etdropClone.appendChild(pf);

                       const transform = p.getAttribute('transform');
                       const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                       if (match) {
                           initialCoords[id] = `${match[1]},${match[2]}`;
                       }
                   }
               });

               let pathNodes = [];
               let curr = historyFEN;
               let ptr = 0;
               let tempStep = 0;
               while (curr) {
                   if (tempStep > exportStart && tempStep <= exportEnd) {
                       pathNodes.push(curr);
                   }
                   if (tempStep === exportEnd) break;
                   const children = curr.v || [];
                   if (children.length === 0) break;
                   const choice = children.length > 1 ? (currentBranch[ptr++] || 0) : 0;
                   curr = children[choice];
                   tempStep++;
               }

               let simMap = new Map(tileMap);
               const delayStr = moveInterval + 's';

               pathNodes.forEach((node, i) => {
                   let stepNum = i + 1;
                   let moveData = node.lastMove;
                   if (!moveData) return;
                   
                   let pieceId = simMap.get(`${moveData.startX},${moveData.startY}`);
                   let capturedId = simMap.get(`${moveData.endX},${moveData.endY}`);
                   if (!pieceId) return;

                   // Extract node comment
                   const nodeComment = node.c || "";

                   let visStart = getVisualCoords(moveData.startX, moveData.startY);
                   let visEnd = getVisualCoords(moveData.endX, moveData.endY);
                   let fromStr = `${visStart.x * 48 + 24},${visStart.y * 48 + 24}`;
                   let toStr = `${visEnd.x * 48 + 24},${visEnd.y * 48 + 24}`;

                   movedPieceIds.add(pieceId);
                   simMap.delete(`${moveData.startX},${moveData.startY}`);
                   if (capturedId) simMap.delete(`${moveData.endX},${moveData.endY}`);
                   simMap.set(`${moveData.endX},${moveData.endY}`, pieceId);

                   let beginStr = (stepNum === 1) ?
                       `${delayStr};step0.end+${delayStr}` :
                       `step${stepNum - 1}.begin+${delayStr}`;

                   const boardPiece = etboardClone.querySelector(`#${pieceId}`);
                   if (boardPiece) {
                       const animT = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
                       animT.setAttribute('id', `step${stepNum}`);
                       // Add base64 comment data
                       animT.setAttribute('data-comment', b64EncodeUnicode(nodeComment));
                       animT.setAttribute('begin', beginStr);
                       animT.setAttribute('attributeName', 'transform');
                       animT.setAttribute('attributeType', 'XML');
                       animT.setAttribute('type', 'translate');
                       animT.setAttribute('from', fromStr);
                       animT.setAttribute('to', toStr);
                       animT.setAttribute('dur', `${moveSpeed}s`);
                       animT.setAttribute('fill', 'freeze');
                       boardPiece.appendChild(animT);
                   }

                   const dropPiece = etdropClone.querySelector(`#${pieceId}f`);
                   if (dropPiece) {
                       const animT = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
                       animT.setAttribute('begin', `step${stepNum}.begin`);
                       animT.setAttribute('attributeName', 'transform');
                       animT.setAttribute('attributeType', 'XML');
                       animT.setAttribute('type', 'translate');
                       animT.setAttribute('from', fromStr);
                       animT.setAttribute('to', toStr);
                       animT.setAttribute('dur', `${moveSpeed}s`);
                       animT.setAttribute('fill', 'freeze');
                       dropPiece.appendChild(animT);

                       const animO = document.createElementNS('http://www.w3.org/2000/svg', 'set');
                       animO.setAttribute('begin', `step${stepNum}.begin`);
                       animO.setAttribute('end', `step${stepNum}.end`);
                       animO.setAttribute('attributeName', 'opacity');
                       animO.setAttribute('to', '1');
                       dropPiece.appendChild(animO);
                   }

                   if (capturedId) {
                       const capPiece = etboardClone.querySelector(`#${capturedId}`);
                       if (capPiece) {
                           const animCap = document.createElementNS('http://www.w3.org/2000/svg', 'set');
                           animCap.setAttribute('begin', `step${stepNum}.end`);
                           animCap.setAttribute('end', `step0.begin+${moveInterval}s`);
                           animCap.setAttribute('attributeName', 'opacity');
                           animCap.setAttribute('to', '0');
                           capPiece.appendChild(animCap);
                       }
                   }
               });

               if (pathNodes.length > 0) {
                   const finalStepNum = pathNodes.length;
                   const step0 = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
                   step0.setAttribute('id', 'step0');
                   // Add base64 initial comment data
                   step0.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
                   step0.setAttribute('begin', `step${finalStepNum}.end+${moveInterval * 2}s`);
                   step0.setAttribute('attributeName', 'opacity');
                   step0.setAttribute('values', '1;0;0;1');
                   step0.setAttribute('dur', `${moveInterval * 3}s`);
                   step0.setAttribute('fill', 'freeze');
                   etboardClone.appendChild(step0);

                   movedPieceIds.forEach(pid => {
                       const initialPos = initialCoords[pid];
                       if (!initialPos) return;

                       const bp = etboardClone.querySelector(`#${pid}`);
                       if (bp) {
                           const r = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
                           r.setAttribute('begin', `step0.begin+${moveInterval}s`);
                           r.setAttribute('dur', `${moveInterval}s`);
                           r.setAttribute('attributeName', 'transform');
                           r.setAttribute('attributeType', 'XML');
                           r.setAttribute('type', 'translate');
                           r.setAttribute('to', initialPos);
                           r.setAttribute('fill', 'freeze');
                           bp.appendChild(r);
                       }

                       const dp = etdropClone.querySelector(`#${pid}f`);
                       if (dp) {
                           const r = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
                           r.setAttribute('begin', `step0.begin+${moveInterval}s`);
                           r.setAttribute('dur', `${moveInterval}s`);
                           r.setAttribute('attributeName', 'transform');
                           r.setAttribute('attributeType', 'XML');
                           r.setAttribute('type', 'translate');
                           r.setAttribute('to', initialPos);
                           r.setAttribute('fill', 'freeze');
                           dp.appendChild(r);
                       }
                   });
               }

               const svgStringXML = new XMLSerializer().serializeToString(cloneSvg);
               const svgString = svgStringXML.replace(/ xmlns:a.*?=.*?".*?"/g, '').replace(/ a[^t].*?=.*?".*?"/g, '').replace(/^\s+|\s+$/gm, '').replace(/[\r\n]/gm, '');
               showSvgExportModal(svgString, true);

               jumpToStep(savedStep);
           });

5. SVG Export Modal Display Modification

Locate function showSvgExportModal(svgContent, isAnimation) { and modify it to use the new CSS classes to remove white edges and make it perfectly responsive:

JavaScript

            // Step 9: Render Preview / Download UI Modal
           function showSvgExportModal(svgContent, isAnimation) {
               const blob = new Blob([svgContent], { type: 'image/svg+xml' });
               const url = URL.createObjectURL(blob);
               const size = blob.size;

               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');
               const filename = `ejcees_${isAnimation ? 'animate' : 'static'}_${timestamp}.svg`;

               // Modal Overlay - Using new CSS Class
               const modal = document.createElement('div');
               modal.className = 'ejcees-export-modal';

               // Modal Box Container - Using new CSS Class
               const box = document.createElement('div');
               box.className = 'ejcees-export-box';

               // Preview Container - Using new CSS Class
               const preview = document.createElement('div');
               preview.className = 'ejcees-export-preview';
               preview.innerHTML = svgContent;

               const link = document.createElement('a');
               link.href = url;
               link.download = filename;
               link.textContent = `Download (${size} bytes)`;
               link.style.color = '#66b2ff';
               link.style.textDecoration = 'none';
               link.style.fontSize = '16px';
               link.style.fontWeight = 'bold';
               link.style.padding = '5px 15px';
               link.style.border = '1px solid #66b2ff';
               link.style.borderRadius = '4px';

               const closeBtn = document.createElement('button');
               closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="22" height="22"><path fill="#fff" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
               closeBtn.style.position = 'absolute';
               closeBtn.style.top = '10px';
               closeBtn.style.right = '10px';
               closeBtn.style.background = 'transparent';
               closeBtn.style.border = 'none';
               closeBtn.style.cursor = 'pointer';
               closeBtn.onclick = () => {
                   document.body.removeChild(modal);
                   URL.revokeObjectURL(url);
               };

               box.appendChild(closeBtn);
               box.appendChild(preview);
               box.appendChild(link);
               modal.appendChild(box);
               document.body.appendChild(modal);
           }