forked from sheetjs/docs.sheetjs.com
		
	feat: [Electron Demo] - Add CSS styles, loading spinner, worksheet tabs.
This commit is contained in:
		
							parent
							
								
									db305abddb
								
							
						
					
					
						commit
						22d9563f39
					
				@ -231,6 +231,7 @@ The demo project is wired for `electron-forge` to build the standalone binary.
 | 
			
		||||
- [`main.js`](pathname:///electron/main.js) : main process script
 | 
			
		||||
- [`index.html`](pathname:///electron/index.html) : window page
 | 
			
		||||
- [`index.js`](pathname:///electron/index.js) : script loaded in render context
 | 
			
		||||
- [`styles.css`](pathname:///electron/styles.css) : stylesheet
 | 
			
		||||
 | 
			
		||||
:::caution pass
 | 
			
		||||
 | 
			
		||||
@ -248,6 +249,7 @@ curl -LO https://docs.sheetjs.com/electron/package.json
 | 
			
		||||
curl -LO https://docs.sheetjs.com/electron/main.js
 | 
			
		||||
curl -LO https://docs.sheetjs.com/electron/index.html
 | 
			
		||||
curl -LO https://docs.sheetjs.com/electron/index.js
 | 
			
		||||
curl -LO https://docs.sheetjs.com/electron/styles.css
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:::note pass
 | 
			
		||||
@ -265,6 +267,7 @@ curl.exe -LO https://docs.sheetjs.com/electron/package.json
 | 
			
		||||
curl.exe -LO https://docs.sheetjs.com/electron/main.js
 | 
			
		||||
curl.exe -LO https://docs.sheetjs.com/electron/index.html
 | 
			
		||||
curl.exe -LO https://docs.sheetjs.com/electron/index.js
 | 
			
		||||
curl.exe -LO https://docs.sheetjs.com/electron/styles.css
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 25 KiB  | 
@ -7,31 +7,37 @@
 | 
			
		||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https:">
 | 
			
		||||
<meta name="robots" content="noindex">
 | 
			
		||||
<title>SheetJS Electron Demo</title>
 | 
			
		||||
<style>
 | 
			
		||||
#drop{
 | 
			
		||||
  border:2px dashed #bbb;
 | 
			
		||||
  -moz-border-radius:5px;
 | 
			
		||||
  -webkit-border-radius:5px;
 | 
			
		||||
  border-radius:5px;
 | 
			
		||||
  padding:25px;
 | 
			
		||||
  text-align:center;
 | 
			
		||||
  font:20pt bold,"Vollkorn";color:#bbb
 | 
			
		||||
}
 | 
			
		||||
a { text-decoration: none }
 | 
			
		||||
</style>
 | 
			
		||||
<link rel="preconnect" href="https://fonts.googleapis.com">
 | 
			
		||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
			
		||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
 | 
			
		||||
<link rel="stylesheet" href="styles.css">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<pre>
 | 
			
		||||
<b><a href="https://sheetjs.com">SheetJS Electron Demo</a></b>
 | 
			
		||||
<br />
 | 
			
		||||
<button id="readBtn">Click here to select a file from your computer</button><br />
 | 
			
		||||
<div id="drop">Drop a spreadsheet file here to see sheet data</div>
 | 
			
		||||
<input type="file" name="xlfile" id="readIn" /> ... or click here to select a file
 | 
			
		||||
 | 
			
		||||
</pre>
 | 
			
		||||
<p><input type="submit" value="Export Data!" id="exportBtn" disabled="true"></p>
 | 
			
		||||
<div id="htmlout"></div>
 | 
			
		||||
<br />
 | 
			
		||||
<script src="index.js"></script>
 | 
			
		||||
  <div id="spinner-overlay" class="spinner-overlay" style="display:none;"><div class="spinner"></div></div>
 | 
			
		||||
  <header>
 | 
			
		||||
    <h1 class="text-heading">SheetJS Electron Demo</h1>
 | 
			
		||||
    <p class="text-muted text-condensed">Load a spreadsheet file to see it's contents</p>
 | 
			
		||||
  </header>
 | 
			
		||||
  <section class="file-upload" id="drop-container">
 | 
			
		||||
    <div id="drop">
 | 
			
		||||
      <input type="file" id="readIn" style="display:none" tabindex="0" aria-label="Select spreadsheet file">
 | 
			
		||||
      <p class="text-muted text-condensed">Drag and drop a file here</p>
 | 
			
		||||
      <p class="text-muted text-small">or</p>
 | 
			
		||||
      <button type="button" id="readBtn" tabindex="0" aria-label="Open file picker">Click here to select a file</button>
 | 
			
		||||
      <div id="fileStatus" class="file-status" style="display: none;"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
  <section id="htmlout"></section>
 | 
			
		||||
  <section class="export">
 | 
			
		||||
    <p><input type="submit" value="Export" id="exportBtn" disabled="true" tabindex="0" aria-label="Export spreadsheet"></p>
 | 
			
		||||
  </section>
 | 
			
		||||
</body>
 | 
			
		||||
<footer>
 | 
			
		||||
  <ul>
 | 
			
		||||
    <li><a href="https://docs.sheetjs.com/docs/" class="text-condensed">SheetJS CE Docs</a></li>
 | 
			
		||||
    <li><a href="https://www.electronjs.org/docs" class="text-condensed">Electron Docs</a></li>
 | 
			
		||||
  </ul>
 | 
			
		||||
</footer>
 | 
			
		||||
<script src="index.js"></script>
 | 
			
		||||
<!-- Cloudflare Pages Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "5045fe4c2b784ddb8c3c6ee7fa0593e5"}'></script><!-- Cloudflare Pages Analytics --></body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,73 +1,283 @@
 | 
			
		||||
/* sheetjs (C) 2013-present SheetJS -- https://sheetjs.com */
 | 
			
		||||
const XLSX = require('xlsx');
 | 
			
		||||
const electron = require('@electron/remote');
 | 
			
		||||
 | 
			
		||||
/* list of supported extensions */
 | 
			
		||||
// --- Supported Extensions ---
 | 
			
		||||
const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split("|");
 | 
			
		||||
 | 
			
		||||
/* write file with Electron API */
 | 
			
		||||
/**
 | 
			
		||||
 * Export current HTML table as a spreadsheet file using Electron API.
 | 
			
		||||
 */
 | 
			
		||||
async function exportFile() {
 | 
			
		||||
  const HTMLOUT = document.getElementById('htmlout');
 | 
			
		||||
  const wb = XLSX.utils.table_to_book(HTMLOUT.getElementsByTagName("TABLE")[0]);
 | 
			
		||||
  const o = await electron.dialog.showSaveDialog({
 | 
			
		||||
    title: 'Save file as',
 | 
			
		||||
    filters: [{
 | 
			
		||||
      name: "Spreadsheets",
 | 
			
		||||
      extensions: EXTENSIONS
 | 
			
		||||
    }]
 | 
			
		||||
    filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }]
 | 
			
		||||
  });
 | 
			
		||||
  XLSX.writeFile(wb, o.filePath);
 | 
			
		||||
  electron.dialog.showMessageBox({ message: "Exported data to " + o.filePath, buttons: ["OK"] });
 | 
			
		||||
}
 | 
			
		||||
document.getElementById('exportBtn').addEventListener('click', exportFile, false);
 | 
			
		||||
 | 
			
		||||
/* common handler to create HTML tables on the page */
 | 
			
		||||
function process_wb(wb) {
 | 
			
		||||
  const HTMLOUT = document.getElementById('htmlout');
 | 
			
		||||
  const XPORT = document.getElementById('exportBtn');
 | 
			
		||||
  XPORT.disabled = false;
 | 
			
		||||
  HTMLOUT.innerHTML = "";
 | 
			
		||||
  wb.SheetNames.forEach(function(sheetName) {
 | 
			
		||||
    const htmlstr = XLSX.utils.sheet_to_html(wb.Sheets[sheetName],{editable:true});
 | 
			
		||||
    HTMLOUT.innerHTML += htmlstr;
 | 
			
		||||
/**
 | 
			
		||||
 * Create a <thead> element for a table from a header row array.
 | 
			
		||||
 */
 | 
			
		||||
function createTableHead(headerRow) {
 | 
			
		||||
  const thead = document.createElement('thead');
 | 
			
		||||
  const headRow = document.createElement('tr');
 | 
			
		||||
  headerRow.forEach(cell => {
 | 
			
		||||
    const th = document.createElement('th');
 | 
			
		||||
    th.textContent = cell !== undefined ? cell : '';
 | 
			
		||||
    headRow.appendChild(th);
 | 
			
		||||
  });
 | 
			
		||||
  thead.appendChild(headRow);
 | 
			
		||||
  return thead;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* read file with Electron API */
 | 
			
		||||
/**
 | 
			
		||||
 * Create a <tbody> element for a table from a 2D data array.
 | 
			
		||||
 */
 | 
			
		||||
function createTableBody(data) {
 | 
			
		||||
  const tbody = document.createElement('tbody');
 | 
			
		||||
  for (let i = 1; i < data.length; ++i) {
 | 
			
		||||
    const row = data[i];
 | 
			
		||||
    const tr = document.createElement('tr');
 | 
			
		||||
    row.forEach(cell => {
 | 
			
		||||
      const td = document.createElement('td');
 | 
			
		||||
      td.textContent = cell !== undefined ? cell : '';
 | 
			
		||||
      tr.appendChild(td);
 | 
			
		||||
    });
 | 
			
		||||
    tbody.appendChild(tr);
 | 
			
		||||
  }
 | 
			
		||||
  return tbody;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrap a table in a responsive container div.
 | 
			
		||||
 */
 | 
			
		||||
function createResponsiveDiv(table) {
 | 
			
		||||
  const responsiveDiv = document.createElement('div');
 | 
			
		||||
  responsiveDiv.className = 'table-responsive';
 | 
			
		||||
  responsiveDiv.appendChild(table);
 | 
			
		||||
  return responsiveDiv;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Main Table Processing ---
 | 
			
		||||
/**
 | 
			
		||||
 * Render all sheets of a workbook as HTML tables with clickable tabs.
 | 
			
		||||
 */
 | 
			
		||||
function renderWorkbookToTables(wb) {
 | 
			
		||||
  const HTMLOUT = document.getElementById('htmlout');
 | 
			
		||||
  const exportBtn = document.getElementById('exportBtn');
 | 
			
		||||
  exportBtn.disabled = false;
 | 
			
		||||
  HTMLOUT.innerHTML = "";
 | 
			
		||||
 | 
			
		||||
  // Create tab container
 | 
			
		||||
  const tabContainer = document.createElement('div');
 | 
			
		||||
  tabContainer.className = 'sheetjs-tab-container';
 | 
			
		||||
 | 
			
		||||
  // Create content container
 | 
			
		||||
  const contentContainer = document.createElement('div');
 | 
			
		||||
  contentContainer.className = 'sheetjs-tab-content';
 | 
			
		||||
 | 
			
		||||
  // Store tables for each sheet
 | 
			
		||||
  const tables = [];
 | 
			
		||||
 | 
			
		||||
  wb.SheetNames.forEach(function(sheetName, idx) {
 | 
			
		||||
    // Create tab button
 | 
			
		||||
    const tab = document.createElement('button');
 | 
			
		||||
    tab.className = 'sheetjs-tab-btn text-small';
 | 
			
		||||
    tab.textContent = sheetName;
 | 
			
		||||
    tab.setAttribute('data-sheet-idx', idx);
 | 
			
		||||
    if(idx === 0) tab.classList.add('active');
 | 
			
		||||
    tab.addEventListener('click', function() {
 | 
			
		||||
      // Remove active from all tabs
 | 
			
		||||
      Array.from(tabContainer.children).forEach(btn => btn.classList.remove('active'));
 | 
			
		||||
      tab.classList.add('active');
 | 
			
		||||
      // Hide all tables and show the selected one
 | 
			
		||||
      tables.forEach((tableDiv, tIdx) => {
 | 
			
		||||
        tableDiv.style.display = (tIdx === idx) ? '' : 'none';
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    tabContainer.appendChild(tab);
 | 
			
		||||
 | 
			
		||||
    // Create table for this sheet
 | 
			
		||||
    const sheet = wb.Sheets[sheetName];
 | 
			
		||||
    const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
 | 
			
		||||
    if (data.length === 0) {
 | 
			
		||||
      const emptyDiv = document.createElement('div');
 | 
			
		||||
      emptyDiv.textContent = `Sheet '${sheetName}' is empty.`;
 | 
			
		||||
      tables.push(emptyDiv);
 | 
			
		||||
      contentContainer.appendChild(emptyDiv);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const table = document.createElement('table');
 | 
			
		||||
    table.className = 'sheetjs-table';
 | 
			
		||||
    // Remove caption, handled by tab
 | 
			
		||||
    table.appendChild(createTableHead(data[0]));
 | 
			
		||||
    table.appendChild(createTableBody(data));
 | 
			
		||||
    const responsiveDiv = createResponsiveDiv(table);
 | 
			
		||||
    if(idx !== 0) responsiveDiv.style.display = 'none';
 | 
			
		||||
    tables.push(responsiveDiv);
 | 
			
		||||
    contentContainer.appendChild(responsiveDiv);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Only show tabs if more than one sheet
 | 
			
		||||
  if (wb.SheetNames.length > 1) {
 | 
			
		||||
    HTMLOUT.appendChild(tabContainer);
 | 
			
		||||
  }
 | 
			
		||||
  HTMLOUT.appendChild(contentContainer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- File Import Logic ---
 | 
			
		||||
/**
 | 
			
		||||
 * Handle file selection dialog and render the selected spreadsheet.
 | 
			
		||||
 */
 | 
			
		||||
async function handleReadBtn() {
 | 
			
		||||
  const o = await electron.dialog.showOpenDialog({
 | 
			
		||||
    title: 'Select a file',
 | 
			
		||||
    filters: [{
 | 
			
		||||
      name: "Spreadsheets",
 | 
			
		||||
      extensions: EXTENSIONS
 | 
			
		||||
    }],
 | 
			
		||||
    filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }],
 | 
			
		||||
    properties: ['openFile']
 | 
			
		||||
  });
 | 
			
		||||
  if(o.filePaths.length == 0) throw new Error("No file was selected!");
 | 
			
		||||
  process_wb(XLSX.readFile(o.filePaths[0]));
 | 
			
		||||
  showSpinner();
 | 
			
		||||
  // yield to event loop to render spinner
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 200));
 | 
			
		||||
  try {
 | 
			
		||||
    const filePath = o.filePaths[0];
 | 
			
		||||
    const fileName = filePath.split(/[/\\]/).pop();
 | 
			
		||||
    renderWorkbookToTables(XLSX.readFile(filePath));
 | 
			
		||||
    showLoadedFileUI(fileName);
 | 
			
		||||
  } finally {
 | 
			
		||||
    hideSpinner();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
document.getElementById('readBtn').addEventListener('click', handleReadBtn, false);
 | 
			
		||||
 | 
			
		||||
/* read file with Web APIs */
 | 
			
		||||
// --- UI Templates and Helpers ---
 | 
			
		||||
/**
 | 
			
		||||
 * Return HTML for the drag-and-drop area.
 | 
			
		||||
 */
 | 
			
		||||
function getDropAreaHTML() {
 | 
			
		||||
  return `<div id="drop">
 | 
			
		||||
    <input type="file" id="readIn" style="display:none">
 | 
			
		||||
    <p class="text-muted text-condensed">Drag and drop a file here</p>
 | 
			
		||||
    <p class="text-muted text-small">or</p>
 | 
			
		||||
    <button type="button" id="readBtn">Click here to select a file</button>
 | 
			
		||||
    <div id="fileStatus" class="file-status" style="display: none;"></div>
 | 
			
		||||
  </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Return HTML for the file-loaded UI.
 | 
			
		||||
 */
 | 
			
		||||
function getFileLoadedHTML(fileName) {
 | 
			
		||||
  return `<div class="file-loaded">
 | 
			
		||||
    <span class="file-name text-muted text-small">${fileName}</span>
 | 
			
		||||
    <button type="button" id="unloadBtn" class="unload-btn">Unload</button>
 | 
			
		||||
  </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Update the drop-container's inner HTML.
 | 
			
		||||
 */
 | 
			
		||||
function updateDropContainer(html) {
 | 
			
		||||
  document.getElementById('drop-container').innerHTML = html;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Restore the original drag-and-drop UI and reset state.
 | 
			
		||||
 */
 | 
			
		||||
function restoreDropUI() {
 | 
			
		||||
  updateDropContainer(getDropAreaHTML());
 | 
			
		||||
  // Remove the table/output
 | 
			
		||||
  const htmlout = document.getElementById('htmlout');
 | 
			
		||||
  if (htmlout) htmlout.innerHTML = '';
 | 
			
		||||
  // Reset export button state
 | 
			
		||||
  const exportBtn = document.getElementById('exportBtn');
 | 
			
		||||
  if (exportBtn) {
 | 
			
		||||
    exportBtn.disabled = true;
 | 
			
		||||
    exportBtn.classList.add('disabled');
 | 
			
		||||
  }
 | 
			
		||||
  // Re-attach event listeners after restoring UI
 | 
			
		||||
  attachDropListeners();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Show UI for loaded file and attach unload handler.
 | 
			
		||||
 */
 | 
			
		||||
function showLoadedFileUI(fileName) {
 | 
			
		||||
  updateDropContainer(getFileLoadedHTML(fileName));
 | 
			
		||||
  document.getElementById('unloadBtn').addEventListener('click', restoreDropUI);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Event Listener Helpers ---
 | 
			
		||||
/**
 | 
			
		||||
 * Add an event listener to an element if it exists.
 | 
			
		||||
 */
 | 
			
		||||
function addListener(id, event, handler) {
 | 
			
		||||
  const el = document.getElementById(id);
 | 
			
		||||
  if (el) el.addEventListener(event, handler, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attach drag-and-drop and file input listeners to the UI.
 | 
			
		||||
 */
 | 
			
		||||
function attachDropListeners() {
 | 
			
		||||
  addListener('readIn', 'change', (e) => {
 | 
			
		||||
    showSpinner();
 | 
			
		||||
    // Defer to next tick to ensure spinner renders before heavy work
 | 
			
		||||
    setTimeout(() => readFile(e.target.files), 0);
 | 
			
		||||
  });
 | 
			
		||||
  addListener('readBtn', 'click', handleReadBtn);
 | 
			
		||||
  const drop = document.getElementById('drop');
 | 
			
		||||
  const dropContainer = document.getElementById('drop-container');
 | 
			
		||||
  if (!drop || !dropContainer) return;
 | 
			
		||||
  const handleDrag = (e) => {
 | 
			
		||||
    e.stopPropagation(); e.preventDefault();
 | 
			
		||||
    e.dataTransfer.dropEffect = 'copy';
 | 
			
		||||
    dropContainer.classList.add('drag-over');
 | 
			
		||||
  };
 | 
			
		||||
  const handleDragLeave = (e) => {
 | 
			
		||||
    e.stopPropagation(); e.preventDefault();
 | 
			
		||||
    dropContainer.classList.remove('drag-over');
 | 
			
		||||
  };
 | 
			
		||||
  drop.addEventListener('drop', (e) => {
 | 
			
		||||
    e.stopPropagation(); e.preventDefault();
 | 
			
		||||
    dropContainer.classList.remove('drag-over');
 | 
			
		||||
    readFile(e.dataTransfer.files);
 | 
			
		||||
  }, false);
 | 
			
		||||
  drop.addEventListener('dragenter', handleDrag, false);
 | 
			
		||||
  drop.addEventListener('dragover', handleDrag, false);
 | 
			
		||||
  drop.addEventListener('dragleave', handleDragLeave, false);
 | 
			
		||||
  drop.addEventListener('dragend', handleDragLeave, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Spinner Helpers ---
 | 
			
		||||
function showSpinner() {
 | 
			
		||||
  const spinner = document.getElementById('spinner-overlay');
 | 
			
		||||
  if (spinner) spinner.style.display = 'flex';
 | 
			
		||||
}
 | 
			
		||||
function hideSpinner() {
 | 
			
		||||
  const spinner = document.getElementById('spinner-overlay');
 | 
			
		||||
  if (spinner) spinner.style.display = 'none';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- File Reader for Drag-and-Drop and Input ---
 | 
			
		||||
/**
 | 
			
		||||
 * Read file(s) from input or drag-and-drop and render as table.
 | 
			
		||||
 */
 | 
			
		||||
async function readFile(files) {
 | 
			
		||||
  if (!files || files.length === 0) return;
 | 
			
		||||
  const f = files[0];
 | 
			
		||||
  const data = await f.arrayBuffer();
 | 
			
		||||
  process_wb(XLSX.read(data));
 | 
			
		||||
  showSpinner();
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await f.arrayBuffer();
 | 
			
		||||
    renderWorkbookToTables(XLSX.read(data));
 | 
			
		||||
    showLoadedFileUI(f.name);
 | 
			
		||||
  } finally {
 | 
			
		||||
    hideSpinner();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// file input element
 | 
			
		||||
document.getElementById('readIn').addEventListener('change', (e) => { readFile(e.target.files); }, false);
 | 
			
		||||
 | 
			
		||||
// drag and drop
 | 
			
		||||
const drop = document.getElementById('drop');
 | 
			
		||||
drop.addEventListener('drop', (e) => {
 | 
			
		||||
  e.stopPropagation(); e.preventDefault();
 | 
			
		||||
  readFile(e.dataTransfer.files);
 | 
			
		||||
}, false);
 | 
			
		||||
 | 
			
		||||
const handleDrag = (e) => {
 | 
			
		||||
  e.stopPropagation(); e.preventDefault();
 | 
			
		||||
  e.dataTransfer.dropEffect = 'copy';
 | 
			
		||||
};
 | 
			
		||||
drop.addEventListener('dragenter', handleDrag, false);
 | 
			
		||||
drop.addEventListener('dragover', handleDrag, false);
 | 
			
		||||
// --- Initial Setup ---
 | 
			
		||||
attachDropListeners();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										350
									
								
								docz/static/electron/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										350
									
								
								docz/static/electron/styles.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,350 @@
 | 
			
		||||
/* =====================
 | 
			
		||||
   Root Variables
 | 
			
		||||
   ===================== */
 | 
			
		||||
:root {
 | 
			
		||||
    --text-base: #212529;
 | 
			
		||||
    --text-muted: #666363;
 | 
			
		||||
    --text-accent: #0c9244;
 | 
			
		||||
    --button-primary: #212529;
 | 
			
		||||
    --button-text: #fff;
 | 
			
		||||
    --button-primary-hover: #0c9244;
 | 
			
		||||
    --button-primary-active: #075025;
 | 
			
		||||
    --button-primary-disabled: #6c757d;
 | 
			
		||||
    --white: #fff;
 | 
			
		||||
    --black: #000;
 | 
			
		||||
    --table-even: #f6f6f6;
 | 
			
		||||
    --table-hover: #88dda98f;
 | 
			
		||||
    --table-head: #f8f9faaf;
 | 
			
		||||
    --danger: #c0392b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Global Styles & Reset
 | 
			
		||||
   ===================== */
 | 
			
		||||
body, html {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  font-family: "Roboto", sans-serif;
 | 
			
		||||
  font-optical-sizing: auto;
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: stretch;
 | 
			
		||||
  justify-content: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
li {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Typography
 | 
			
		||||
   ===================== */
 | 
			
		||||
h1, p {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    color: var(--text-base);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-muted {
 | 
			
		||||
    color: var(--text-muted) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-condensed {
 | 
			
		||||
    font-family: "Roboto Condensed", sans-serif;
 | 
			
		||||
    font-optical-sizing: auto;
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-small {
 | 
			
		||||
    font-size: 0.875rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Links
 | 
			
		||||
   ===================== */
 | 
			
		||||
a {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    color: var(--text-base);
 | 
			
		||||
}
 | 
			
		||||
a:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
    color: var(--text-accent);
 | 
			
		||||
}   
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Header
 | 
			
		||||
   ===================== */
 | 
			
		||||
header {
 | 
			
		||||
    margin: 4rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Buttons & Inputs
 | 
			
		||||
   ===================== */
 | 
			
		||||
button, input[type="submit"] {
 | 
			
		||||
    background-color: var(--button-primary);
 | 
			
		||||
    border: none;
 | 
			
		||||
    padding: 0.5rem 1rem;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: background-color 0.2s ease;
 | 
			
		||||
    color: var(--button-text);
 | 
			
		||||
}
 | 
			
		||||
button:hover, input[type="submit"]:hover {
 | 
			
		||||
    background-color: var(--button-primary-hover);
 | 
			
		||||
}
 | 
			
		||||
button:active, input[type="submit"]:active {
 | 
			
		||||
    background-color: var(--button-primary-active);
 | 
			
		||||
}
 | 
			
		||||
button:disabled, input[type="submit"]:disabled {
 | 
			
		||||
    background-color: var(--button-primary-disabled);
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
    opacity: 0.3;
 | 
			
		||||
}
 | 
			
		||||
input[type="file"] {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button:focus, input[type="submit"]:focus, input[type="file"]:focus {
 | 
			
		||||
    outline: 3px solid var(--text-accent);
 | 
			
		||||
    outline-offset: 2px;
 | 
			
		||||
    box-shadow: 0 0 0 2px var(--text-accent);
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   File Upload
 | 
			
		||||
   ===================== */
 | 
			
		||||
.file-upload {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin: 1rem auto;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: 0.5rem;
 | 
			
		||||
    border: 1px dashed var(--text-muted);
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    width: 75%;
 | 
			
		||||
    max-width: 600px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: var(--white);
 | 
			
		||||
}
 | 
			
		||||
.file-upload input[type="file"] {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
.file-upload.drag-over {
 | 
			
		||||
    border-color: var(--text-accent);
 | 
			
		||||
    background-color: #e6f8ee;
 | 
			
		||||
    box-shadow: 0 0 0 2px var(--text-accent);
 | 
			
		||||
    transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#drop {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Export Section
 | 
			
		||||
   ===================== */
 | 
			
		||||
.export {
 | 
			
		||||
    margin: 2rem 0;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Footer
 | 
			
		||||
   ===================== */
 | 
			
		||||
footer {
 | 
			
		||||
    background-color: #eee;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
footer ul {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: 1rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
footer li {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Responsive Table Container
 | 
			
		||||
   ===================== */
 | 
			
		||||
.table-responsive {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  -webkit-overflow-scrolling: touch;
 | 
			
		||||
  margin: 2rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   SheetJS Table Styles
 | 
			
		||||
   ===================== */
 | 
			
		||||
.sheetjs-table {
 | 
			
		||||
  width: 80%;
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
  max-width: 900px;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  font-family: "Roboto", "Roboto Condensed", sans-serif;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
#htmlout {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table caption {
 | 
			
		||||
  caption-side: top;
 | 
			
		||||
  font-family: "Roboto Condensed", sans-serif;
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
  color: var(--text-accent);
 | 
			
		||||
  padding: 0.5rem;
 | 
			
		||||
  letter-spacing: 0.05em;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table thead {
 | 
			
		||||
  background: var(--table-head);
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table th, .sheetjs-table td {
 | 
			
		||||
  padding: 0.25rem 1rem;
 | 
			
		||||
  border: 1px solid var(--text-muted);
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table th {
 | 
			
		||||
  color: var(--text-base);
 | 
			
		||||
  font-family: "Roboto Condensed", sans-serif;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  letter-spacing: 0.03em;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table tbody tr:nth-child(even) {
 | 
			
		||||
  background: var(--table-even);
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-table tbody tr:hover {
 | 
			
		||||
  background: var(--table-hover);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   File Status/Loaded/Unload
 | 
			
		||||
   ===================== */
 | 
			
		||||
.file-status {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  color: var(--gray-888);
 | 
			
		||||
  min-height: 1.5em;
 | 
			
		||||
  transition: color 0.2s;
 | 
			
		||||
}
 | 
			
		||||
.file-loaded {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding: 1em;
 | 
			
		||||
}
 | 
			
		||||
.unload-btn:hover {
 | 
			
		||||
  background: var(--danger);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Sheet Tabs
 | 
			
		||||
   ===================== */
 | 
			
		||||
.sheetjs-tab-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 0.5rem;
 | 
			
		||||
  margin: 1rem auto 0.5rem auto;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-tab-btn {
 | 
			
		||||
  background: var(--button-primary);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 0.5rem 1.2rem;
 | 
			
		||||
  border-radius: 6px 6px 0 0;
 | 
			
		||||
  font-family: "Roboto Condensed", sans-serif;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  color: var(--button-text);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: background 0.18s, color 0.18s, box-shadow 0.18s;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  top: 2px;
 | 
			
		||||
  box-shadow: 0 2px 6px rgba(12, 146, 68, 0.05);
 | 
			
		||||
  opacity: 0.94;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-tab-btn.active, .sheetjs-tab-btn:focus {
 | 
			
		||||
  background: var(--text-accent);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  z-index: 2;
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(12, 146, 68, 0.12);
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-tab-btn:hover:not(.active) {
 | 
			
		||||
  background: var(--button-primary-hover);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
.sheetjs-tab-content {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Spinner Styles */
 | 
			
		||||
.spinner-overlay {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0; left: 0;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  background: rgba(255,255,255,0.6);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  z-index: 10000;
 | 
			
		||||
}
 | 
			
		||||
.spinner {
 | 
			
		||||
  border: 6px solid #f3f3f3;
 | 
			
		||||
  border-top: 6px solid var(--text-accent);
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  width: 48px;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% { transform: rotate(0deg); }
 | 
			
		||||
  100% { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* =====================
 | 
			
		||||
   Media Queries
 | 
			
		||||
   ===================== */
 | 
			
		||||
@media (max-width: 700px) {
 | 
			
		||||
  .sheetjs-table th, .sheetjs-table td {
 | 
			
		||||
    padding: 0.25rem 0.5rem;
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
  }
 | 
			
		||||
  .sheetjs-table caption {
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    padding: 0.25rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user