From 3826694c159828bdbc1348ad81098f6a108bb314 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 3 Jan 2026 12:40:41 +0100 Subject: [PATCH] import cells --- css/styles.css | 138 ++++++++++++++++++++++++++++++++++++++++++++--- index.html | 56 +++++++++++++++++++ js/app.js | 142 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 326 insertions(+), 10 deletions(-) diff --git a/css/styles.css b/css/styles.css index b4c26ea..003ed1f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -862,18 +862,21 @@ footer a:hover { } dialog { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - max-width: 400px; - width: 90%; - padding: var(--space-xl); - background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); color: var(--text-primary); + background: var(--bg-secondary); + padding: var(--space-xl); + max-width: 400px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +dialog[open] { + display: flex; + flex-direction: column; } dialog::backdrop { @@ -910,6 +913,103 @@ kbd { border-radius: var(--radius-sm); } +/* Import Dialog Styles */ +#import-dialog { + max-width: 650px; + width: 95%; +} + +.import-instructions { + margin-bottom: var(--space-lg); + color: var(--text-secondary); + font-size: 0.9rem; +} + +.import-examples { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-md); + margin: var(--space-md) 0; +} + +.import-example { + background: var(--bg-tertiary); + padding: var(--space-md); + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); +} + +.import-example strong { + display: block; + margin-bottom: var(--space-xs); + color: var(--accent); + font-size: 0.85rem; +} + +.import-example pre { + margin: 0; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-primary); + white-space: pre; +} + +.import-notes { + background: var(--bg-tertiary); + padding: var(--space-md); + border-radius: var(--radius-sm); + border-left: 3px solid var(--accent); + margin-top: var(--space-md); +} + +.import-notes p { + margin: 0 0 var(--space-xs); +} + +.import-notes ul { + margin: var(--space-sm) 0 0; + padding-left: var(--space-lg); + font-size: 0.85rem; +} + +.import-notes li { + margin-bottom: var(--space-xs); +} + +#import-textarea { + width: 100%; + min-height: 200px; + padding: var(--space-md); + font-family: var(--font-mono); + font-size: 0.9rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + resize: vertical; + margin-bottom: var(--space-md); +} + +#import-textarea:focus { + outline: 2px solid var(--accent); + outline-offset: 0; + border-color: var(--accent); +} + +#import-textarea::placeholder { + color: var(--text-muted); +} + +.dialog-buttons { + display: flex; + gap: var(--space-md); + justify-content: flex-end; +} + +.dialog-buttons .btn { + min-width: 100px; +} + @media (max-width: 640px) { .container { padding: var(--space-md); @@ -959,6 +1059,28 @@ kbd { .metric-tooltip::after { left: 20%; } + + dialog { + width: 95%; + max-width: none; + padding: var(--space-lg); + } + + #import-dialog { + max-height: 85vh; + } + + .import-examples { + grid-template-columns: 1fr; + } + + .dialog-buttons { + flex-direction: column-reverse; + } + + .dialog-buttons .btn { + width: 100%; + } } @media (prefers-reduced-motion: reduce) { diff --git a/index.html b/index.html index 6d86d9e..a40bbee 100644 --- a/index.html +++ b/index.html @@ -56,6 +56,9 @@ + @@ -331,6 +334,59 @@ + + +

Import Cell Data

+
+

Paste your cell data below. Supported formats:

+
+
+ Format 1: Capacity only +
2830
+2700
+2840
+
+
+ Format 2: Capacity; IR +
2830; 26
+2700; 25
+2840; 25
+
+
+ Format 3: Label; Capacity +
Cell01; 2830
+Cell02; 2700
+Cell03; 2840
+
+
+ Format 4: Label; Capacity; IR +
Cell01; 2830; 26
+Cell02; 2700; 25
+Cell03; 2840; 25
+
+
+
+

Notes:

+
    +
  • Labels must start with a letter and be max 16 characters
  • +
  • Capacity is in mAh (milliampere-hours)
  • +
  • IR (Internal Resistance) is in mΩ (milliohms) and optional
  • +
  • Use semicolon (;) or comma (,) as separator
  • +
  • Empty lines and invalid entries will be skipped
  • +
+
+
+ +
+ + +
+
+ diff --git a/js/app.js b/js/app.js index e0aca92..1f8e8e7 100644 --- a/js/app.js +++ b/js/app.js @@ -27,6 +27,7 @@ const DOM = { cellTbody: document.getElementById('cell-tbody'), btnAddCell: document.getElementById('btn-add-cell'), btnLoadExample: document.getElementById('btn-load-example'), + btnImport: document.getElementById('btn-import'), btnClearAll: document.getElementById('btn-clear-all'), statCount: document.getElementById('stat-count'), statAvgCap: document.getElementById('stat-avg-cap'), @@ -62,7 +63,11 @@ const DOM = { btnExportCsv: document.getElementById('btn-export-csv'), btnCopyResults: document.getElementById('btn-copy-results'), shortcutsDialog: document.getElementById('shortcuts-dialog'), - btnCloseShortcuts: document.getElementById('btn-close-shortcuts') + btnCloseShortcuts: document.getElementById('btn-close-shortcuts'), + importDialog: document.getElementById('import-dialog'), + importTextarea: document.getElementById('import-textarea'), + btnImportConfirm: document.getElementById('btn-import-confirm'), + btnImportCancel: document.getElementById('btn-import-cancel') }; // ============================================================================= @@ -261,6 +266,132 @@ function loadExampleData() { exampleCells.forEach(cell => addCell(cell)); } +// ============================================================================= +// Import Functionality +// ============================================================================= + +function openImportDialog() { + DOM.importTextarea.value = ''; + DOM.importDialog.showModal(); + DOM.importTextarea.focus(); +} + +function closeImportDialog() { + DOM.importDialog.close(); +} + +function parseImportData(text) { + const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); + const cells = []; + let importedCount = 0; + let skippedCount = 0; + + for (const line of lines) { + // Support both semicolon and comma as separators + const parts = line.split(/[;,]/).map(p => p.trim()); + + if (parts.length === 0) { + skippedCount++; + continue; + } + + let label = null; + let capacity = null; + let ir = null; + + // Parse based on number of columns + if (parts.length === 1) { + // Format: capacity + capacity = parseFloat(parts[0]); + } else if (parts.length === 2) { + // Could be: label; capacity OR capacity; ir + // Check if first part starts with a letter (indicates label) + if (/^[a-zA-Z]/.test(parts[0])) { + // Format: label; capacity + label = parts[0]; + capacity = parseFloat(parts[1]); + } else { + // Format: capacity; ir + capacity = parseFloat(parts[0]); + ir = parseFloat(parts[1]); + } + } else if (parts.length === 3) { + // Format: label; capacity; ir + label = parts[0]; + capacity = parseFloat(parts[1]); + ir = parseFloat(parts[2]); + } else { + // Too many columns, skip + skippedCount++; + continue; + } + + // Validate capacity + if (isNaN(capacity) || capacity <= 0) { + skippedCount++; + continue; + } + + // Validate label if provided + if (label) { + // Must start with letter and be max 16 chars + if (!/^[a-zA-Z]/.test(label) || label.length > 16) { + skippedCount++; + continue; + } + } + + // Validate IR if provided + if (ir !== null && (isNaN(ir) || ir < 0)) { + ir = null; // Invalid IR, ignore it + } + + cells.push({ + label: label, + capacity: capacity, + ir: ir + }); + importedCount++; + } + + return { cells, importedCount, skippedCount }; +} + +function importCells() { + const text = DOM.importTextarea.value; + + if (!text.trim()) { + alert('Please paste some data to import.'); + return; + } + + const { cells, importedCount, skippedCount } = parseImportData(text); + + if (cells.length === 0) { + alert('No valid cells found in the input. Please check the format.'); + return; + } + + //Remove all cells + AppState.cells = []; + AppState.cellIdCounter = 0; + DOM.cellTbody.innerHTML = ''; + updateCellStats(); + updateMatchingButtonState(); + + // Add all imported cells + cells.forEach(cell => addCell(cell)); + + // Show summary + let message = `Successfully imported ${importedCount} cell(s).`; + if (skippedCount > 0) { + message += `\n${skippedCount} line(s) were skipped due to invalid format.`; + } + alert(message); + + closeImportDialog(); +} + // ============================================================================= // Weight Sliders // ============================================================================= @@ -564,7 +695,11 @@ function setupKeyboardShortcuts() { if (e.altKey && e.key === 'a') { e.preventDefault(); addCell(); } if (e.altKey && e.key === 's') { e.preventDefault(); if (!DOM.btnStartMatching.disabled) startMatching(); } if (e.altKey && e.key === 'e') { e.preventDefault(); loadExampleData(); } - if (e.key === 'Escape') { if (AppState.isRunning) stopMatching(); if (DOM.shortcutsDialog.open) DOM.shortcutsDialog.close(); } + if (e.key === 'Escape') { + if (AppState.isRunning) stopMatching(); + if (DOM.shortcutsDialog.open) DOM.shortcutsDialog.close(); + if (DOM.importDialog.open) closeImportDialog(); + } if (e.key === '?' && !e.target.matches('input, textarea')) { e.preventDefault(); DOM.shortcutsDialog.showModal(); } }); } @@ -574,6 +709,7 @@ function initEventListeners() { DOM.cellsParallel.addEventListener('input', updateConfigDisplay); DOM.btnAddCell.addEventListener('click', () => addCell()); DOM.btnLoadExample.addEventListener('click', loadExampleData); + DOM.btnImport.addEventListener('click', openImportDialog); DOM.btnClearAll.addEventListener('click', clearAllCells); DOM.weightCapacity.addEventListener('input', () => updateWeights('capacity')); DOM.weightIr.addEventListener('input', () => updateWeights('ir')); @@ -583,6 +719,8 @@ function initEventListeners() { DOM.btnExportCsv.addEventListener('click', exportCsv); DOM.btnCopyResults.addEventListener('click', copyResults); DOM.btnCloseShortcuts.addEventListener('click', () => DOM.shortcutsDialog.close()); + DOM.btnImportConfirm.addEventListener('click', importCells); + DOM.btnImportCancel.addEventListener('click', closeImportDialog); } // =============================================================================