diff --git a/docz/static/electron/ew.png b/docz/static/electron/ew.png
index 8b9db9f..5fa2f3f 100644
Binary files a/docz/static/electron/ew.png and b/docz/static/electron/ew.png differ
diff --git a/docz/static/electron/index.html b/docz/static/electron/index.html
index 8fcafe1..78c6499 100644
--- a/docz/static/electron/index.html
+++ b/docz/static/electron/index.html
@@ -16,18 +16,18 @@
Drag and drop a file here
or
-
-
+
-
+
+
diff --git a/docz/static/electron/index.js b/docz/static/electron/index.js
index 524953d..1011c16 100644
--- a/docz/static/electron/index.js
+++ b/docz/static/electron/index.js
@@ -1,132 +1,57 @@
-const XLSX = require('xlsx');
-const electron = require('@electron/remote');
+const XLSX = require("xlsx");
+const electron = require("@electron/remote");
// --- Supported Extensions ---
-const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split("|");
+const EXTENSIONS =
+ "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split(
+ "|"
+ );
+
+const dropContainer = document.getElementById("drop-container");
+const dropzone = document.getElementById("drop");
+const fileStatus = document.getElementById("fileStatus");
+const exportBtn = document.getElementById("exportBtn");
+const spinnerOverlay = document.getElementById("spinner-overlay");
+const htmlout = document.getElementById("htmlout");
+
+// open external links in default browser
+document.addEventListener("click", (e) => {
+ if (e.target.tagName === "A" && e.target.href.startsWith("http")) {
+ e.preventDefault();
+ electron.shell.openExternal(e.target.href);
+ }
+});
/**
* 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 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 }]
+ title: "Save file as",
+ 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);
-
-/**
- * Create a 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);
+ electron.dialog.showMessageBox({
+ message: "Exported data to " + o.filePath,
+ buttons: ["OK"],
});
- thead.appendChild(headRow);
- return thead;
}
-/**
- * Create a 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;
-}
+exportBtn.addEventListener("click", exportFile, false);
-/**
- * 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
+ htmlout.innerHTML = "";
+ const sheetNames = wb.SheetNames;
+ sheetNames.forEach((sheetName) => {
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);
+ const sheetname = sheetName;
+ const table = XLSX.utils.sheet_to_html(sheet);
+ htmlout.innerHTML += `
+ ${sheetname}
+ ${table}
+ `;
});
-
- // Only show tabs if more than one sheet
- if (wb.SheetNames.length > 1) {
- HTMLOUT.appendChild(tabContainer);
- }
- HTMLOUT.appendChild(contentContainer);
}
// --- File Import Logic ---
@@ -135,79 +60,81 @@ function renderWorkbookToTables(wb) {
*/
async function handleReadBtn() {
const o = await electron.dialog.showOpenDialog({
- title: 'Select a file',
+ title: "Select a file",
filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }],
- properties: ['openFile']
+ properties: ["openFile"],
});
- if(o.filePaths.length == 0) throw new Error("No file was selected!");
+ if (o.filePaths.length == 0) throw new Error("No file was selected!");
showSpinner();
// yield to event loop to render spinner
- await new Promise(resolve => setTimeout(resolve, 200));
+ await new Promise((resolve) => setTimeout(resolve, 200));
try {
const filePath = o.filePaths[0];
const fileName = filePath.split(/[/\\]/).pop();
renderWorkbookToTables(XLSX.readFile(filePath));
showLoadedFileUI(fileName);
+ showExportBtn();
} finally {
hideSpinner();
+ hideDropUI();
}
}
// --- UI Templates and Helpers ---
-/**
- * Return HTML for the drag-and-drop area.
- */
-function getDropAreaHTML() {
- return `
-
-
Drag and drop a file here
-
or
-
-
-
`;
-}
-/**
- * Return HTML for the file-loaded UI.
- */
-function getFileLoadedHTML(fileName) {
+const getLoadedFileUI = (fileName) => {
return `
- ${fileName}
-
-
`;
-}
+ ${fileName}
+
+ `;
+};
-/**
- * Update the drop-container's inner HTML.
- */
-function updateDropContainer(html) {
- document.getElementById('drop-container').innerHTML = html;
-}
+const hideDropUI = () => {
+ if (dropContainer) dropContainer.style.display = "none";
+};
-/**
- * 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');
+const showDropUI = () => {
+ if (dropContainer) dropContainer.style.display = "block";
+};
+
+const hideLoadedFileUI = () => {
+ if (fileStatus) fileStatus.innerHTML = "";
+};
+
+const hideOutputUI = () => {
+ if (htmlout) htmlout.innerHTML = "";
+};
+
+const showLoadedFileUI = (fileName) => {
+ const loadedFileUI = getLoadedFileUI(fileName);
+ fileStatus.innerHTML = loadedFileUI;
+ const unloadBtn = fileStatus.querySelector("#unloadBtn");
+ if (unloadBtn) {
+ unloadBtn.addEventListener("click", () => {
+ hideLoadedFileUI();
+ hideExportBtn();
+ showDropUI();
+ hideOutputUI();
+ });
}
- // Re-attach event listeners after restoring UI
- attachDropListeners();
+ hideDropUI();
+ showExportBtn();
+};
+
+const hideExportBtn = () => {
+ if (exportBtn) exportBtn.disabled = true;
+};
+
+const showExportBtn = () => {
+ if (exportBtn) exportBtn.disabled = false;
+};
+
+function showSpinner() {
+ if (spinnerOverlay) spinnerOverlay.style.display = "flex";
}
-/**
- * Show UI for loaded file and attach unload handler.
- */
-function showLoadedFileUI(fileName) {
- updateDropContainer(getFileLoadedHTML(fileName));
- document.getElementById('unloadBtn').addEventListener('click', restoreDropUI);
+function hideSpinner() {
+ if (spinnerOverlay) spinnerOverlay.style.display = "none";
}
// --- Event Listener Helpers ---
@@ -223,43 +150,38 @@ function addListener(id, event, handler) {
* Attach drag-and-drop and file input listeners to the UI.
*/
function attachDropListeners() {
- addListener('readIn', 'change', (e) => {
+ 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;
+ addListener("readBtn", "click", handleReadBtn);
+ if (!dropzone || !dropContainer) return;
const handleDrag = (e) => {
- e.stopPropagation(); e.preventDefault();
- e.dataTransfer.dropEffect = 'copy';
- dropContainer.classList.add('drag-over');
+ 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');
+ 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';
+ dropzone.addEventListener(
+ "drop",
+ (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ dropContainer.classList.remove("drag-over");
+ readFile(e.dataTransfer.files);
+ },
+ false
+ );
+ dropzone.addEventListener("dragenter", handleDrag, false);
+ dropzone.addEventListener("dragover", handleDrag, false);
+ dropzone.addEventListener("dragleave", handleDragLeave, false);
+ dropzone.addEventListener("dragend", handleDragLeave, false);
}
// --- File Reader for Drag-and-Drop and Input ---
diff --git a/docz/static/electron/styles.css b/docz/static/electron/styles.css
index 99405ab..f9c01a4 100644
--- a/docz/static/electron/styles.css
+++ b/docz/static/electron/styles.css
@@ -85,7 +85,7 @@ a:hover {
Header
===================== */
header {
- margin: 4rem 0;
+ margin-top: 2rem;
}
/* =====================
@@ -197,13 +197,12 @@ footer li {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
- margin: 2rem 0;
}
/* =====================
SheetJS Table Styles
===================== */
-.sheetjs-table {
+table {
width: 80%;
padding: 1rem;
max-width: 900px;
@@ -211,11 +210,10 @@ footer li {
margin: 0 auto;
font-family: "Roboto", "Roboto Condensed", sans-serif;
overflow: hidden;
+ margin-bottom: 2rem;
}
-#htmlout {
- padding: 1rem;
-}
-.sheetjs-table caption {
+
+table caption {
caption-side: top;
font-family: "Roboto Condensed", sans-serif;
font-size: 1.2rem;
@@ -223,35 +221,54 @@ footer li {
padding: 0.5rem;
letter-spacing: 0.05em;
}
-.sheetjs-table thead {
+
+table thead {
background: var(--table-head);
}
-.sheetjs-table th, .sheetjs-table td {
+
+table th, table td {
padding: 0.25rem 1rem;
border: 1px solid var(--text-muted);
text-align: left;
font-size: 1rem;
}
-.sheetjs-table th {
+
+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) {
+
+table tbody tr:nth-child(even) {
background: var(--table-even);
}
-.sheetjs-table tbody tr:hover {
+
+table tbody tr:hover {
background: var(--table-hover);
}
+.sheetjs-sheet-name {
+ font-family: "Roboto Condensed", sans-serif;
+ color: var(--text-accent);
+ padding: 0.5rem;
+ font-weight: 600;
+}
+
+.sheetjs-sheet-container {
+ margin: 1rem auto;
+ width: 90%;
+ max-width: 900px;
+}
+
+
+
/* =====================
File Status/Loaded/Unload
===================== */
.file-status {
margin-top: 10px;
font-size: 1rem;
- color: var(--gray-888);
min-height: 1.5em;
transition: color 0.2s;
}
@@ -259,55 +276,14 @@ footer li {
display: flex;
gap: 1rem;
align-items: center;
- justify-content: space-between;
- padding: 1em;
+ justify-content: center;
+ margin-bottom: 1rem;
}
.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;
@@ -328,6 +304,7 @@ footer li {
height: 48px;
animation: spin 1s linear infinite;
}
+
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
@@ -337,11 +314,11 @@ footer li {
Media Queries
===================== */
@media (max-width: 700px) {
- .sheetjs-table th, .sheetjs-table td {
+ table th, table td {
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
- .sheetjs-table caption {
+ table caption {
font-size: 1rem;
padding: 0.25rem;
}