From 76627ae9f035cb19f5b9d7ec00d66dae633e17dc Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 11 Jan 2026 12:01:59 -0300 Subject: [PATCH] feat(suite): Enhanced UI for Sheet, Docs, and Slides editors --- ui/suite/docs/docs.css | 371 +++++++++ ui/suite/docs/docs.html | 296 +++++++ ui/suite/docs/docs.js | 510 ++++++++++++ ui/suite/sheet/sheet.css | 746 +++++++++++++++++ ui/suite/sheet/sheet.html | 716 +++++++++++++++- ui/suite/sheet/sheet.js | 1539 ++++++++++++++++++++++++++++++++++- ui/suite/slides/slides.css | 811 +++++++++++++++++- ui/suite/slides/slides.html | 617 ++++++++++++++ ui/suite/slides/slides.js | 896 ++++++++++++++++++++ 9 files changed, 6475 insertions(+), 27 deletions(-) diff --git a/ui/suite/docs/docs.css b/ui/suite/docs/docs.css index 179628e..1909ba0 100644 --- a/ui/suite/docs/docs.css +++ b/ui/suite/docs/docs.css @@ -1098,6 +1098,377 @@ background: rgba(66, 133, 244, 0.3); } +/* ============================================================================= + FIND & REPLACE MODAL + ============================================================================= */ + +.find-replace-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.find-replace-group label { + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); +} + +.find-replace-group input { + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.find-replace-group input:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.find-replace-options { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--sentient-text-primary, #212121); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--sentient-accent, #4285f4); +} + +.find-results { + padding: 10px 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + color: var(--sentient-text-secondary, #666); + margin-bottom: 16px; +} + +.find-highlight { + background: #ffeb3b; + color: #000; +} + +.find-highlight.current { + background: #ff9800; +} + +/* ============================================================================= + PRINT PREVIEW MODAL + ============================================================================= */ + +.modal-fullscreen { + width: 95vw; + max-width: 1200px; + height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-fullscreen .modal-header { + flex-shrink: 0; +} + +.modal-fullscreen .modal-body { + flex: 1; + overflow: hidden; + padding: 0; +} + +.print-toolbar { + display: flex; + align-items: center; + gap: 16px; + flex: 1; + margin: 0 24px; +} + +.print-toolbar select { + padding: 6px 10px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.print-toolbar .checkbox-label { + font-size: 12px; +} + +.print-preview-body { + display: flex; + justify-content: center; + align-items: flex-start; + background: var(--sentient-bg-tertiary, #e0e0e0); + overflow: auto; + padding: 24px; +} + +.print-preview-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +.print-page { + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 48px; + display: flex; + flex-direction: column; +} + +.print-page.portrait { + width: 8.5in; + min-height: 11in; +} + +.print-page.landscape { + width: 11in; + min-height: 8.5in; +} + +.print-header, +.print-footer { + font-size: 10pt; + color: #666; + text-align: center; + padding: 8px 0; +} + +.print-header { + border-bottom: 1px solid #e0e0e0; + margin-bottom: 16px; +} + +.print-footer { + border-top: 1px solid #e0e0e0; + margin-top: auto; + padding-top: 16px; +} + +.print-content { + flex: 1; + font-size: 12pt; + line-height: 1.6; +} + +/* ============================================================================= + PAGE BREAK + ============================================================================= */ + +.page-break { + display: block; + width: 100%; + height: 2px; + margin: 24px 0; + background: linear-gradient( + 90deg, + transparent 0%, + var(--sentient-border, #e0e0e0) 10%, + var(--sentient-border, #e0e0e0) 90%, + transparent 100% + ); + position: relative; +} + +.page-break::before { + content: "Page Break"; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: var(--sentient-bg-primary, #ffffff); + padding: 2px 12px; + font-size: 10px; + color: var(--sentient-text-muted, #999); + text-transform: uppercase; + letter-spacing: 1px; +} + +@media print { + .page-break { + display: block; + page-break-after: always; + height: 0; + margin: 0; + background: none; + } + + .page-break::before { + display: none; + } +} + + +/* ============================================================================= + HEADER & FOOTER + ============================================================================= */ + +.editor-header, +.editor-footer { + min-height: 48px; + padding: 12px 24px; + font-size: 11px; + color: var(--sentient-text-secondary, #666); + border: 1px dashed transparent; + transition: border-color 0.15s ease, background 0.15s ease; +} + +.editor-header { + border-bottom: 1px dashed var(--sentient-border, #e0e0e0); + margin-bottom: 24px; +} + +.editor-footer { + border-top: 1px dashed var(--sentient-border, #e0e0e0); + margin-top: 24px; +} + +.editor-header:hover, +.editor-footer:hover { + background: rgba(66, 133, 244, 0.02); + border-color: var(--sentient-border, #e0e0e0); +} + +.editor-header:focus, +.editor-footer:focus { + outline: none; + background: rgba(66, 133, 244, 0.05); + border-color: var(--sentient-accent, #4285f4); +} + +.editor-header:empty::before, +.editor-footer:empty::before { + content: attr(data-placeholder); + color: var(--sentient-text-muted, #999); + font-style: italic; +} + +.editor-header:focus:empty::before, +.editor-footer:focus:empty::before { + color: var(--sentient-text-secondary, #666); +} + +/* Header/Footer Modal */ +.hf-tabs { + display: flex; + border-bottom: 1px solid var(--sentient-border, #e0e0e0); + margin-bottom: 16px; +} + +.hf-tab { + padding: 10px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + cursor: pointer; + transition: all 0.15s ease; +} + +.hf-tab:hover { + color: var(--sentient-text-primary, #212121); +} + +.hf-tab.active { + color: var(--sentient-accent, #4285f4); + border-bottom-color: var(--sentient-accent, #4285f4); +} + +.hf-tab-content { + display: none; +} + +.hf-tab-content.active { + display: block; +} + +.hf-editor { + min-height: 80px; + padding: 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + color: var(--sentient-text-primary, #212121); + background: var(--sentient-bg-primary, #ffffff); +} + +.hf-editor:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.hf-editor:empty::before { + content: attr(data-placeholder); + color: var(--sentient-text-muted, #999); +} + +.hf-options { + display: flex; + flex-direction: column; + gap: 8px; + margin: 16px 0; +} + +.hf-insert-options { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--sentient-border, #e0e0e0); +} + +.hf-insert-options label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 8px; +} + +.hf-insert-btns { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} + +@media print { + .editor-header, + .editor-footer { + border: none; + margin: 0; + padding: 8px 0; + } + + .editor-header:empty, + .editor-footer:empty { + display: none; + } +} + .editor-content ::-moz-selection { background: rgba(66, 133, 244, 0.3); } diff --git a/ui/suite/docs/docs.html b/ui/suite/docs/docs.html index f81c1bf..dd68ee5 100644 --- a/ui/suite/docs/docs.html +++ b/ui/suite/docs/docs.html @@ -12,6 +12,46 @@ />
+
+ + +
+
+
+ + + + + + diff --git a/ui/suite/docs/docs.js b/ui/suite/docs/docs.js index aecfb3c..896a97e 100644 --- a/ui/suite/docs/docs.js +++ b/ui/suite/docs/docs.js @@ -20,6 +20,8 @@ chatPanelOpen: true, driveSource: null, zoom: 100, + findMatches: [], + findMatchIndex: -1, }; const elements = {}; @@ -54,6 +56,11 @@ elements.imageModal = document.getElementById("imageModal"); elements.tableModal = document.getElementById("tableModal"); elements.exportModal = document.getElementById("exportModal"); + elements.findReplaceModal = document.getElementById("findReplaceModal"); + elements.printPreviewModal = document.getElementById("printPreviewModal"); + elements.headerFooterModal = document.getElementById("headerFooterModal"); + elements.editorHeader = document.getElementById("editorHeader"); + elements.editorFooter = document.getElementById("editorFooter"); } function bindEvents() { @@ -207,6 +214,94 @@ btn.addEventListener("click", () => exportDocument(btn.dataset.format)); }); + document + .getElementById("findReplaceBtn") + ?.addEventListener("click", showFindReplaceModal); + document + .getElementById("closeFindReplaceModal") + ?.addEventListener("click", () => hideModal("findReplaceModal")); + document.getElementById("findNextBtn")?.addEventListener("click", findNext); + document.getElementById("findPrevBtn")?.addEventListener("click", findPrev); + document + .getElementById("replaceBtn") + ?.addEventListener("click", replaceOne); + document + .getElementById("replaceAllBtn") + ?.addEventListener("click", replaceAll); + document + .getElementById("findInput") + ?.addEventListener("input", performFind); + + document + .getElementById("printPreviewBtn") + ?.addEventListener("click", showPrintPreview); + document + .getElementById("closePrintPreviewModal") + ?.addEventListener("click", () => hideModal("printPreviewModal")); + document + .getElementById("printBtn") + ?.addEventListener("click", printDocument); + document + .getElementById("cancelPrintBtn") + ?.addEventListener("click", () => hideModal("printPreviewModal")); + document + .getElementById("printOrientation") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printPaperSize") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printHeaders") + ?.addEventListener("change", updatePrintPreview); + + document + .getElementById("pageBreakBtn") + ?.addEventListener("click", insertPageBreak); + + document + .getElementById("headerFooterBtn") + ?.addEventListener("click", showHeaderFooterModal); + document + .getElementById("closeHeaderFooterModal") + ?.addEventListener("click", () => hideModal("headerFooterModal")); + document + .getElementById("applyHeaderFooterBtn") + ?.addEventListener("click", applyHeaderFooter); + document + .getElementById("cancelHeaderFooterBtn") + ?.addEventListener("click", () => hideModal("headerFooterModal")); + document + .getElementById("removeHeaderFooterBtn") + ?.addEventListener("click", removeHeaderFooter); + document.querySelectorAll(".hf-tab").forEach((tab) => { + tab.addEventListener("click", () => switchHfTab(tab.dataset.tab)); + }); + document + .getElementById("insertPageNum") + ?.addEventListener("click", () => insertHfField("header", "pageNum")); + document + .getElementById("insertDate") + ?.addEventListener("click", () => insertHfField("header", "date")); + document + .getElementById("insertDocTitle") + ?.addEventListener("click", () => insertHfField("header", "title")); + document + .getElementById("insertFooterPageNum") + ?.addEventListener("click", () => insertHfField("footer", "pageNum")); + document + .getElementById("insertFooterDate") + ?.addEventListener("click", () => insertHfField("footer", "date")); + document + .getElementById("insertFooterDocTitle") + ?.addEventListener("click", () => insertHfField("footer", "title")); + + if (elements.editorHeader) { + elements.editorHeader.addEventListener("input", handleHeaderFooterInput); + } + if (elements.editorFooter) { + elements.editorFooter.addEventListener("input", handleHeaderFooterInput); + } + window.addEventListener("beforeunload", handleBeforeUnload); } @@ -1007,6 +1102,421 @@ ${content} return div.innerHTML; } + function showFindReplaceModal() { + showModal("findReplaceModal"); + document.getElementById("findInput")?.focus(); + state.findMatches = []; + state.findMatchIndex = -1; + clearFindHighlights(); + } + + function performFind() { + const searchText = document.getElementById("findInput")?.value || ""; + const matchCase = document.getElementById("findMatchCase")?.checked; + const wholeWord = document.getElementById("findWholeWord")?.checked; + + clearFindHighlights(); + state.findMatches = []; + state.findMatchIndex = -1; + + if (!searchText || !elements.editorContent) { + updateFindResults(); + return; + } + + const content = elements.editorContent.innerHTML; + let flags = "g"; + if (!matchCase) flags += "i"; + + let searchPattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + if (wholeWord) { + searchPattern = `\\b${searchPattern}\\b`; + } + + const regex = new RegExp(searchPattern, flags); + const textContent = elements.editorContent.textContent; + let match; + + while ((match = regex.exec(textContent)) !== null) { + state.findMatches.push({ + index: match.index, + length: match[0].length, + text: match[0], + }); + } + + if (state.findMatches.length > 0) { + state.findMatchIndex = 0; + highlightAllMatches(searchText, matchCase, wholeWord); + scrollToMatch(); + } + + updateFindResults(); + } + + function highlightAllMatches(searchText, matchCase, wholeWord) { + if (!elements.editorContent) return; + + const walker = document.createTreeWalker( + elements.editorContent, + NodeFilter.SHOW_TEXT, + null, + false, + ); + + const textNodes = []; + let node; + while ((node = walker.nextNode())) { + textNodes.push(node); + } + + let flags = "g"; + if (!matchCase) flags += "i"; + let searchPattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + if (wholeWord) { + searchPattern = `\\b${searchPattern}\\b`; + } + const regex = new RegExp(`(${searchPattern})`, flags); + + textNodes.forEach((textNode) => { + const text = textNode.textContent; + if (regex.test(text)) { + const span = document.createElement("span"); + span.innerHTML = text.replace( + regex, + '$1', + ); + textNode.parentNode.replaceChild(span, textNode); + } + }); + + updateCurrentHighlight(); + } + + function updateCurrentHighlight() { + const highlights = + elements.editorContent?.querySelectorAll(".find-highlight"); + if (!highlights) return; + + highlights.forEach((el, index) => { + el.classList.toggle("current", index === state.findMatchIndex); + }); + } + + function clearFindHighlights() { + if (!elements.editorContent) return; + + const highlights = + elements.editorContent.querySelectorAll(".find-highlight"); + highlights.forEach((el) => { + const parent = el.parentNode; + parent.replaceChild(document.createTextNode(el.textContent), el); + parent.normalize(); + }); + + const wrapperSpans = elements.editorContent.querySelectorAll("span:empty"); + wrapperSpans.forEach((span) => { + if (span.childNodes.length === 0) { + span.remove(); + } + }); + } + + function updateFindResults() { + const resultsEl = document.getElementById("findResults"); + if (resultsEl) { + const count = state.findMatches.length; + const span = resultsEl.querySelector("span"); + if (span) { + span.textContent = + count === 0 + ? "0 matches found" + : `${state.findMatchIndex + 1} of ${count} matches`; + } + } + } + + function scrollToMatch() { + const highlights = + elements.editorContent?.querySelectorAll(".find-highlight"); + if (highlights && highlights[state.findMatchIndex]) { + highlights[state.findMatchIndex].scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + } + + function findNext() { + if (state.findMatches.length === 0) return; + state.findMatchIndex = + (state.findMatchIndex + 1) % state.findMatches.length; + updateCurrentHighlight(); + scrollToMatch(); + updateFindResults(); + } + + function findPrev() { + if (state.findMatches.length === 0) return; + state.findMatchIndex = + (state.findMatchIndex - 1 + state.findMatches.length) % + state.findMatches.length; + updateCurrentHighlight(); + scrollToMatch(); + updateFindResults(); + } + + function replaceOne() { + if (state.findMatches.length === 0 || state.findMatchIndex < 0) return; + + const replaceText = document.getElementById("replaceInput")?.value || ""; + const highlights = + elements.editorContent?.querySelectorAll(".find-highlight"); + + if (highlights && highlights[state.findMatchIndex]) { + const highlight = highlights[state.findMatchIndex]; + highlight.replaceWith(document.createTextNode(replaceText)); + elements.editorContent.normalize(); + + state.findMatches.splice(state.findMatchIndex, 1); + if (state.findMatches.length > 0) { + state.findMatchIndex = state.findMatchIndex % state.findMatches.length; + updateCurrentHighlight(); + scrollToMatch(); + } else { + state.findMatchIndex = -1; + } + updateFindResults(); + + state.isDirty = true; + scheduleAutoSave(); + } + } + + function replaceAll() { + if (state.findMatches.length === 0) return; + + const replaceText = document.getElementById("replaceInput")?.value || ""; + const highlights = + elements.editorContent?.querySelectorAll(".find-highlight"); + + if (highlights) { + const count = highlights.length; + highlights.forEach((highlight) => { + highlight.replaceWith(document.createTextNode(replaceText)); + }); + elements.editorContent.normalize(); + + state.findMatches = []; + state.findMatchIndex = -1; + updateFindResults(); + + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", `Replaced ${count} occurrences.`); + } + } + + function showPrintPreview() { + showModal("printPreviewModal"); + updatePrintPreview(); + } + + function updatePrintPreview() { + const orientation = + document.getElementById("printOrientation")?.value || "portrait"; + const showHeaders = document.getElementById("printHeaders")?.checked; + const printPage = document.getElementById("printPage"); + const printContent = document.getElementById("printContent"); + const printHeader = document.getElementById("printHeader"); + const printFooter = document.getElementById("printFooter"); + + if (printPage) { + printPage.className = `print-page ${orientation}`; + } + + if (printHeader) { + printHeader.innerHTML = showHeaders ? state.docTitle : ""; + printHeader.style.display = showHeaders ? "block" : "none"; + } + + if (printFooter) { + printFooter.innerHTML = showHeaders ? "Page 1" : ""; + printFooter.style.display = showHeaders ? "block" : "none"; + } + + if (printContent && elements.editorContent) { + printContent.innerHTML = elements.editorContent.innerHTML; + } + } + + function printDocument() { + const orientation = + document.getElementById("printOrientation")?.value || "portrait"; + const showHeaders = document.getElementById("printHeaders")?.checked; + const content = elements.editorContent?.innerHTML || ""; + + const printWindow = window.open("", "_blank"); + + printWindow.document.write(` + + + + ${state.docTitle} + + + + ${showHeaders ? `
${state.docTitle}
` : ""} + ${content} + + + `); + + printWindow.document.close(); + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 250); + + hideModal("printPreviewModal"); + } + + function insertPageBreak() { + if (!elements.editorContent) return; + + const pageBreak = document.createElement("div"); + pageBreak.className = "page-break"; + pageBreak.contentEditable = "false"; + + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(pageBreak); + + const newParagraph = document.createElement("p"); + newParagraph.innerHTML = "
"; + pageBreak.after(newParagraph); + + range.setStartAfter(newParagraph); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } else { + elements.editorContent.appendChild(pageBreak); + } + + state.isDirty = true; + scheduleAutoSave(); + } + + function showHeaderFooterModal() { + showModal("headerFooterModal"); + + const headerEditor = document.getElementById("headerEditor"); + const footerEditor = document.getElementById("footerEditor"); + + if (headerEditor && elements.editorHeader) { + headerEditor.innerHTML = elements.editorHeader.innerHTML; + } + if (footerEditor && elements.editorFooter) { + footerEditor.innerHTML = elements.editorFooter.innerHTML; + } + } + + function switchHfTab(tabName) { + document.querySelectorAll(".hf-tab").forEach((tab) => { + tab.classList.toggle("active", tab.dataset.tab === tabName); + }); + document + .getElementById("hfHeaderTab") + ?.classList.toggle("active", tabName === "header"); + document + .getElementById("hfFooterTab") + ?.classList.toggle("active", tabName === "footer"); + } + + function insertHfField(type, field) { + const editorId = type === "header" ? "headerEditor" : "footerEditor"; + const editor = document.getElementById(editorId); + if (!editor) return; + + let fieldContent = ""; + switch (field) { + case "pageNum": + fieldContent = + '[Page #]'; + break; + case "date": + fieldContent = `${new Date().toLocaleDateString()}`; + break; + case "title": + fieldContent = `${state.docTitle}`; + break; + } + + editor.focus(); + document.execCommand("insertHTML", false, fieldContent); + } + + function applyHeaderFooter() { + const headerEditor = document.getElementById("headerEditor"); + const footerEditor = document.getElementById("footerEditor"); + + if (elements.editorHeader && headerEditor) { + elements.editorHeader.innerHTML = headerEditor.innerHTML; + } + if (elements.editorFooter && footerEditor) { + elements.editorFooter.innerHTML = footerEditor.innerHTML; + } + + hideModal("headerFooterModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Header and footer updated!"); + } + + function removeHeaderFooter() { + if (elements.editorHeader) { + elements.editorHeader.innerHTML = ""; + } + if (elements.editorFooter) { + elements.editorFooter.innerHTML = ""; + } + + const headerEditor = document.getElementById("headerEditor"); + const footerEditor = document.getElementById("footerEditor"); + if (headerEditor) headerEditor.innerHTML = ""; + if (footerEditor) footerEditor.innerHTML = ""; + + hideModal("headerFooterModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Header and footer removed."); + } + + function handleHeaderFooterInput() { + state.isDirty = true; + scheduleAutoSave(); + } + function createNewDocument() { state.docId = null; state.docTitle = "Untitled Document"; diff --git a/ui/suite/sheet/sheet.css b/ui/suite/sheet/sheet.css index 170eddb..45d7611 100644 --- a/ui/suite/sheet/sheet.css +++ b/ui/suite/sheet/sheet.css @@ -1329,3 +1329,749 @@ border-color: #ccc !important; } } + +/* ============================================================================= + CHARTS & IMAGES DISPLAY + ============================================================================= */ + +.charts-container, +.images-container { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + z-index: 10; +} + +.chart-wrapper { + position: absolute; + background: var(--sentient-bg-primary, #ffffff); + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-md, 8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + pointer-events: auto; + overflow: hidden; +} + +.chart-wrapper:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.chart-wrapper.selected { + border-color: var(--sentient-accent, #4285f4); + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); +} + +.chart-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-bottom: 1px solid var(--sentient-border, #e0e0e0); + cursor: move; +} + +.chart-title { + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-primary, #212121); + margin: 0; +} + +.chart-actions { + display: flex; + gap: 4px; +} + +.chart-actions button { + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--sentient-text-secondary, #666); + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.chart-actions button:hover { + background: var(--sentient-bg-tertiary, #e0e0e0); +} + +.chart-content { + padding: 16px; + min-height: 200px; +} + +.chart-canvas { + width: 100%; + height: 100%; +} + +.chart-bar-container { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 100%; + padding: 8px; + gap: 8px; +} + +.chart-bar { + flex: 1; + background: var(--sentient-accent, #4285f4); + border-radius: 4px 4px 0 0; + min-width: 20px; + max-width: 60px; + transition: height 0.3s ease; +} + +.chart-bar:nth-child(2) { + background: #34a853; +} + +.chart-bar:nth-child(3) { + background: #fbbc04; +} + +.chart-bar:nth-child(4) { + background: #ea4335; +} + +.chart-bar:nth-child(5) { + background: #9c27b0; +} + +.chart-line-container { + position: relative; + height: 100%; + padding: 8px; +} + +.chart-line { + fill: none; + stroke: var(--sentient-accent, #4285f4); + stroke-width: 2; +} + +.chart-line-point { + fill: var(--sentient-accent, #4285f4); +} + +.chart-pie-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px 16px; + border-top: 1px solid var(--sentient-border, #e0e0e0); + font-size: 11px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 4px; +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; +} + +.image-wrapper { + position: absolute; + border: 1px solid transparent; + border-radius: var(--sentient-radius-sm, 4px); + pointer-events: auto; + cursor: move; + overflow: hidden; +} + +.image-wrapper:hover { + border-color: var(--sentient-border, #e0e0e0); +} + +.image-wrapper.selected { + border-color: var(--sentient-accent, #4285f4); + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); +} + +.image-wrapper img { + width: 100%; + height: 100%; + object-fit: contain; + pointer-events: none; +} + +.image-resize-handle { + position: absolute; + width: 10px; + height: 10px; + background: var(--sentient-accent, #4285f4); + border: 2px solid white; + border-radius: 50%; + cursor: se-resize; + bottom: -5px; + right: -5px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.image-wrapper:hover .image-resize-handle, +.image-wrapper.selected .image-resize-handle { + opacity: 1; +} + +/* ============================================================================= + FIND & REPLACE MODAL + ============================================================================= */ + +.find-replace-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.find-replace-group label { + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); +} + +.find-replace-group input { + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.find-replace-group input:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.find-replace-options { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--sentient-text-primary, #212121); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--sentient-accent, #4285f4); +} + +.find-results { + padding: 10px 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + color: var(--sentient-text-secondary, #666); + margin-bottom: 16px; +} + +/* ============================================================================= + CONDITIONAL FORMATTING MODAL + ============================================================================= */ + +.cf-section { + margin-bottom: 16px; +} + +.cf-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 6px; +} + +.cf-section input, +.cf-section select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.cf-section input:focus, +.cf-section select:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.cf-values { + display: flex; + gap: 12px; +} + +.cf-values input { + flex: 1; +} + +.cf-style-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.cf-style-row { + display: flex; + align-items: center; + gap: 8px; +} + +.cf-style-row label { + margin-bottom: 0; + min-width: 80px; +} + +.cf-style-row input[type="color"] { + width: 40px; + height: 32px; + padding: 2px; + cursor: pointer; +} + +.cf-style-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--sentient-accent, #4285f4); +} + +.cf-preview { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--sentient-border, #e0e0e0); +} + +.cf-preview label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 8px; +} + +.cf-preview-cell { + width: 120px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: #ffeb3b; +} + +/* ============================================================================= + DATA VALIDATION MODAL + ============================================================================= */ + +.dv-tabs { + display: flex; + border-bottom: 1px solid var(--sentient-border, #e0e0e0); + margin-bottom: 16px; +} + +.dv-tab { + padding: 10px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + cursor: pointer; + transition: all 0.15s ease; +} + +.dv-tab:hover { + color: var(--sentient-text-primary, #212121); +} + +.dv-tab.active { + color: var(--sentient-accent, #4285f4); + border-bottom-color: var(--sentient-accent, #4285f4); +} + +.dv-tab-content { + display: none; +} + +.dv-tab-content.active { + display: block; +} + +.dv-section { + margin-bottom: 16px; +} + +.dv-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 6px; +} + +.dv-section input, +.dv-section select, +.dv-section textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); + font-family: inherit; +} + +.dv-section input:focus, +.dv-section select:focus, +.dv-section textarea:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.dv-section textarea { + min-height: 80px; + resize: vertical; +} + +.dv-value-row { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 12px; +} + +.dv-list { + margin-top: 12px; +} + +/* ============================================================================= + PRINT PREVIEW MODAL + ============================================================================= */ + +.modal-fullscreen { + width: 95vw; + max-width: 1400px; + height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-fullscreen .modal-header { + flex-shrink: 0; +} + +.modal-fullscreen .modal-body { + flex: 1; + overflow: hidden; + padding: 0; +} + +.print-toolbar { + display: flex; + align-items: center; + gap: 16px; + flex: 1; + margin: 0 24px; +} + +.print-toolbar select { + padding: 6px 10px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.print-toolbar .checkbox-label { + font-size: 12px; +} + +.print-preview-body { + display: flex; + justify-content: center; + align-items: flex-start; + background: var(--sentient-bg-tertiary, #e0e0e0); + overflow: auto; + padding: 24px; +} + +.print-preview-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +.print-page { + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 48px; +} + +.print-page.portrait { + width: 8.5in; + min-height: 11in; +} + +.print-page.landscape { + width: 11in; + min-height: 8.5in; +} + +.print-content { + width: 100%; + overflow: hidden; +} + +.print-content table { + width: 100%; + border-collapse: collapse; + font-size: 10pt; +} + +.print-content td, +.print-content th { + border: 1px solid #ccc; + padding: 4px 8px; + text-align: left; +} + +.print-content th { + background: #f5f5f5; + font-weight: 600; +} + +/* ============================================================================= + CUSTOM NUMBER FORMAT MODAL + ============================================================================= */ + +.cnf-section { + margin-bottom: 16px; +} + +.cnf-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 6px; +} + +.cnf-section input, +.cnf-section select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.cnf-section input:focus, +.cnf-section select:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.cnf-preview { + padding: 12px 16px; + background: var(--sentient-bg-secondary, #f5f5f5); + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 16px; + font-family: monospace; + text-align: right; +} + +.cnf-formats-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + max-height: 180px; + overflow-y: auto; +} + +.cnf-format-item { + padding: 10px 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + text-align: center; + cursor: pointer; + transition: all 0.15s ease; +} + +.cnf-format-item:hover { + border-color: var(--sentient-accent, #4285f4); + background: var(--sentient-bg-primary, #ffffff); +} + +.cnf-format-item.selected { + border-color: var(--sentient-accent, #4285f4); + background: rgba(66, 133, 244, 0.1); +} + +/* ============================================================================= + INSERT IMAGE MODAL + ============================================================================= */ + +.img-tabs { + display: flex; + border-bottom: 1px solid var(--sentient-border, #e0e0e0); + margin-bottom: 16px; +} + +.img-tab { + padding: 10px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + cursor: pointer; + transition: all 0.15s ease; +} + +.img-tab:hover { + color: var(--sentient-text-primary, #212121); +} + +.img-tab.active { + color: var(--sentient-accent, #4285f4); + border-bottom-color: var(--sentient-accent, #4285f4); +} + +.img-tab-content { + display: none; +} + +.img-tab-content.active { + display: block; +} + +.img-section { + margin-bottom: 16px; +} + +.img-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 6px; +} + +.img-section input { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.img-section input:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.img-drop-zone { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 40px; + border: 2px dashed var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-md, 8px); + color: var(--sentient-text-secondary, #666); + transition: all 0.15s ease; +} + +.img-drop-zone:hover, +.img-drop-zone.dragover { + border-color: var(--sentient-accent, #4285f4); + background: rgba(66, 133, 244, 0.05); +} + +.img-drop-zone p { + margin: 0; + font-size: 14px; +} + +.img-preview-container { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--sentient-border, #e0e0e0); +} + +.img-preview-container label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 8px; +} + +.img-preview-container img { + max-width: 100%; + max-height: 200px; + border-radius: var(--sentient-radius-sm, 4px); + border: 1px solid var(--sentient-border, #e0e0e0); +} + +/* ============================================================================= + NUMBER FORMAT SELECT + ============================================================================= */ + +.number-format { + min-width: 140px; +} + +/* ============================================================================= + UTILITY CLASSES + ============================================================================= */ + +.hidden { + display: none !important; +} diff --git a/ui/suite/sheet/sheet.html b/ui/suite/sheet/sheet.html index 91f85b0..7a44c83 100644 --- a/ui/suite/sheet/sheet.html +++ b/ui/suite/sheet/sheet.html @@ -45,6 +45,46 @@ +
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ +
+ + + + +
@@ -297,6 +531,14 @@ class="cursor-indicators" id="cursorIndicators" > +
+
@@ -598,7 +840,7 @@ - @@ -606,4 +848,464 @@ + + + + + + + + + + + + diff --git a/ui/suite/sheet/sheet.js b/ui/suite/sheet/sheet.js index 4e35796..4baa9ee 100644 --- a/ui/suite/sheet/sheet.js +++ b/ui/suite/sheet/sheet.js @@ -37,6 +37,9 @@ isDirty: false, autoSaveTimer: null, chatPanelOpen: true, + findMatches: [], + findMatchIndex: -1, + decimalPlaces: 2, }; const elements = {}; @@ -50,6 +53,8 @@ connectChatWebSocket(); selectCell(0, 0); updateCellAddress(); + renderCharts(); + renderImages(); } function cacheElements() { @@ -76,6 +81,18 @@ elements.chatMessages = document.getElementById("chatMessages"); elements.chatInput = document.getElementById("chatInput"); elements.chatForm = document.getElementById("chatForm"); + elements.findReplaceModal = document.getElementById("findReplaceModal"); + elements.conditionalFormatModal = document.getElementById( + "conditionalFormatModal", + ); + elements.dataValidationModal = document.getElementById( + "dataValidationModal", + ); + elements.printPreviewModal = document.getElementById("printPreviewModal"); + elements.customNumberFormatModal = document.getElementById( + "customNumberFormatModal", + ); + elements.insertImageModal = document.getElementById("insertImageModal"); } function renderGrid() { @@ -233,11 +250,14 @@ .getElementById("mergeCellsBtn") ?.addEventListener("click", mergeCells); document - .getElementById("formatCurrencyBtn") - ?.addEventListener("click", () => formatCells("currency")); + .getElementById("numberFormat") + ?.addEventListener("change", handleNumberFormatChange); document - .getElementById("formatPercentBtn") - ?.addEventListener("click", () => formatCells("percent")); + .getElementById("decreaseDecimalBtn") + ?.addEventListener("click", decreaseDecimal); + document + .getElementById("increaseDecimalBtn") + ?.addEventListener("click", increaseDecimal); document .getElementById("textColorInput") @@ -282,6 +302,157 @@ document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); + document + .getElementById("findReplaceBtn") + ?.addEventListener("click", showFindReplaceModal); + document + .getElementById("closeFindReplaceModal") + ?.addEventListener("click", () => hideModal("findReplaceModal")); + document.getElementById("findNextBtn")?.addEventListener("click", findNext); + document.getElementById("findPrevBtn")?.addEventListener("click", findPrev); + document + .getElementById("replaceBtn") + ?.addEventListener("click", replaceOne); + document + .getElementById("replaceAllBtn") + ?.addEventListener("click", replaceAll); + document + .getElementById("findInput") + ?.addEventListener("input", performFind); + + document + .getElementById("conditionalFormatBtn") + ?.addEventListener("click", showConditionalFormatModal); + document + .getElementById("closeConditionalFormatModal") + ?.addEventListener("click", () => hideModal("conditionalFormatModal")); + document + .getElementById("applyCfBtn") + ?.addEventListener("click", applyConditionalFormat); + document + .getElementById("cancelCfBtn") + ?.addEventListener("click", () => hideModal("conditionalFormatModal")); + document + .getElementById("cfRuleType") + ?.addEventListener("change", handleCfRuleTypeChange); + document + .getElementById("cfBgColor") + ?.addEventListener("input", updateCfPreview); + document + .getElementById("cfTextColor") + ?.addEventListener("input", updateCfPreview); + document + .getElementById("cfBold") + ?.addEventListener("change", updateCfPreview); + document + .getElementById("cfItalic") + ?.addEventListener("change", updateCfPreview); + + document + .getElementById("dataValidationBtn") + ?.addEventListener("click", showDataValidationModal); + document + .getElementById("closeDataValidationModal") + ?.addEventListener("click", () => hideModal("dataValidationModal")); + document + .getElementById("applyDvBtn") + ?.addEventListener("click", applyDataValidation); + document + .getElementById("cancelDvBtn") + ?.addEventListener("click", () => hideModal("dataValidationModal")); + document + .getElementById("clearDvBtn") + ?.addEventListener("click", clearDataValidation); + document + .getElementById("dvType") + ?.addEventListener("change", handleDvTypeChange); + document + .getElementById("dvOperator") + ?.addEventListener("change", handleDvOperatorChange); + document.querySelectorAll(".dv-tab").forEach((tab) => { + tab.addEventListener("click", () => switchDvTab(tab.dataset.tab)); + }); + + document + .getElementById("printPreviewBtn") + ?.addEventListener("click", showPrintPreview); + document + .getElementById("closePrintPreviewModal") + ?.addEventListener("click", () => hideModal("printPreviewModal")); + document.getElementById("printBtn")?.addEventListener("click", printSheet); + document + .getElementById("cancelPrintBtn") + ?.addEventListener("click", () => hideModal("printPreviewModal")); + document + .getElementById("printOrientation") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printPaperSize") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printScale") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printGridlines") + ?.addEventListener("change", updatePrintPreview); + document + .getElementById("printHeaders") + ?.addEventListener("change", updatePrintPreview); + + document + .getElementById("insertChartBtn") + ?.addEventListener("click", () => showModal("chartModal")); + document + .getElementById("insertChartBtnConfirm") + ?.addEventListener("click", insertChart); + document + .getElementById("cancelChartBtn") + ?.addEventListener("click", () => hideModal("chartModal")); + + document + .getElementById("insertImageBtn") + ?.addEventListener("click", showInsertImageModal); + document + .getElementById("closeInsertImageModal") + ?.addEventListener("click", () => hideModal("insertImageModal")); + document + .getElementById("insertImgBtn") + ?.addEventListener("click", insertImage); + document + .getElementById("cancelImgBtn") + ?.addEventListener("click", () => hideModal("insertImageModal")); + document.querySelectorAll(".img-tab").forEach((tab) => { + tab.addEventListener("click", () => switchImgTab(tab.dataset.tab)); + }); + + document + .getElementById("filterBtn") + ?.addEventListener("click", toggleFilter); + document + .getElementById("sortAscBtn") + ?.addEventListener("click", sortAscending); + document + .getElementById("sortDescBtn") + ?.addEventListener("click", sortDescending); + + document + .getElementById("closeCustomFormatModal") + ?.addEventListener("click", () => hideModal("customNumberFormatModal")); + document + .getElementById("applyCnfBtn") + ?.addEventListener("click", applyCustomNumberFormat); + document + .getElementById("cancelCnfBtn") + ?.addEventListener("click", () => hideModal("customNumberFormatModal")); + document.querySelectorAll(".cnf-format-item").forEach((item) => { + item.addEventListener("click", () => + selectCustomFormat(item.dataset.format), + ); + }); + document + .getElementById("cnfFormatCode") + ?.addEventListener("input", updateCnfPreview); + document .getElementById("chatToggle") ?.addEventListener("click", toggleChatPanel); @@ -1053,7 +1224,41 @@ } function mergeCells() { - addChatMessage("assistant", "Merge cells feature coming soon!"); + const { start, end } = state.selection; + if (start.row === end.row && start.col === end.col) { + addChatMessage("assistant", "Select multiple cells to merge."); + return; + } + + saveToHistory(); + const ws = state.worksheets[state.activeWorksheet]; + + const firstKey = `${start.row},${start.col}`; + let mergedValue = ""; + for (let r = start.row; r <= end.row; r++) { + for (let c = start.col; c <= end.col; c++) { + const key = `${r},${c}`; + const cellData = ws.data[key]; + if (cellData?.value && !mergedValue) { + mergedValue = cellData.value; + } + if (r !== start.row || c !== start.col) { + delete ws.data[key]; + } + } + } + + if (!ws.data[firstKey]) ws.data[firstKey] = {}; + ws.data[firstKey].value = mergedValue; + ws.data[firstKey].merged = { + rowSpan: end.row - start.row + 1, + colSpan: end.col - start.col + 1, + }; + + renderAllCells(); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Cells merged successfully!"); } function saveToHistory() { @@ -1698,6 +1903,1330 @@ // Chat uses main WebSocket connection } + function handleNumberFormatChange(e) { + const format = e.target.value; + if (format === "custom") { + showModal("customNumberFormatModal"); + return; + } + applyNumberFormat(format); + } + + function applyNumberFormat(format) { + saveToHistory(); + const { start, end } = state.selection; + const ws = state.worksheets[state.activeWorksheet]; + + for (let r = start.row; r <= end.row; r++) { + for (let c = start.col; c <= end.col; c++) { + const key = `${r},${c}`; + if (!ws.data[key]) ws.data[key] = { value: "" }; + ws.data[key].format = format; + + const rawValue = ws.data[key].rawValue || ws.data[key].value; + if (rawValue) { + ws.data[key].rawValue = rawValue; + ws.data[key].value = formatValue(rawValue, format); + } + renderCell(r, c); + } + } + + state.isDirty = true; + scheduleAutoSave(); + } + + function formatValue(value, format) { + const num = parseFloat(value); + if (isNaN(num) && format !== "text") return value; + + switch (format) { + case "number": + return num.toLocaleString("en-US", { + minimumFractionDigits: state.decimalPlaces, + maximumFractionDigits: state.decimalPlaces, + }); + case "currency": + return num.toLocaleString("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: state.decimalPlaces, + }); + case "accounting": + const formatted = Math.abs(num).toLocaleString("en-US", { + style: "currency", + currency: "USD", + }); + return num < 0 ? `(${formatted})` : formatted; + case "percent": + return (num * 100).toFixed(state.decimalPlaces) + "%"; + case "scientific": + return num.toExponential(state.decimalPlaces); + case "date_short": + const d1 = new Date(num); + return isNaN(d1.getTime()) ? value : d1.toLocaleDateString("en-US"); + case "date_long": + const d2 = new Date(num); + return isNaN(d2.getTime()) + ? value + : d2.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + case "time": + const d3 = new Date(num); + return isNaN(d3.getTime()) + ? value + : d3.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }); + case "datetime": + const d4 = new Date(num); + return isNaN(d4.getTime()) ? value : d4.toLocaleString("en-US"); + case "fraction": + return toFraction(num); + case "text": + return String(value); + default: + return value; + } + } + + function toFraction(decimal) { + const tolerance = 1e-6; + let h1 = 1, + h2 = 0, + k1 = 0, + k2 = 1; + let b = decimal; + do { + const a = Math.floor(b); + let aux = h1; + h1 = a * h1 + h2; + h2 = aux; + aux = k1; + k1 = a * k1 + k2; + k2 = aux; + b = 1 / (b - a); + } while (Math.abs(decimal - h1 / k1) > decimal * tolerance); + + if (k1 === 1) return String(h1); + const whole = Math.floor(h1 / k1); + const remainder = h1 % k1; + if (whole === 0) return `${remainder}/${k1}`; + return `${whole} ${remainder}/${k1}`; + } + + function decreaseDecimal() { + if (state.decimalPlaces > 0) { + state.decimalPlaces--; + reapplyFormats(); + } + } + + function increaseDecimal() { + if (state.decimalPlaces < 10) { + state.decimalPlaces++; + reapplyFormats(); + } + } + + function reapplyFormats() { + const { start, end } = state.selection; + const ws = state.worksheets[state.activeWorksheet]; + + for (let r = start.row; r <= end.row; r++) { + for (let c = start.col; c <= end.col; c++) { + const key = `${r},${c}`; + const cellData = ws.data[key]; + if (cellData?.format && cellData?.rawValue) { + cellData.value = formatValue(cellData.rawValue, cellData.format); + renderCell(r, c); + } + } + } + } + + function showFindReplaceModal() { + showModal("findReplaceModal"); + document.getElementById("findInput")?.focus(); + state.findMatches = []; + state.findMatchIndex = -1; + } + + function performFind() { + const searchText = document.getElementById("findInput")?.value || ""; + const matchCase = document.getElementById("findMatchCase")?.checked; + const wholeCell = document.getElementById("findWholeCell")?.checked; + const useRegex = document.getElementById("findRegex")?.checked; + + state.findMatches = []; + state.findMatchIndex = -1; + + if (!searchText) { + updateFindResults(); + return; + } + + const ws = state.worksheets[state.activeWorksheet]; + let pattern; + + if (useRegex) { + try { + pattern = new RegExp(searchText, matchCase ? "" : "i"); + } catch (e) { + updateFindResults(); + return; + } + } + + for (let r = 0; r < CONFIG.ROWS; r++) { + for (let c = 0; c < CONFIG.COLS; c++) { + const key = `${r},${c}`; + const cellData = ws.data[key]; + const cellValue = cellData?.value || ""; + + if (!cellValue) continue; + + let matches = false; + const compareValue = matchCase ? cellValue : cellValue.toLowerCase(); + const compareSearch = matchCase ? searchText : searchText.toLowerCase(); + + if (useRegex) { + matches = pattern.test(cellValue); + } else if (wholeCell) { + matches = compareValue === compareSearch; + } else { + matches = compareValue.includes(compareSearch); + } + + if (matches) { + state.findMatches.push({ row: r, col: c }); + } + } + } + + updateFindResults(); + if (state.findMatches.length > 0) { + state.findMatchIndex = 0; + highlightFindMatch(); + } + } + + function updateFindResults() { + const resultsEl = document.getElementById("findResults"); + if (resultsEl) { + const count = state.findMatches.length; + resultsEl.querySelector("span").textContent = + count === 0 + ? "0 matches found" + : `${state.findMatchIndex + 1} of ${count} matches`; + } + } + + function highlightFindMatch() { + if (state.findMatches.length === 0) return; + const match = state.findMatches[state.findMatchIndex]; + selectCell(match.row, match.col); + updateFindResults(); + } + + function findNext() { + if (state.findMatches.length === 0) return; + state.findMatchIndex = + (state.findMatchIndex + 1) % state.findMatches.length; + highlightFindMatch(); + } + + function findPrev() { + if (state.findMatches.length === 0) return; + state.findMatchIndex = + (state.findMatchIndex - 1 + state.findMatches.length) % + state.findMatches.length; + highlightFindMatch(); + } + + function replaceOne() { + if (state.findMatches.length === 0 || state.findMatchIndex < 0) return; + + const replaceText = document.getElementById("replaceInput")?.value || ""; + const match = state.findMatches[state.findMatchIndex]; + const ws = state.worksheets[state.activeWorksheet]; + const key = `${match.row},${match.col}`; + + saveToHistory(); + + const searchText = document.getElementById("findInput")?.value || ""; + const matchCase = document.getElementById("findMatchCase")?.checked; + const useRegex = document.getElementById("findRegex")?.checked; + const cellValue = ws.data[key]?.value || ""; + + let newValue; + if (useRegex) { + const pattern = new RegExp(searchText, matchCase ? "g" : "gi"); + newValue = cellValue.replace(pattern, replaceText); + } else { + const flags = matchCase ? "g" : "gi"; + const escapedSearch = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + newValue = cellValue.replace( + new RegExp(escapedSearch, flags), + replaceText, + ); + } + + if (!ws.data[key]) ws.data[key] = {}; + ws.data[key].value = newValue; + renderCell(match.row, match.col); + + state.findMatches.splice(state.findMatchIndex, 1); + if (state.findMatches.length > 0) { + state.findMatchIndex = state.findMatchIndex % state.findMatches.length; + highlightFindMatch(); + } else { + state.findMatchIndex = -1; + updateFindResults(); + } + + state.isDirty = true; + scheduleAutoSave(); + } + + function replaceAll() { + if (state.findMatches.length === 0) return; + + const replaceText = document.getElementById("replaceInput")?.value || ""; + const searchText = document.getElementById("findInput")?.value || ""; + const matchCase = document.getElementById("findMatchCase")?.checked; + const useRegex = document.getElementById("findRegex")?.checked; + const ws = state.worksheets[state.activeWorksheet]; + + saveToHistory(); + + let count = 0; + for (const match of state.findMatches) { + const key = `${match.row},${match.col}`; + const cellValue = ws.data[key]?.value || ""; + + let newValue; + if (useRegex) { + const pattern = new RegExp(searchText, matchCase ? "g" : "gi"); + newValue = cellValue.replace(pattern, replaceText); + } else { + const flags = matchCase ? "g" : "gi"; + const escapedSearch = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + newValue = cellValue.replace( + new RegExp(escapedSearch, flags), + replaceText, + ); + } + + if (!ws.data[key]) ws.data[key] = {}; + ws.data[key].value = newValue; + renderCell(match.row, match.col); + count++; + } + + state.findMatches = []; + state.findMatchIndex = -1; + updateFindResults(); + + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", `Replaced ${count} occurrences.`); + } + + function showConditionalFormatModal() { + const { start, end } = state.selection; + const range = `${getColName(start.col)}${start.row + 1}:${getColName(end.col)}${end.row + 1}`; + const rangeInput = document.getElementById("cfRange"); + if (rangeInput) rangeInput.value = range; + showModal("conditionalFormatModal"); + handleCfRuleTypeChange(); + updateCfPreview(); + } + + function handleCfRuleTypeChange() { + const ruleType = document.getElementById("cfRuleType")?.value; + const value2 = document.getElementById("cfValue2"); + const valuesSection = document.getElementById("cfValuesSection"); + + if (value2) { + if (ruleType === "between") { + value2.classList.remove("hidden"); + value2.placeholder = "and"; + } else { + value2.classList.add("hidden"); + } + } + + const noValueTypes = [ + "duplicate", + "unique", + "blank", + "not_blank", + "above_average", + "below_average", + "color_scale", + "data_bar", + "icon_set", + ]; + if (valuesSection) { + if (noValueTypes.includes(ruleType)) { + valuesSection.style.display = "none"; + } else { + valuesSection.style.display = "flex"; + } + } + } + + function updateCfPreview() { + const bgColor = document.getElementById("cfBgColor")?.value || "#ffeb3b"; + const textColor = + document.getElementById("cfTextColor")?.value || "#000000"; + const bold = document.getElementById("cfBold")?.checked; + const italic = document.getElementById("cfItalic")?.checked; + + const previewCell = document.getElementById("cfPreviewCell"); + if (previewCell) { + previewCell.style.background = bgColor; + previewCell.style.color = textColor; + previewCell.style.fontWeight = bold ? "bold" : "normal"; + previewCell.style.fontStyle = italic ? "italic" : "normal"; + } + } + + function applyConditionalFormat() { + const rangeStr = document.getElementById("cfRange")?.value; + if (!rangeStr) { + alert("Please specify a range."); + return; + } + + const ruleType = document.getElementById("cfRuleType")?.value; + const value1 = document.getElementById("cfValue1")?.value; + const value2 = document.getElementById("cfValue2")?.value; + const bgColor = document.getElementById("cfBgColor")?.value; + const textColor = document.getElementById("cfTextColor")?.value; + const bold = document.getElementById("cfBold")?.checked; + const italic = document.getElementById("cfItalic")?.checked; + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.conditionalFormats) ws.conditionalFormats = []; + + const rule = { + id: `cf_${Date.now()}`, + range: rangeStr, + ruleType, + value1, + value2, + style: { + background: bgColor, + color: textColor, + fontWeight: bold ? "bold" : "normal", + fontStyle: italic ? "italic" : "normal", + }, + }; + + ws.conditionalFormats.push(rule); + applyConditionalFormatsToRange(rule); + + hideModal("conditionalFormatModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Conditional formatting applied!"); + } + + function applyConditionalFormatsToRange(rule) { + const ws = state.worksheets[state.activeWorksheet]; + const rangeParts = rule.range.split(":"); + if (rangeParts.length !== 2) return; + + const startRef = parseCellRef(rangeParts[0]); + const endRef = parseCellRef(rangeParts[1]); + if (!startRef || !endRef) return; + + for (let r = startRef.row; r <= endRef.row; r++) { + for (let c = startRef.col; c <= endRef.col; c++) { + const key = `${r},${c}`; + const cellData = ws.data[key]; + const cellValue = parseFloat(cellData?.value) || 0; + + let conditionMet = false; + switch (rule.ruleType) { + case "greater_than": + conditionMet = cellValue > parseFloat(rule.value1); + break; + case "less_than": + conditionMet = cellValue < parseFloat(rule.value1); + break; + case "equal_to": + conditionMet = cellValue === parseFloat(rule.value1); + break; + case "between": + conditionMet = + cellValue >= parseFloat(rule.value1) && + cellValue <= parseFloat(rule.value2); + break; + case "text_contains": + conditionMet = (cellData?.value || "") + .toLowerCase() + .includes(rule.value1.toLowerCase()); + break; + case "blank": + conditionMet = !cellData?.value; + break; + case "not_blank": + conditionMet = !!cellData?.value; + break; + default: + conditionMet = false; + } + + if (conditionMet && cellData) { + if (!cellData.style) cellData.style = {}; + Object.assign(cellData.style, rule.style); + renderCell(r, c); + } + } + } + } + + function showDataValidationModal() { + const { start, end } = state.selection; + const range = `${getColName(start.col)}${start.row + 1}:${getColName(end.col)}${end.row + 1}`; + const rangeInput = document.getElementById("dvRange"); + if (rangeInput) rangeInput.value = range; + showModal("dataValidationModal"); + handleDvTypeChange(); + } + + function switchDvTab(tabName) { + document.querySelectorAll(".dv-tab").forEach((tab) => { + tab.classList.toggle("active", tab.dataset.tab === tabName); + }); + document.querySelectorAll(".dv-tab-content").forEach((content) => { + const contentId = content.id + .replace("dv", "") + .replace("Tab", "") + .toLowerCase(); + content.classList.toggle("active", contentId === tabName); + }); + } + + function handleDvTypeChange() { + const dvType = document.getElementById("dvType")?.value; + const criteriaSection = document.getElementById("dvCriteriaSection"); + const valuesSection = document.getElementById("dvValuesSection"); + const listSection = document.getElementById("dvListSection"); + const value2Row = document.getElementById("dvValue2Row"); + const value1Label = document.getElementById("dvValue1Label"); + + if (criteriaSection) { + criteriaSection.style.display = + dvType === "any" || dvType === "list" || dvType === "custom" + ? "none" + : "block"; + } + + if (valuesSection) { + valuesSection.style.display = + dvType === "any" || dvType === "list" ? "none" : "block"; + } + + if (listSection) { + listSection.classList.toggle("hidden", dvType !== "list"); + } + + if (value1Label) { + value1Label.textContent = dvType === "custom" ? "Formula:" : "Minimum:"; + } + } + + function handleDvOperatorChange() { + const operator = document.getElementById("dvOperator")?.value; + const value2Row = document.getElementById("dvValue2Row"); + const value1Label = document.getElementById("dvValue1Label"); + + if (value2Row) { + value2Row.style.display = + operator === "between" || operator === "not_between" ? "block" : "none"; + } + + if (value1Label) { + if (operator === "between" || operator === "not_between") { + value1Label.textContent = "Minimum:"; + } else { + value1Label.textContent = "Value:"; + } + } + } + + function applyDataValidation() { + const rangeStr = document.getElementById("dvRange")?.value; + if (!rangeStr) { + alert("Please specify a range."); + return; + } + + const dvType = document.getElementById("dvType")?.value; + const operator = document.getElementById("dvOperator")?.value; + const value1 = document.getElementById("dvValue1")?.value; + const value2 = document.getElementById("dvValue2")?.value; + const listSource = document.getElementById("dvListSource")?.value; + const showInput = document.getElementById("dvShowInput")?.checked; + const inputTitle = document.getElementById("dvInputTitle")?.value; + const inputMessage = document.getElementById("dvInputMessage")?.value; + const showError = document.getElementById("dvShowError")?.checked; + const errorStyle = document.getElementById("dvErrorStyle")?.value; + const errorTitle = document.getElementById("dvErrorTitle")?.value; + const errorMessage = document.getElementById("dvErrorMessage")?.value; + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.validations) ws.validations = {}; + + const validation = { + type: dvType, + operator, + value1, + value2, + listValues: listSource ? listSource.split(",").map((s) => s.trim()) : [], + showInput, + inputTitle, + inputMessage, + showError, + errorStyle, + errorTitle, + errorMessage, + }; + + const rangeParts = rangeStr.split(":"); + const startRef = parseCellRef(rangeParts[0]); + const endRef = + rangeParts.length > 1 ? parseCellRef(rangeParts[1]) : startRef; + + if (startRef && endRef) { + for (let r = startRef.row; r <= endRef.row; r++) { + for (let c = startRef.col; c <= endRef.col; c++) { + ws.validations[`${r},${c}`] = validation; + } + } + } + + hideModal("dataValidationModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Data validation applied!"); + } + + function clearDataValidation() { + const rangeStr = document.getElementById("dvRange")?.value; + if (!rangeStr) return; + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.validations) return; + + const rangeParts = rangeStr.split(":"); + const startRef = parseCellRef(rangeParts[0]); + const endRef = + rangeParts.length > 1 ? parseCellRef(rangeParts[1]) : startRef; + + if (startRef && endRef) { + for (let r = startRef.row; r <= endRef.row; r++) { + for (let c = startRef.col; c <= endRef.col; c++) { + delete ws.validations[`${r},${c}`]; + } + } + } + + hideModal("dataValidationModal"); + state.isDirty = true; + scheduleAutoSave(); + } + + function showPrintPreview() { + showModal("printPreviewModal"); + updatePrintPreview(); + } + + function updatePrintPreview() { + const orientation = + document.getElementById("printOrientation")?.value || "portrait"; + const showGridlines = document.getElementById("printGridlines")?.checked; + const showHeaders = document.getElementById("printHeaders")?.checked; + const printPage = document.getElementById("printPage"); + const printContent = document.getElementById("printContent"); + + if (printPage) { + printPage.className = `print-page ${orientation}`; + } + + if (!printContent) return; + + const ws = state.worksheets[state.activeWorksheet]; + let html = ""; + + if (showHeaders) { + html += ""; + for (let c = 0; c < CONFIG.COLS; c++) { + html += ``; + } + html += ""; + } + + html += ""; + let hasData = false; + let maxRow = 0; + let maxCol = 0; + + for (const key in ws.data) { + if (ws.data[key]?.value) { + hasData = true; + const [r, c] = key.split(",").map(Number); + maxRow = Math.max(maxRow, r); + maxCol = Math.max(maxCol, c); + } + } + + if (!hasData) { + maxRow = 10; + maxCol = 5; + } + + for (let r = 0; r <= maxRow; r++) { + html += ""; + if (showHeaders) { + html += ``; + } + for (let c = 0; c <= maxCol; c++) { + const key = `${r},${c}`; + const cellData = ws.data[key]; + const value = cellData?.value || ""; + const style = cellData?.style || {}; + let styleStr = ""; + + if (style.fontWeight) styleStr += `font-weight:${style.fontWeight};`; + if (style.fontStyle) styleStr += `font-style:${style.fontStyle};`; + if (style.textAlign) styleStr += `text-align:${style.textAlign};`; + if (style.color) styleStr += `color:${style.color};`; + if (style.background) styleStr += `background:${style.background};`; + + const borderStyle = showGridlines ? "" : "border:none;"; + html += ``; + } + html += ""; + } + + html += "
${getColName(c)}
${r + 1}${escapeHtml(value)}
"; + printContent.innerHTML = html; + } + + function printSheet() { + const printContent = document.getElementById("printContent")?.innerHTML; + if (!printContent) return; + + const orientation = + document.getElementById("printOrientation")?.value || "portrait"; + const printWindow = window.open("", "_blank"); + + printWindow.document.write(` + + + + ${state.sheetName} + + + + ${printContent} + + + `); + + printWindow.document.close(); + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + printWindow.close(); + }, 250); + + hideModal("printPreviewModal"); + } + + function insertChart() { + const chartType = + document.querySelector(".chart-type-btn.active")?.dataset.type || "bar"; + const dataRange = document.getElementById("chartDataRange")?.value; + const chartTitle = document.getElementById("chartTitle")?.value || "Chart"; + + if (!dataRange) { + alert("Please specify a data range."); + return; + } + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.charts) ws.charts = []; + + const chart = { + id: `chart_${Date.now()}`, + type: chartType, + title: chartTitle, + dataRange, + position: { + row: state.activeCell.row, + col: state.activeCell.col, + width: 400, + height: 300, + }, + }; + + ws.charts.push(chart); + hideModal("chartModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage( + "assistant", + `${chartType.charAt(0).toUpperCase() + chartType.slice(1)} chart created!`, + ); + } + + function showInsertImageModal() { + showModal("insertImageModal"); + } + + function switchImgTab(tabName) { + document.querySelectorAll(".img-tab").forEach((tab) => { + tab.classList.toggle("active", tab.dataset.tab === tabName); + }); + document.querySelectorAll(".img-tab-content").forEach((content) => { + const contentId = content.id + .replace("img", "") + .replace("Tab", "") + .toLowerCase(); + content.classList.toggle("active", contentId === tabName); + }); + } + + function insertImage() { + const urlTab = document.getElementById("imgUrlTab"); + const isUrlTab = urlTab?.classList.contains("active"); + let imageUrl; + + if (isUrlTab) { + imageUrl = document.getElementById("imgUrl")?.value; + } else { + const fileInput = document.getElementById("imgFile"); + if (fileInput?.files?.[0]) { + addChatMessage( + "assistant", + "Image upload coming soon! Please use a URL for now.", + ); + return; + } + } + + if (!imageUrl) { + alert("Please enter an image URL."); + return; + } + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.images) ws.images = []; + + const image = { + id: `img_${Date.now()}`, + url: imageUrl, + position: { + row: state.activeCell.row, + col: state.activeCell.col, + width: 200, + height: 150, + }, + }; + + ws.images.push(image); + hideModal("insertImageModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Image inserted!"); + } + + function toggleFilter() { + const ws = state.worksheets[state.activeWorksheet]; + ws.filterEnabled = !ws.filterEnabled; + addChatMessage( + "assistant", + ws.filterEnabled + ? "Filter enabled. Click column headers to filter." + : "Filter disabled.", + ); + } + + function selectCustomFormat(formatCode) { + document.querySelectorAll(".cnf-format-item").forEach((item) => { + item.classList.toggle("selected", item.dataset.format === formatCode); + }); + const formatInput = document.getElementById("cnfFormatCode"); + if (formatInput) { + formatInput.value = formatCode; + } + updateCnfPreview(); + } + + function updateCnfPreview() { + const formatCode = + document.getElementById("cnfFormatCode")?.value || "#,##0.00"; + const previewEl = document.getElementById("cnfPreview"); + if (!previewEl) return; + + const sampleValue = 1234.5678; + let formatted; + + if (formatCode.includes("$")) { + formatted = sampleValue.toLocaleString("en-US", { + style: "currency", + currency: "USD", + }); + } else if (formatCode.includes("%")) { + formatted = (sampleValue * 100).toFixed(2) + "%"; + } else if (formatCode.includes("E")) { + formatted = sampleValue.toExponential(2); + } else if (formatCode.includes("MM") || formatCode.includes("DD")) { + formatted = new Date().toLocaleDateString(); + } else if (formatCode.includes("HH")) { + formatted = new Date().toLocaleTimeString(); + } else { + const decimals = (formatCode.match(/0+$/)?.[0] || "").length; + formatted = sampleValue.toLocaleString("en-US", { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + } + + previewEl.textContent = formatted; + } + + function applyCustomNumberFormat() { + const formatCode = document.getElementById("cnfFormatCode")?.value; + if (!formatCode) return; + + saveToHistory(); + const { start, end } = state.selection; + const ws = state.worksheets[state.activeWorksheet]; + + for (let r = start.row; r <= end.row; r++) { + for (let c = start.col; c <= end.col; c++) { + const key = `${r},${c}`; + if (!ws.data[key]) ws.data[key] = { value: "" }; + ws.data[key].customFormat = formatCode; + renderCell(r, c); + } + } + + hideModal("customNumberFormatModal"); + state.isDirty = true; + scheduleAutoSave(); + } + + function renderCharts() { + const chartsContainer = document.getElementById("chartsContainer"); + if (!chartsContainer) return; + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.charts || ws.charts.length === 0) { + chartsContainer.innerHTML = ""; + return; + } + + chartsContainer.innerHTML = ws.charts + .map((chart) => renderChartHTML(chart)) + .join(""); + + chartsContainer.querySelectorAll(".chart-wrapper").forEach((wrapper) => { + const chartId = wrapper.dataset.chartId; + wrapper.addEventListener("click", () => selectChart(chartId)); + wrapper.querySelector(".chart-delete")?.addEventListener("click", (e) => { + e.stopPropagation(); + deleteChart(chartId); + }); + wrapper + .querySelector(".chart-header") + ?.addEventListener("mousedown", (e) => { + startDragChart(e, chartId); + }); + }); + } + + function renderChartHTML(chart) { + const { id, type, title, position, dataRange } = chart; + const left = position?.col ? position.col * CONFIG.COL_WIDTH : 100; + const top = position?.row ? position.row * CONFIG.ROW_HEIGHT : 100; + const width = position?.width || 400; + const height = position?.height || 300; + + const data = getChartData(dataRange); + let chartContent = ""; + + switch (type) { + case "bar": + chartContent = renderBarChart(data, height - 80); + break; + case "line": + chartContent = renderLineChart(data, width - 32, height - 80); + break; + case "pie": + chartContent = renderPieChart(data, Math.min(width, height) - 100); + break; + default: + chartContent = renderBarChart(data, height - 80); + } + + return ` +
+
+

${escapeHtml(title || "Chart")}

+
+ +
+
+
+ ${chartContent} +
+ ${renderChartLegend(data)} +
+ `; + } + + function getChartData(dataRange) { + if (!dataRange) return { labels: [], values: [] }; + + const ws = state.worksheets[state.activeWorksheet]; + const rangeParts = dataRange.split(":"); + if (rangeParts.length !== 2) return { labels: [], values: [] }; + + const startRef = parseCellRef(rangeParts[0]); + const endRef = parseCellRef(rangeParts[1]); + if (!startRef || !endRef) return { labels: [], values: [] }; + + const labels = []; + const values = []; + + if (startRef.col === endRef.col) { + for (let r = startRef.row; r <= endRef.row; r++) { + const key = `${r},${startRef.col}`; + const cellData = ws.data[key]; + const val = parseFloat(cellData?.value) || 0; + values.push(val); + labels.push(`Row ${r + 1}`); + } + } else { + for (let c = startRef.col; c <= endRef.col; c++) { + const key = `${startRef.row},${c}`; + const cellData = ws.data[key]; + const val = parseFloat(cellData?.value) || 0; + values.push(val); + labels.push(getColName(c)); + } + } + + return { labels, values }; + } + + function renderBarChart(data, maxHeight) { + if (!data.values.length) return '
No data
'; + + const maxVal = Math.max(...data.values, 1); + const bars = data.values + .map((val, i) => { + const height = (val / maxVal) * maxHeight; + return `
`; + }) + .join(""); + + return `
${bars}
`; + } + + function renderLineChart(data, width, height) { + if (!data.values.length) return '
No data
'; + + const maxVal = Math.max(...data.values, 1); + const padding = 20; + const chartWidth = width - padding * 2; + const chartHeight = height - padding * 2; + + const points = data.values.map((val, i) => { + const x = padding + (i / (data.values.length - 1 || 1)) * chartWidth; + const y = padding + chartHeight - (val / maxVal) * chartHeight; + return `${x},${y}`; + }); + + const circles = data.values + .map((val, i) => { + const x = padding + (i / (data.values.length - 1 || 1)) * chartWidth; + const y = padding + chartHeight - (val / maxVal) * chartHeight; + return ``; + }) + .join(""); + + return ` + + + ${circles} + + `; + } + + function renderPieChart(data, size) { + if (!data.values.length) return '
No data
'; + + const total = data.values.reduce((a, b) => a + b, 0) || 1; + const colors = [ + "#4285f4", + "#34a853", + "#fbbc04", + "#ea4335", + "#9c27b0", + "#00bcd4", + "#ff5722", + ]; + const cx = size / 2; + const cy = size / 2; + const r = size / 2 - 10; + + let startAngle = 0; + const slices = data.values + .map((val, i) => { + const angle = (val / total) * 360; + const endAngle = startAngle + angle; + const largeArc = angle > 180 ? 1 : 0; + + const x1 = cx + r * Math.cos((startAngle * Math.PI) / 180); + const y1 = cy + r * Math.sin((startAngle * Math.PI) / 180); + const x2 = cx + r * Math.cos((endAngle * Math.PI) / 180); + const y2 = cy + r * Math.sin((endAngle * Math.PI) / 180); + + const path = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`; + startAngle = endAngle; + + return ``; + }) + .join(""); + + return ` +
+ + ${slices} + +
+ `; + } + + function renderChartLegend(data) { + const colors = [ + "#4285f4", + "#34a853", + "#fbbc04", + "#ea4335", + "#9c27b0", + "#00bcd4", + "#ff5722", + ]; + const items = data.labels + .map( + (label, i) => + `
${escapeHtml(label)}
`, + ) + .join(""); + + return `
${items}
`; + } + + function selectChart(chartId) { + document.querySelectorAll(".chart-wrapper").forEach((el) => { + el.classList.toggle("selected", el.dataset.chartId === chartId); + }); + } + + function deleteChart(chartId) { + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.charts) return; + + ws.charts = ws.charts.filter((c) => c.id !== chartId); + renderCharts(); + state.isDirty = true; + scheduleAutoSave(); + } + + function startDragChart(e, chartId) { + const wrapper = document.querySelector(`[data-chart-id="${chartId}"]`); + if (!wrapper) return; + + const startX = e.clientX; + const startY = e.clientY; + const startLeft = parseInt(wrapper.style.left) || 0; + const startTop = parseInt(wrapper.style.top) || 0; + + const onMouseMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + wrapper.style.left = `${startLeft + dx}px`; + wrapper.style.top = `${startTop + dy}px`; + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + const ws = state.worksheets[state.activeWorksheet]; + const chart = ws.charts?.find((c) => c.id === chartId); + if (chart) { + chart.position.col = Math.round( + parseInt(wrapper.style.left) / CONFIG.COL_WIDTH, + ); + chart.position.row = Math.round( + parseInt(wrapper.style.top) / CONFIG.ROW_HEIGHT, + ); + state.isDirty = true; + scheduleAutoSave(); + } + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + } + + function renderImages() { + const imagesContainer = document.getElementById("imagesContainer"); + if (!imagesContainer) return; + + const ws = state.worksheets[state.activeWorksheet]; + if (!ws.images || ws.images.length === 0) { + imagesContainer.innerHTML = ""; + return; + } + + imagesContainer.innerHTML = ws.images + .map((img) => { + const left = img.position?.col + ? img.position.col * CONFIG.COL_WIDTH + : 100; + const top = img.position?.row + ? img.position.row * CONFIG.ROW_HEIGHT + : 100; + const width = img.position?.width || 200; + const height = img.position?.height || 150; + + return ` +
+ Embedded image +
+
+ `; + }) + .join(""); + + imagesContainer.querySelectorAll(".image-wrapper").forEach((wrapper) => { + const imageId = wrapper.dataset.imageId; + wrapper.addEventListener("click", () => selectImage(imageId)); + wrapper.addEventListener("mousedown", (e) => { + if (e.target.classList.contains("image-resize-handle")) { + startResizeImage(e, imageId); + } else { + startDragImage(e, imageId); + } + }); + }); + } + + function selectImage(imageId) { + document.querySelectorAll(".image-wrapper").forEach((el) => { + el.classList.toggle("selected", el.dataset.imageId === imageId); + }); + } + + function startDragImage(e, imageId) { + const wrapper = document.querySelector(`[data-image-id="${imageId}"]`); + if (!wrapper) return; + + const startX = e.clientX; + const startY = e.clientY; + const startLeft = parseInt(wrapper.style.left) || 0; + const startTop = parseInt(wrapper.style.top) || 0; + + const onMouseMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + wrapper.style.left = `${startLeft + dx}px`; + wrapper.style.top = `${startTop + dy}px`; + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + const ws = state.worksheets[state.activeWorksheet]; + const img = ws.images?.find((i) => i.id === imageId); + if (img) { + img.position.col = Math.round( + parseInt(wrapper.style.left) / CONFIG.COL_WIDTH, + ); + img.position.row = Math.round( + parseInt(wrapper.style.top) / CONFIG.ROW_HEIGHT, + ); + state.isDirty = true; + scheduleAutoSave(); + } + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + e.preventDefault(); + } + + function startResizeImage(e, imageId) { + const wrapper = document.querySelector(`[data-image-id="${imageId}"]`); + if (!wrapper) return; + + const startX = e.clientX; + const startY = e.clientY; + const startWidth = parseInt(wrapper.style.width) || 200; + const startHeight = parseInt(wrapper.style.height) || 150; + const aspectRatio = startWidth / startHeight; + + const onMouseMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + const newWidth = Math.max(50, startWidth + dx); + const newHeight = newWidth / aspectRatio; + wrapper.style.width = `${newWidth}px`; + wrapper.style.height = `${newHeight}px`; + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + const ws = state.worksheets[state.activeWorksheet]; + const img = ws.images?.find((i) => i.id === imageId); + if (img) { + img.position.width = parseInt(wrapper.style.width); + img.position.height = parseInt(wrapper.style.height); + state.isDirty = true; + scheduleAutoSave(); + } + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + e.preventDefault(); + e.stopPropagation(); + } + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { diff --git a/ui/suite/slides/slides.css b/ui/suite/slides/slides.css index 527d872..7917555 100644 --- a/ui/suite/slides/slides.css +++ b/ui/suite/slides/slides.css @@ -118,7 +118,9 @@ border-radius: 4px; cursor: pointer; color: var(--sentient-text-secondary, #666666); - transition: background 0.15s, color 0.15s; + transition: + background 0.15s, + color 0.15s; } .btn-icon:hover { @@ -302,7 +304,9 @@ cursor: pointer; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: border-color 0.15s, box-shadow 0.15s; + transition: + border-color 0.15s, + box-shadow 0.15s; } .slide-thumbnail:hover { @@ -349,7 +353,10 @@ color: var(--sentient-text-secondary, #666666); font-size: 13px; cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; + transition: + background 0.15s, + border-color 0.15s, + color 0.15s; } .btn-add-slide:hover { @@ -453,14 +460,50 @@ cursor: pointer; } -.handle.nw { top: -5px; left: -5px; cursor: nwse-resize; } -.handle.n { top: -5px; left: 50%; transform: translateX(-50%); cursor: ns-resize; } -.handle.ne { top: -5px; right: -5px; cursor: nesw-resize; } -.handle.w { top: 50%; left: -5px; transform: translateY(-50%); cursor: ew-resize; } -.handle.e { top: 50%; right: -5px; transform: translateY(-50%); cursor: ew-resize; } -.handle.sw { bottom: -5px; left: -5px; cursor: nesw-resize; } -.handle.s { bottom: -5px; left: 50%; transform: translateX(-50%); cursor: ns-resize; } -.handle.se { bottom: -5px; right: -5px; cursor: nwse-resize; } +.handle.nw { + top: -5px; + left: -5px; + cursor: nwse-resize; +} +.handle.n { + top: -5px; + left: 50%; + transform: translateX(-50%); + cursor: ns-resize; +} +.handle.ne { + top: -5px; + right: -5px; + cursor: nesw-resize; +} +.handle.w { + top: 50%; + left: -5px; + transform: translateY(-50%); + cursor: ew-resize; +} +.handle.e { + top: 50%; + right: -5px; + transform: translateY(-50%); + cursor: ew-resize; +} +.handle.sw { + bottom: -5px; + left: -5px; + cursor: nesw-resize; +} +.handle.s { + bottom: -5px; + left: 50%; + transform: translateX(-50%); + cursor: ns-resize; +} +.handle.se { + bottom: -5px; + right: -5px; + cursor: nwse-resize; +} .rotate-handle { position: absolute; @@ -497,7 +540,9 @@ position: absolute; width: 16px; height: 16px; - transition: left 0.1s, top 0.1s; + transition: + left 0.1s, + top 0.1s; } .cursor-indicator::after { @@ -524,7 +569,9 @@ display: flex; flex-direction: column; flex-shrink: 0; - transition: width 0.2s ease, opacity 0.2s ease; + transition: + width 0.2s ease, + opacity 0.2s ease; } .chat-panel.collapsed { @@ -663,7 +710,10 @@ font-size: 12px; color: var(--sentient-text-secondary, #666666); cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; + transition: + background 0.15s, + border-color 0.15s, + color 0.15s; } .suggestion-btn:hover { @@ -932,7 +982,10 @@ border-radius: 8px; cursor: pointer; color: var(--sentient-text-secondary, #666666); - transition: background 0.15s, border-color 0.15s, color 0.15s; + transition: + background 0.15s, + border-color 0.15s, + color 0.15s; } .shape-btn:hover { @@ -1034,3 +1087,731 @@ background: transparent; border-radius: 8px; cursor: pointer; +} + +/* ============================================================================= + TRANSITIONS MODAL + ============================================================================= */ + +.transitions-section { + margin-bottom: 20px; +} + +.transitions-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 10px; +} + +.transitions-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; +} + +.transition-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border: 2px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.transition-btn:hover { + background: var(--sentient-bg-primary, #ffffff); + border-color: var(--sentient-border, #e0e0e0); +} + +.transition-btn.active { + border-color: var(--sentient-accent, #4285f4); + background: rgba(66, 133, 244, 0.1); +} + +.transition-btn span { + font-size: 11px; + color: var(--sentient-text-secondary, #666); +} + +.transition-preview { + width: 48px; + height: 32px; + background: var(--sentient-accent, #4285f4); + border-radius: 4px; + position: relative; + overflow: hidden; +} + +.transition-preview::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.3) 0%, + transparent 50% + ); +} + +.duration-control { + display: flex; + align-items: center; + gap: 12px; +} + +.duration-control input[type="range"] { + flex: 1; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: var(--sentient-border, #e0e0e0); + border-radius: 2px; +} + +.duration-control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: var(--sentient-accent, #4285f4); + border-radius: 50%; + cursor: pointer; +} + +.duration-control span { + min-width: 40px; + font-size: 13px; + color: var(--sentient-text-primary, #212121); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--sentient-text-primary, #212121); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--sentient-accent, #4285f4); +} + +/* ============================================================================= + ANIMATIONS MODAL + ============================================================================= */ + +.animations-section { + margin-bottom: 20px; +} + +.animations-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 8px; +} + +.animations-section select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.animations-section select:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.selected-element-info { + padding: 12px 16px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + color: var(--sentient-text-secondary, #666); +} + +.animation-timing { + display: flex; + gap: 16px; +} + +.timing-group { + display: flex; + align-items: center; + gap: 8px; +} + +.timing-group label { + margin-bottom: 0; + min-width: auto; +} + +.timing-group select, +.timing-group input[type="number"] { + width: auto; + min-width: 80px; + padding: 8px 10px; +} + +.timing-group span { + font-size: 12px; + color: var(--sentient-text-secondary, #666); +} + +.animation-order-list { + min-height: 100px; + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + padding: 8px; +} + +.animation-order-list .no-animations { + text-align: center; + color: var(--sentient-text-muted, #999); + font-size: 13px; + padding: 24px; + margin: 0; +} + +.animation-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: 4px; + margin-bottom: 4px; + cursor: grab; +} + +.animation-item:last-child { + margin-bottom: 0; +} + +.animation-item:active { + cursor: grabbing; +} + +.animation-item .animation-name { + font-size: 13px; + color: var(--sentient-text-primary, #212121); +} + +.animation-item .animation-element { + font-size: 11px; + color: var(--sentient-text-secondary, #666); +} + +.animation-item .animation-remove { + width: 20px; + height: 20px; + border: none; + background: none; + color: var(--sentient-text-muted, #999); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.animation-item .animation-remove:hover { + color: var(--sentient-error, #ea4335); +} + +/* ============================================================================= + MODAL LARGE + ============================================================================= */ + +.modal-content.modal-large { + max-width: 600px; +} + +.modal-content.modal-fullscreen { + width: 95vw; + max-width: 1400px; + height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-fullscreen .modal-header { + flex-shrink: 0; +} + +.modal-fullscreen .modal-body { + flex: 1; + overflow: hidden; + padding: 0; +} + +/* ============================================================================= + SLIDE SORTER VIEW + ============================================================================= */ + +.sorter-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + margin: 0 24px; +} + +.sorter-body { + display: flex; + background: var(--sentient-bg-tertiary, #e0e0e0); + overflow: auto; + padding: 24px; +} + +.sorter-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 24px; + width: 100%; + align-content: start; +} + +.sorter-slide { + position: relative; + aspect-ratio: 16 / 9; + background: white; + border: 2px solid transparent; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + cursor: grab; + overflow: hidden; + transition: all 0.15s ease; +} + +.sorter-slide:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.sorter-slide.selected { + border-color: var(--sentient-accent, #4285f4); + box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.2); +} + +.sorter-slide.dragging { + opacity: 0.5; + cursor: grabbing; +} + +.sorter-slide.drag-over { + border-color: var(--sentient-accent, #4285f4); + background: rgba(66, 133, 244, 0.1); +} + +.sorter-slide-content { + width: 100%; + height: 100%; + pointer-events: none; + transform-origin: top left; +} + +.sorter-slide-number { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.sorter-slide-actions { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.sorter-slide:hover .sorter-slide-actions { + opacity: 1; +} + +.sorter-slide-actions button { + width: 28px; + height: 28px; + border: none; + background: rgba(0, 0, 0, 0.6); + color: white; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.sorter-slide-actions button:hover { + background: rgba(0, 0, 0, 0.8); +} + +/* ============================================================================= + EXPORT PDF MODAL + ============================================================================= */ + +.export-section { + margin-bottom: 20px; +} + +.export-section label { + display: block; + font-size: 13px; + font-weight: 500; + color: var(--sentient-text-secondary, #666); + margin-bottom: 8px; +} + +.export-section select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.export-section select:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.export-range-options { + display: flex; + flex-direction: column; + gap: 10px; +} + +.radio-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--sentient-text-primary, #212121); + cursor: pointer; +} + +.radio-label input[type="radio"] { + width: 16px; + height: 16px; + accent-color: var(--sentient-accent, #4285f4); +} + +.range-input { + margin-left: 8px; + padding: 6px 10px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 13px; + width: 120px; +} + +.range-input:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +@media (max-width: 768px) { + .transitions-grid { + grid-template-columns: repeat(3, 1fr); + } + + .animation-timing { + flex-direction: column; + gap: 12px; + } + + .timing-group { + width: 100%; + } + + .timing-group select, + .timing-group input[type="number"] { + flex: 1; + } + + .sorter-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; + } + + .sorter-toolbar { + flex-wrap: wrap; + margin: 0 12px; + } +} + +/* ============================================================================= + MASTER SLIDE MODAL + ============================================================================= */ + +.master-layout { + display: flex; + gap: 24px; + min-height: 400px; +} + +.master-sidebar { + width: 200px; + flex-shrink: 0; + border-right: 1px solid var(--sentient-border, #e0e0e0); + padding-right: 24px; +} + +.master-sidebar h4 { + margin: 0 0 16px 0; + font-size: 13px; + font-weight: 600; + color: var(--sentient-text-primary, #212121); +} + +.master-layout-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.master-layout-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px; + background: var(--sentient-bg-secondary, #f5f5f5); + border: 2px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.master-layout-item:hover { + background: var(--sentient-bg-primary, #ffffff); + border-color: var(--sentient-border, #e0e0e0); +} + +.master-layout-item.active { + border-color: var(--sentient-accent, #4285f4); + background: rgba(66, 133, 244, 0.1); +} + +.master-layout-item span { + font-size: 11px; + color: var(--sentient-text-secondary, #666); +} + +.layout-preview { + width: 80px; + height: 45px; + background: white; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: 4px; + padding: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.preview-title { + height: 8px; + background: var(--sentient-text-primary, #212121); + border-radius: 2px; +} + +.preview-title.small { + height: 5px; + width: 60%; +} + +.preview-subtitle { + height: 5px; + width: 50%; + background: var(--sentient-text-secondary, #666); + border-radius: 2px; +} + +.preview-content { + flex: 1; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: 2px; +} + +.preview-columns { + flex: 1; + display: flex; + gap: 4px; +} + +.preview-col { + flex: 1; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: 2px; +} + +.preview-section-title { + height: 10px; + width: 70%; + margin: auto; + background: var(--sentient-text-primary, #212121); + border-radius: 2px; +} + +.blank-preview { + background: white; +} + +.master-editor { + flex: 1; +} + +.master-section { + margin-bottom: 24px; +} + +.master-section h4 { + margin: 0 0 12px 0; + font-size: 13px; + font-weight: 600; + color: var(--sentient-text-primary, #212121); +} + +.color-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.color-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.color-item label { + font-size: 12px; + color: var(--sentient-text-secondary, #666); +} + +.color-item input[type="color"] { + width: 100%; + height: 36px; + padding: 2px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: 4px; + cursor: pointer; +} + +.font-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.font-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.font-item label { + font-size: 12px; + color: var(--sentient-text-secondary, #666); +} + +.font-item select { + padding: 10px 12px; + border: 1px solid var(--sentient-border, #e0e0e0); + border-radius: var(--sentient-radius-sm, 4px); + font-size: 14px; + background: var(--sentient-bg-primary, #ffffff); + color: var(--sentient-text-primary, #212121); +} + +.font-item select:focus { + outline: none; + border-color: var(--sentient-accent, #4285f4); +} + +.master-preview { + padding: 16px; + background: var(--sentient-bg-secondary, #f5f5f5); + border-radius: 8px; +} + +.preview-slide { + aspect-ratio: 16 / 9; + background: white; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.preview-heading { + margin: 0 0 12px 0; + font-size: 24px; + font-weight: 600; +} + +.preview-body { + margin: 0; + font-size: 14px; +} + +@media (max-width: 768px) { + .master-layout { + flex-direction: column; + } + + .master-sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--sentient-border, #e0e0e0); + padding-right: 0; + padding-bottom: 16px; + } + + .master-layout-list { + flex-direction: row; + flex-wrap: wrap; + } + + .master-layout-item { + width: calc(33.33% - 8px); + } + + .color-grid { + grid-template-columns: repeat(2, 1fr); + } + + .font-grid { + grid-template-columns: 1fr; + } +} diff --git a/ui/suite/slides/slides.html b/ui/suite/slides/slides.html index 631b658..be5d7a9 100644 --- a/ui/suite/slides/slides.html +++ b/ui/suite/slides/slides.html @@ -111,6 +111,46 @@ +
+ + +
+
+ 0.5s +
+ +
+ +
+ + + + + + + + + + + + + diff --git a/ui/suite/slides/slides.js b/ui/suite/slides/slides.js index f25fcc3..b006ffa 100644 --- a/ui/suite/slides/slides.js +++ b/ui/suite/slides/slides.js @@ -141,6 +141,123 @@ .getElementById("shareBtn") ?.addEventListener("click", () => showModal("shareModal")); + document + .getElementById("transitionsBtn") + ?.addEventListener("click", showTransitionsModal); + document + .getElementById("closeTransitionsModal") + ?.addEventListener("click", () => hideModal("transitionsModal")); + document + .getElementById("applyTransitionsBtn") + ?.addEventListener("click", applyTransition); + document + .getElementById("cancelTransitionsBtn") + ?.addEventListener("click", () => hideModal("transitionsModal")); + document + .getElementById("transitionDuration") + ?.addEventListener("input", updateDurationDisplay); + document.querySelectorAll(".transition-btn").forEach((btn) => { + btn.addEventListener("click", () => + selectTransition(btn.dataset.transition), + ); + }); + + document + .getElementById("animationsBtn") + ?.addEventListener("click", showAnimationsModal); + document + .getElementById("closeAnimationsModal") + ?.addEventListener("click", () => hideModal("animationsModal")); + document + .getElementById("applyAnimationsBtn") + ?.addEventListener("click", applyAnimation); + document + .getElementById("cancelAnimationsBtn") + ?.addEventListener("click", () => hideModal("animationsModal")); + document + .getElementById("previewAnimationBtn") + ?.addEventListener("click", previewAnimation); + + document + .getElementById("slideSorterBtn") + ?.addEventListener("click", showSlideSorter); + document + .getElementById("closeSlideSorterModal") + ?.addEventListener("click", () => hideModal("slideSorterModal")); + document + .getElementById("applySorterBtn") + ?.addEventListener("click", applySorterChanges); + document + .getElementById("cancelSorterBtn") + ?.addEventListener("click", () => hideModal("slideSorterModal")); + document + .getElementById("sorterAddSlide") + ?.addEventListener("click", sorterAddSlide); + document + .getElementById("sorterDuplicateSlide") + ?.addEventListener("click", sorterDuplicateSlide); + document + .getElementById("sorterDeleteSlide") + ?.addEventListener("click", sorterDeleteSlide); + + document + .getElementById("masterSlideBtn") + ?.addEventListener("click", showMasterSlideModal); + document + .getElementById("closeMasterSlideModal") + ?.addEventListener("click", () => hideModal("masterSlideModal")); + document + .getElementById("applyMasterBtn") + ?.addEventListener("click", applyMasterSlide); + document + .getElementById("cancelMasterBtn") + ?.addEventListener("click", () => hideModal("masterSlideModal")); + document + .getElementById("resetMasterBtn") + ?.addEventListener("click", resetMasterSlide); + document.querySelectorAll(".master-layout-item").forEach((item) => { + item.addEventListener("click", () => + selectMasterLayout(item.dataset.layout), + ); + }); + document + .getElementById("masterPrimaryColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterSecondaryColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterAccentColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterBgColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterTextColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterTextLightColor") + ?.addEventListener("input", updateMasterPreview); + document + .getElementById("masterHeadingFont") + ?.addEventListener("change", updateMasterPreview); + document + .getElementById("masterBodyFont") + ?.addEventListener("change", updateMasterPreview); + + document + .getElementById("exportPdfBtn") + ?.addEventListener("click", showExportPdfModal); + document + .getElementById("closeExportPdfModal") + ?.addEventListener("click", () => hideModal("exportPdfModal")); + document + .getElementById("exportPdfBtnConfirm") + ?.addEventListener("click", exportToPdf); + document + .getElementById("cancelExportPdfBtn") + ?.addEventListener("click", () => hideModal("exportPdfModal")); + document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); @@ -2008,6 +2125,780 @@ return localStorage.getItem("gb-user-name") || "Anonymous"; } + function showTransitionsModal() { + showModal("transitionsModal"); + const currentSlide = state.slides[state.currentSlideIndex]; + if (currentSlide?.transition?.transition_type) { + selectTransition(currentSlide.transition.transition_type); + } + if (currentSlide?.transition?.duration) { + const durationInput = document.getElementById("transitionDuration"); + const durationValue = document.getElementById("durationValue"); + if (durationInput) durationInput.value = currentSlide.transition.duration; + if (durationValue) + durationValue.textContent = `${currentSlide.transition.duration}s`; + } + } + + function selectTransition(transitionType) { + document.querySelectorAll(".transition-btn").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.transition === transitionType); + }); + } + + function updateDurationDisplay() { + const durationInput = document.getElementById("transitionDuration"); + const durationValue = document.getElementById("durationValue"); + if (durationInput && durationValue) { + durationValue.textContent = `${durationInput.value}s`; + } + } + + function applyTransition() { + const activeBtn = document.querySelector(".transition-btn.active"); + const transitionType = activeBtn?.dataset.transition || "none"; + const duration = parseFloat( + document.getElementById("transitionDuration")?.value || 0.5, + ); + const applyToAll = document.getElementById("applyToAllSlides")?.checked; + + saveToHistory(); + + const transition = { + transition_type: transitionType, + duration: duration, + }; + + if (applyToAll) { + state.slides.forEach((slide) => { + slide.transition = { ...transition }; + }); + addChatMessage( + "assistant", + `Applied ${transitionType} transition to all slides.`, + ); + } else { + const currentSlide = state.slides[state.currentSlideIndex]; + if (currentSlide) { + currentSlide.transition = transition; + } + addChatMessage( + "assistant", + `Applied ${transitionType} transition to current slide.`, + ); + } + + hideModal("transitionsModal"); + state.isDirty = true; + scheduleAutoSave(); + } + + function showAnimationsModal() { + showModal("animationsModal"); + updateSelectedElementInfo(); + updateAnimationOrderList(); + } + + function updateSelectedElementInfo() { + const infoEl = document.getElementById("selectedElementInfo"); + if (!infoEl) return; + + if (state.selectedElement) { + const slide = state.slides[state.currentSlideIndex]; + const element = slide?.elements?.find( + (el) => el.id === state.selectedElement, + ); + if (element) { + const type = element.element_type || "Unknown"; + const content = + element.content?.text?.substring(0, 30) || + element.content?.shape_type || + ""; + infoEl.textContent = `${type}: ${content}${content.length > 30 ? "..." : ""}`; + return; + } + } + infoEl.textContent = "No element selected"; + } + + function updateAnimationOrderList() { + const listEl = document.getElementById("animationOrderList"); + if (!listEl) return; + + const slide = state.slides[state.currentSlideIndex]; + const animations = []; + + slide?.elements?.forEach((element) => { + if (element.animations?.length > 0) { + element.animations.forEach((anim) => { + animations.push({ + elementId: element.id, + elementType: element.element_type, + animation: anim, + }); + }); + } + }); + + if (animations.length === 0) { + listEl.innerHTML = '

No animations added yet

'; + return; + } + + listEl.innerHTML = animations + .map( + (item, index) => ` +
+
+
${item.animation.type || "Animation"}
+
${item.elementType}
+
+ +
+ `, + ) + .join(""); + + listEl.querySelectorAll(".animation-remove").forEach((btn) => { + btn.addEventListener("click", () => removeAnimation(btn.dataset.element)); + }); + } + + function applyAnimation() { + if (!state.selectedElement) { + addChatMessage( + "assistant", + "Please select an element on the slide first.", + ); + return; + } + + const entrance = document.getElementById("entranceAnimation")?.value; + const emphasis = document.getElementById("emphasisAnimation")?.value; + const exit = document.getElementById("exitAnimation")?.value; + const start = + document.getElementById("animationStart")?.value || "on-click"; + const duration = parseFloat( + document.getElementById("animationDuration")?.value || 0.5, + ); + const delay = parseFloat( + document.getElementById("animationDelay")?.value || 0, + ); + + const slide = state.slides[state.currentSlideIndex]; + const element = slide?.elements?.find( + (el) => el.id === state.selectedElement, + ); + + if (!element) return; + + saveToHistory(); + + element.animations = []; + + if (entrance && entrance !== "none") { + element.animations.push({ + type: entrance, + category: "entrance", + start, + duration, + delay, + }); + } + + if (emphasis && emphasis !== "none") { + element.animations.push({ + type: emphasis, + category: "emphasis", + start: "after-previous", + duration, + delay: 0, + }); + } + + if (exit && exit !== "none") { + element.animations.push({ + type: exit, + category: "exit", + start: "after-previous", + duration, + delay: 0, + }); + } + + updateAnimationOrderList(); + hideModal("animationsModal"); + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Animation applied to selected element."); + } + + function removeAnimation(elementId) { + const slide = state.slides[state.currentSlideIndex]; + const element = slide?.elements?.find((el) => el.id === elementId); + if (element) { + element.animations = []; + updateAnimationOrderList(); + state.isDirty = true; + scheduleAutoSave(); + } + } + + function previewAnimation() { + if (!state.selectedElement) { + addChatMessage( + "assistant", + "Select an element to preview its animation.", + ); + return; + } + + const entrance = document.getElementById("entranceAnimation")?.value; + const node = document.querySelector( + `[data-element-id="${state.selectedElement}"]`, + ); + + if (!node || !entrance || entrance === "none") return; + + node.style.animation = "none"; + node.offsetHeight; + + const animationName = entrance.replace(/-/g, ""); + node.style.animation = `${animationName} 0.5s ease`; + + setTimeout(() => { + node.style.animation = ""; + }, 600); + } + + let sorterSlideOrder = []; + let sorterSelectedSlide = null; + + function showSlideSorter() { + showModal("slideSorterModal"); + sorterSlideOrder = state.slides.map((_, i) => i); + sorterSelectedSlide = null; + renderSorterGrid(); + } + + function renderSorterGrid() { + const grid = document.getElementById("sorterGrid"); + if (!grid) return; + + grid.innerHTML = sorterSlideOrder + .map((slideIndex, position) => { + const slide = state.slides[slideIndex]; + if (!slide) return ""; + + const isSelected = sorterSelectedSlide === position; + return ` +
+
+ ${renderSorterSlidePreview(slide)} +
+
${position + 1}
+
+ + +
+
+ `; + }) + .join(""); + + grid.querySelectorAll(".sorter-slide").forEach((el) => { + el.addEventListener("click", (e) => { + if (e.target.closest(".sorter-slide-actions")) return; + sorterSelectSlide(parseInt(el.dataset.position)); + }); + + el.addEventListener("dragstart", handleSorterDragStart); + el.addEventListener("dragover", handleSorterDragOver); + el.addEventListener("drop", handleSorterDrop); + el.addEventListener("dragend", handleSorterDragEnd); + + el.querySelectorAll(".sorter-slide-actions button").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + const action = btn.dataset.action; + const position = parseInt(el.dataset.position); + if (action === "duplicate") { + sorterDuplicateAt(position); + } else if (action === "delete") { + sorterDeleteAt(position); + } + }); + }); + }); + } + + function renderSorterSlidePreview(slide) { + const bgColor = slide.background?.color || "#ffffff"; + let html = `
`; + + if (slide.elements) { + slide.elements.slice(0, 3).forEach((el) => { + if (el.element_type === "text" && el.content?.text) { + const text = el.content.text.substring(0, 50); + html += `
${escapeHtml(text)}
`; + } + }); + } + + html += "
"; + return html; + } + + function sorterSelectSlide(position) { + sorterSelectedSlide = position; + document.querySelectorAll(".sorter-slide").forEach((el) => { + el.classList.toggle( + "selected", + parseInt(el.dataset.position) === position, + ); + }); + } + + let draggedPosition = null; + + function handleSorterDragStart(e) { + draggedPosition = parseInt(e.currentTarget.dataset.position); + e.currentTarget.classList.add("dragging"); + e.dataTransfer.effectAllowed = "move"; + } + + function handleSorterDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + e.currentTarget.classList.add("drag-over"); + } + + function handleSorterDrop(e) { + e.preventDefault(); + const targetPosition = parseInt(e.currentTarget.dataset.position); + + if (draggedPosition !== null && draggedPosition !== targetPosition) { + const draggedIndex = sorterSlideOrder[draggedPosition]; + sorterSlideOrder.splice(draggedPosition, 1); + sorterSlideOrder.splice(targetPosition, 0, draggedIndex); + renderSorterGrid(); + } + + e.currentTarget.classList.remove("drag-over"); + } + + function handleSorterDragEnd(e) { + e.currentTarget.classList.remove("dragging"); + document.querySelectorAll(".sorter-slide").forEach((el) => { + el.classList.remove("drag-over"); + }); + draggedPosition = null; + } + + function sorterAddSlide() { + const newSlide = createSlide("blank"); + state.slides.push(newSlide); + sorterSlideOrder.push(state.slides.length - 1); + renderSorterGrid(); + } + + function sorterDuplicateSlide() { + if (sorterSelectedSlide === null) { + addChatMessage("assistant", "Select a slide to duplicate."); + return; + } + sorterDuplicateAt(sorterSelectedSlide); + } + + function sorterDuplicateAt(position) { + const originalIndex = sorterSlideOrder[position]; + const original = state.slides[originalIndex]; + if (!original) return; + + const duplicated = JSON.parse(JSON.stringify(original)); + duplicated.id = generateId(); + state.slides.push(duplicated); + sorterSlideOrder.splice(position + 1, 0, state.slides.length - 1); + renderSorterGrid(); + } + + function sorterDeleteSlide() { + if (sorterSelectedSlide === null) { + addChatMessage("assistant", "Select a slide to delete."); + return; + } + sorterDeleteAt(sorterSelectedSlide); + } + + function sorterDeleteAt(position) { + if (sorterSlideOrder.length <= 1) { + addChatMessage("assistant", "Cannot delete the last slide."); + return; + } + sorterSlideOrder.splice(position, 1); + if (sorterSelectedSlide >= sorterSlideOrder.length) { + sorterSelectedSlide = sorterSlideOrder.length - 1; + } + renderSorterGrid(); + } + + function applySorterChanges() { + const reorderedSlides = sorterSlideOrder.map((i) => state.slides[i]); + state.slides = reorderedSlides; + state.currentSlideIndex = 0; + + hideModal("slideSorterModal"); + renderThumbnails(); + renderCurrentSlide(); + updateSlideCounter(); + + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Slide order updated!"); + } + + function showExportPdfModal() { + showModal("exportPdfModal"); + } + + function exportToPdf() { + const rangeType = document.querySelector( + 'input[name="slideRange"]:checked', + )?.value; + const layout = document.getElementById("pdfLayout")?.value || "full"; + const orientation = + document.getElementById("pdfOrientation")?.value || "landscape"; + + let slidesToExport = []; + + switch (rangeType) { + case "all": + slidesToExport = state.slides.map((_, i) => i); + break; + case "current": + slidesToExport = [state.currentSlideIndex]; + break; + case "custom": + const customRange = document.getElementById("customRange")?.value || ""; + slidesToExport = parseSlideRange(customRange); + break; + default: + slidesToExport = state.slides.map((_, i) => i); + } + + if (slidesToExport.length === 0) { + addChatMessage("assistant", "No slides to export."); + return; + } + + const printWindow = window.open("", "_blank"); + const slidesPerPage = getLayoutSlidesPerPage(layout); + + let htmlContent = ` + + + + ${state.presentationName} - PDF Export + + + + `; + + let slideCount = 0; + slidesToExport.forEach((slideIndex, i) => { + const slide = state.slides[slideIndex]; + if (!slide) return; + + if (slideCount > 0 && slideCount % slidesPerPage === 0) { + htmlContent += '
'; + } + + if (slideCount % slidesPerPage === 0) { + htmlContent += '
'; + } + + const slideClass = + slidesPerPage === 1 + ? "slide-full" + : slidesPerPage === 2 + ? "slide-2" + : slidesPerPage === 4 + ? "slide-4" + : "slide-6"; + const bgColor = slide.background?.color || "#ffffff"; + + htmlContent += ` +
+
+ ${renderSlideContentForExport(slide)} +
+
Slide ${slideIndex + 1}
+ ${layout === "notes" && slide.notes ? `
${escapeHtml(slide.notes)}
` : ""} +
+ `; + + slideCount++; + if (slideCount % slidesPerPage === 0 || i === slidesToExport.length - 1) { + htmlContent += "
"; + } + }); + + htmlContent += ""; + + printWindow.document.write(htmlContent); + printWindow.document.close(); + printWindow.focus(); + + setTimeout(() => { + printWindow.print(); + }, 500); + + hideModal("exportPdfModal"); + addChatMessage( + "assistant", + `Exporting ${slidesToExport.length} slide(s) to PDF...`, + ); + } + + function parseSlideRange(rangeStr) { + const slides = []; + const parts = rangeStr.split(","); + + parts.forEach((part) => { + part = part.trim(); + if (part.includes("-")) { + const [start, end] = part.split("-").map((n) => parseInt(n.trim()) - 1); + for ( + let i = Math.max(0, start); + i <= Math.min(state.slides.length - 1, end); + i++ + ) { + if (!slides.includes(i)) slides.push(i); + } + } else { + const num = parseInt(part) - 1; + if (num >= 0 && num < state.slides.length && !slides.includes(num)) { + slides.push(num); + } + } + }); + + return slides.sort((a, b) => a - b); + } + + function getLayoutSlidesPerPage(layout) { + switch (layout) { + case "full": + case "notes": + return 1; + case "handout-2": + return 2; + case "handout-4": + return 4; + case "handout-6": + return 6; + default: + return 1; + } + } + + function renderSlideContentForExport(slide) { + let html = ""; + if (slide.elements) { + slide.elements.forEach((el) => { + if (el.element_type === "text" && el.content?.text) { + const fontSize = el.style?.fontSize || 16; + const fontWeight = el.style?.fontWeight || "normal"; + const color = el.style?.color || "#000"; + html += `
${escapeHtml(el.content.text)}
`; + } + }); + } + return html || "

Empty slide

"; + } + + let selectedMasterLayout = "title"; + + function showMasterSlideModal() { + showModal("masterSlideModal"); + selectedMasterLayout = "title"; + + if (state.theme) { + const colors = state.theme.colors || {}; + const fonts = state.theme.fonts || {}; + + setColorInput("masterPrimaryColor", colors.primary || "#4285f4"); + setColorInput("masterSecondaryColor", colors.secondary || "#34a853"); + setColorInput("masterAccentColor", colors.accent || "#fbbc04"); + setColorInput("masterBgColor", colors.background || "#ffffff"); + setColorInput("masterTextColor", colors.text || "#212121"); + setColorInput("masterTextLightColor", colors.text_light || "#666666"); + + setSelectValue("masterHeadingFont", fonts.heading || "Arial"); + setSelectValue("masterBodyFont", fonts.body || "Arial"); + } + + updateMasterPreview(); + updateMasterLayoutSelection(); + } + + function setColorInput(id, value) { + const el = document.getElementById(id); + if (el) el.value = value; + } + + function setSelectValue(id, value) { + const el = document.getElementById(id); + if (el) el.value = value; + } + + function selectMasterLayout(layout) { + selectedMasterLayout = layout; + updateMasterLayoutSelection(); + } + + function updateMasterLayoutSelection() { + document.querySelectorAll(".master-layout-item").forEach((item) => { + item.classList.toggle( + "active", + item.dataset.layout === selectedMasterLayout, + ); + }); + } + + function updateMasterPreview() { + const bgColor = + document.getElementById("masterBgColor")?.value || "#ffffff"; + const textColor = + document.getElementById("masterTextColor")?.value || "#212121"; + const textLightColor = + document.getElementById("masterTextLightColor")?.value || "#666666"; + const headingFont = + document.getElementById("masterHeadingFont")?.value || "Arial"; + const bodyFont = + document.getElementById("masterBodyFont")?.value || "Arial"; + + const previewSlide = document.querySelector(".preview-slide"); + const previewHeading = document.getElementById("previewHeading"); + const previewBody = document.getElementById("previewBody"); + + if (previewSlide) { + previewSlide.style.background = bgColor; + } + if (previewHeading) { + previewHeading.style.color = textColor; + previewHeading.style.fontFamily = headingFont; + } + if (previewBody) { + previewBody.style.color = textLightColor; + previewBody.style.fontFamily = bodyFont; + } + } + + function applyMasterSlide() { + const primaryColor = + document.getElementById("masterPrimaryColor")?.value || "#4285f4"; + const secondaryColor = + document.getElementById("masterSecondaryColor")?.value || "#34a853"; + const accentColor = + document.getElementById("masterAccentColor")?.value || "#fbbc04"; + const bgColor = + document.getElementById("masterBgColor")?.value || "#ffffff"; + const textColor = + document.getElementById("masterTextColor")?.value || "#212121"; + const textLightColor = + document.getElementById("masterTextLightColor")?.value || "#666666"; + const headingFont = + document.getElementById("masterHeadingFont")?.value || "Arial"; + const bodyFont = + document.getElementById("masterBodyFont")?.value || "Arial"; + + saveToHistory(); + + state.theme = { + name: "Custom", + colors: { + primary: primaryColor, + secondary: secondaryColor, + accent: accentColor, + background: bgColor, + text: textColor, + text_light: textLightColor, + }, + fonts: { + heading: headingFont, + body: bodyFont, + }, + }; + + state.slides.forEach((slide) => { + slide.background = slide.background || {}; + slide.background.color = bgColor; + + if (slide.elements) { + slide.elements.forEach((el) => { + if (el.element_type === "text") { + el.style = el.style || {}; + const isHeading = + el.style.fontSize >= 24 || el.style.fontWeight === "bold"; + el.style.fontFamily = isHeading ? headingFont : bodyFont; + el.style.color = isHeading ? textColor : textLightColor; + } + }); + } + }); + + hideModal("masterSlideModal"); + renderThumbnails(); + renderCurrentSlide(); + + state.isDirty = true; + scheduleAutoSave(); + addChatMessage("assistant", "Master slide theme applied to all slides!"); + } + + function resetMasterSlide() { + setColorInput("masterPrimaryColor", "#4285f4"); + setColorInput("masterSecondaryColor", "#34a853"); + setColorInput("masterAccentColor", "#fbbc04"); + setColorInput("masterBgColor", "#ffffff"); + setColorInput("masterTextColor", "#212121"); + setColorInput("masterTextLightColor", "#666666"); + setSelectValue("masterHeadingFont", "Arial"); + setSelectValue("masterBodyFont", "Arial"); + + updateMasterPreview(); + } + window.gbSlides = { init, addSlide, @@ -2023,6 +2914,11 @@ hideModal, toggleChatPanel, savePresentation, + showTransitionsModal, + showAnimationsModal, + showSlideSorter, + exportToPdf, + showMasterSlideModal, }; if (document.readyState === "loading") {