feat(suite): Enhanced UI for Sheet, Docs, and Slides editors

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-11 12:01:59 -03:00
parent 10299814b2
commit 76627ae9f0
9 changed files with 6475 additions and 27 deletions

View file

@ -1098,6 +1098,377 @@
background: rgba(66, 133, 244, 0.3); 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 { .editor-content ::-moz-selection {
background: rgba(66, 133, 244, 0.3); background: rgba(66, 133, 244, 0.3);
} }

View file

@ -12,6 +12,46 @@
/> />
</div> </div>
<div class="toolbar-center"> <div class="toolbar-center">
<div class="toolbar-group">
<button
class="btn-icon"
id="printPreviewBtn"
title="Print Preview"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
</button>
<button
class="btn-icon"
id="findReplaceBtn"
title="Find & Replace (Ctrl+H)"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
</div>
<span class="toolbar-divider"></span>
<div class="toolbar-group"> <div class="toolbar-group">
<button class="btn-icon" id="undoBtn" title="Undo (Ctrl+Z)"> <button class="btn-icon" id="undoBtn" title="Undo (Ctrl+Z)">
<svg <svg
@ -316,6 +356,40 @@
</div> </div>
<span class="toolbar-divider"></span> <span class="toolbar-divider"></span>
<div class="toolbar-group"> <div class="toolbar-group">
<button
class="btn-icon"
id="headerFooterBtn"
title="Edit Header & Footer"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<line x1="3" y1="7" x2="21" y2="7"></line>
<line x1="3" y1="17" x2="21" y2="17"></line>
</svg>
</button>
<button
class="btn-icon"
id="pageBreakBtn"
title="Insert Page Break"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M3 12h4l3-9 4 18 3-9h4"></path>
</svg>
</button>
<button class="btn-icon" id="linkBtn" title="Insert Link"> <button class="btn-icon" id="linkBtn" title="Insert Link">
<svg <svg
width="16" width="16"
@ -415,6 +489,12 @@
<div class="docs-main"> <div class="docs-main">
<div class="docs-canvas"> <div class="docs-canvas">
<div class="editor-page" id="editorPage"> <div class="editor-page" id="editorPage">
<div
class="editor-header"
id="editorHeader"
contenteditable="true"
data-placeholder="Click to add header"
></div>
<div <div
class="editor-content" class="editor-content"
id="editorContent" id="editorContent"
@ -422,6 +502,12 @@
spellcheck="true" spellcheck="true"
data-placeholder="Start typing..." data-placeholder="Start typing..."
></div> ></div>
<div
class="editor-footer"
id="editorFooter"
contenteditable="true"
data-placeholder="Click to add footer"
></div>
</div> </div>
</div> </div>
@ -731,4 +817,214 @@
</div> </div>
</div> </div>
<div class="modal hidden" id="findReplaceModal">
<div class="modal-content">
<div class="modal-header">
<h3>Find & Replace</h3>
<button class="btn-close" id="closeFindReplaceModal">×</button>
</div>
<div class="modal-body">
<div class="find-replace-group">
<label>Find:</label>
<input
type="text"
id="findInput"
placeholder="Search text..."
autofocus
/>
</div>
<div class="find-replace-group">
<label>Replace:</label>
<input
type="text"
id="replaceInput"
placeholder="Replace with..."
/>
</div>
<div class="find-replace-options">
<label class="checkbox-label">
<input type="checkbox" id="findMatchCase" />
Match case
</label>
<label class="checkbox-label">
<input type="checkbox" id="findWholeWord" />
Whole words only
</label>
</div>
<div class="find-results" id="findResults">
<span>0 matches found</span>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="findPrevBtn">Previous</button>
<button class="btn-secondary" id="findNextBtn">Next</button>
<button class="btn-primary" id="replaceBtn">Replace</button>
<button class="btn-primary" id="replaceAllBtn">
Replace All
</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="printPreviewModal">
<div class="modal-content modal-fullscreen">
<div class="modal-header">
<h3>Print Preview</h3>
<div class="print-toolbar">
<select id="printOrientation">
<option value="portrait">Portrait</option>
<option value="landscape">Landscape</option>
</select>
<select id="printPaperSize">
<option value="letter">Letter (8.5" x 11")</option>
<option value="a4">A4 (210mm x 297mm)</option>
<option value="legal">Legal (8.5" x 14")</option>
</select>
<label class="checkbox-label">
<input type="checkbox" id="printHeaders" />
Headers & Footers
</label>
</div>
<button class="btn-close" id="closePrintPreviewModal">×</button>
</div>
<div class="modal-body print-preview-body">
<div class="print-preview-container" id="printPreviewContainer">
<div class="print-page" id="printPage">
<div class="print-header" id="printHeader"></div>
<div class="print-content" id="printContent"></div>
<div class="print-footer" id="printFooter"></div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelPrintBtn">Cancel</button>
<button class="btn-primary" id="printBtn">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
Print
</button>
</div>
</div>
</div>
<div class="modal hidden" id="headerFooterModal">
<div class="modal-content">
<div class="modal-header">
<h3>Header & Footer</h3>
<button class="btn-close" id="closeHeaderFooterModal">×</button>
</div>
<div class="modal-body">
<div class="hf-tabs">
<button class="hf-tab active" data-tab="header">Header</button>
<button class="hf-tab" data-tab="footer">Footer</button>
</div>
<div class="hf-tab-content active" id="hfHeaderTab">
<div class="form-group">
<label>Header Content:</label>
<div
class="hf-editor"
id="headerEditor"
contenteditable="true"
data-placeholder="Enter header text..."
></div>
</div>
<div class="hf-options">
<label class="checkbox-label">
<input type="checkbox" id="showHeaderFirstPage" />
Different first page
</label>
<label class="checkbox-label">
<input type="checkbox" id="showHeaderOddEven" />
Different odd & even pages
</label>
</div>
<div class="hf-insert-options">
<label>Insert:</label>
<div class="hf-insert-btns">
<button class="btn-secondary btn-sm" id="insertPageNum">
Page Number
</button>
<button class="btn-secondary btn-sm" id="insertDate">
Date
</button>
<button
class="btn-secondary btn-sm"
id="insertDocTitle"
>
Document Title
</button>
</div>
</div>
</div>
<div class="hf-tab-content" id="hfFooterTab">
<div class="form-group">
<label>Footer Content:</label>
<div
class="hf-editor"
id="footerEditor"
contenteditable="true"
data-placeholder="Enter footer text..."
></div>
</div>
<div class="hf-options">
<label class="checkbox-label">
<input type="checkbox" id="showFooterFirstPage" />
Different first page
</label>
<label class="checkbox-label">
<input type="checkbox" id="showFooterOddEven" />
Different odd & even pages
</label>
</div>
<div class="hf-insert-options">
<label>Insert:</label>
<div class="hf-insert-btns">
<button
class="btn-secondary btn-sm"
id="insertFooterPageNum"
>
Page Number
</button>
<button
class="btn-secondary btn-sm"
id="insertFooterDate"
>
Date
</button>
<button
class="btn-secondary btn-sm"
id="insertFooterDocTitle"
>
Document Title
</button>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="removeHeaderFooterBtn">
Remove All
</button>
<button class="btn-secondary" id="cancelHeaderFooterBtn">
Cancel
</button>
<button class="btn-primary" id="applyHeaderFooterBtn">
Apply
</button>
</div>
</div>
</div>
</div>
<script src="docs/docs.js"></script> <script src="docs/docs.js"></script>

View file

@ -20,6 +20,8 @@
chatPanelOpen: true, chatPanelOpen: true,
driveSource: null, driveSource: null,
zoom: 100, zoom: 100,
findMatches: [],
findMatchIndex: -1,
}; };
const elements = {}; const elements = {};
@ -54,6 +56,11 @@
elements.imageModal = document.getElementById("imageModal"); elements.imageModal = document.getElementById("imageModal");
elements.tableModal = document.getElementById("tableModal"); elements.tableModal = document.getElementById("tableModal");
elements.exportModal = document.getElementById("exportModal"); 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() { function bindEvents() {
@ -207,6 +214,94 @@
btn.addEventListener("click", () => exportDocument(btn.dataset.format)); 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); window.addEventListener("beforeunload", handleBeforeUnload);
} }
@ -1007,6 +1102,421 @@ ${content}
return div.innerHTML; 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,
'<mark class="find-highlight">$1</mark>',
);
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(`
<!DOCTYPE html>
<html>
<head>
<title>${state.docTitle}</title>
<style>
@page { size: ${orientation}; margin: 1in; }
body {
font-family: Arial, sans-serif;
font-size: 12pt;
line-height: 1.6;
color: #000;
}
h1 { font-size: 24pt; margin-bottom: 12pt; }
h2 { font-size: 18pt; margin-bottom: 10pt; }
h3 { font-size: 14pt; margin-bottom: 8pt; }
p { margin-bottom: 12pt; }
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
td, th { border: 1px solid #ccc; padding: 8px; }
.page-break { page-break-after: always; }
${showHeaders ? `.header { text-align: center; font-size: 10pt; color: #666; margin-bottom: 24pt; }` : ""}
</style>
</head>
<body>
${showHeaders ? `<div class="header">${state.docTitle}</div>` : ""}
${content}
</body>
</html>
`);
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 = "<br>";
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 =
'<span class="hf-field" data-field="pageNum">[Page #]</span>';
break;
case "date":
fieldContent = `<span class="hf-field" data-field="date">${new Date().toLocaleDateString()}</span>`;
break;
case "title":
fieldContent = `<span class="hf-field" data-field="title">${state.docTitle}</span>`;
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() { function createNewDocument() {
state.docId = null; state.docId = null;
state.docTitle = "Untitled Document"; state.docTitle = "Untitled Document";

View file

@ -1329,3 +1329,749 @@
border-color: #ccc !important; 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;
}

View file

@ -45,6 +45,46 @@
</button> </button>
</div> </div>
<span class="toolbar-divider"></span> <span class="toolbar-divider"></span>
<div class="toolbar-group">
<button
class="btn-icon"
id="printPreviewBtn"
title="Print Preview"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
</button>
<button
class="btn-icon"
id="findReplaceBtn"
title="Find & Replace (Ctrl+H)"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
</div>
<span class="toolbar-divider"></span>
<div class="toolbar-group"> <div class="toolbar-group">
<select <select
class="toolbar-select font-family" class="toolbar-select font-family"
@ -197,6 +237,66 @@
</button> </button>
</div> </div>
<span class="toolbar-divider"></span> <span class="toolbar-divider"></span>
<div class="toolbar-group">
<select
class="toolbar-select number-format"
id="numberFormat"
title="Number Format"
>
<option value="general">General</option>
<option value="number">Number (1,234.56)</option>
<option value="currency">Currency ($1,234.56)</option>
<option value="accounting">Accounting</option>
<option value="percent">Percent (12.34%)</option>
<option value="scientific">Scientific (1.23E+03)</option>
<option value="date_short">Date (1/15/2024)</option>
<option value="date_long">Date (January 15, 2024)</option>
<option value="time">Time (3:45 PM)</option>
<option value="datetime">Date & Time</option>
<option value="fraction">Fraction (1/4)</option>
<option value="text">Text</option>
<option value="custom">Custom...</option>
</select>
<button
class="btn-icon"
id="decreaseDecimalBtn"
title="Decrease Decimal"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<text x="2" y="16" font-size="10" fill="currentColor">
.0
</text>
<path d="M18 8l-4 4 4 4"></path>
</svg>
</button>
<button
class="btn-icon"
id="increaseDecimalBtn"
title="Increase Decimal"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<text x="2" y="16" font-size="10" fill="currentColor">
.00
</text>
<path d="M14 8l4 4-4 4"></path>
</svg>
</button>
</div>
<span class="toolbar-divider"></span>
<div class="toolbar-group"> <div class="toolbar-group">
<button class="btn-icon" id="mergeCellsBtn" title="Merge Cells"> <button class="btn-icon" id="mergeCellsBtn" title="Merge Cells">
<svg <svg
@ -216,17 +316,151 @@
</button> </button>
<button <button
class="btn-icon" class="btn-icon"
id="formatCurrencyBtn" id="conditionalFormatBtn"
title="Format as Currency" title="Conditional Formatting"
> >
<span>$</span> <svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="7"
height="7"
fill="#4CAF50"
></rect>
<rect
x="14"
y="3"
width="7"
height="7"
fill="#FFC107"
></rect>
<rect
x="3"
y="14"
width="7"
height="7"
fill="#F44336"
></rect>
<rect
x="14"
y="14"
width="7"
height="7"
fill="#2196F3"
></rect>
</svg>
</button> </button>
<button <button
class="btn-icon" class="btn-icon"
id="formatPercentBtn" id="dataValidationBtn"
title="Format as Percent" title="Data Validation"
> >
<span>%</span> <svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 11l3 3L22 4"></path>
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
></path>
</svg>
</button>
</div>
<span class="toolbar-divider"></span>
<div class="toolbar-group">
<button
class="btn-icon"
id="insertChartBtn"
title="Insert Chart"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="12" width="4" height="9"></rect>
<rect x="10" y="6" width="4" height="15"></rect>
<rect x="17" y="3" width="4" height="18"></rect>
</svg>
</button>
<button
class="btn-icon"
id="insertImageBtn"
title="Insert Image"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
ry="2"
></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</button>
<button class="btn-icon" id="filterBtn" title="Filter">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polygon
points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"
></polygon>
</svg>
</button>
<button class="btn-icon" id="sortAscBtn" title="Sort A→Z">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 5h10M11 9h7M11 13h4"></path>
<path d="M3 17l3 3 3-3M6 18V4"></path>
</svg>
</button>
<button class="btn-icon" id="sortDescBtn" title="Sort Z→A">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 5h4M11 9h7M11 13h10"></path>
<path d="M3 7l3-3 3 3M6 6v12"></path>
</svg>
</button> </button>
</div> </div>
</div> </div>
@ -297,6 +531,14 @@
class="cursor-indicators" class="cursor-indicators"
id="cursorIndicators" id="cursorIndicators"
></div> ></div>
<div
class="charts-container"
id="chartsContainer"
></div>
<div
class="images-container"
id="imagesContainer"
></div>
</div> </div>
</div> </div>
</div> </div>
@ -598,7 +840,7 @@
<button class="btn-secondary" id="cancelChartBtn"> <button class="btn-secondary" id="cancelChartBtn">
Cancel Cancel
</button> </button>
<button class="btn-primary" id="insertChartBtn"> <button class="btn-primary" id="insertChartBtnConfirm">
Insert Chart Insert Chart
</button> </button>
</div> </div>
@ -606,4 +848,464 @@
</div> </div>
</div> </div>
<div class="modal hidden" id="findReplaceModal">
<div class="modal-content">
<div class="modal-header">
<h3>Find & Replace</h3>
<button class="btn-close" id="closeFindReplaceModal">×</button>
</div>
<div class="modal-body">
<div class="find-replace-group">
<label>Find:</label>
<input
type="text"
id="findInput"
placeholder="Search text..."
autofocus
/>
</div>
<div class="find-replace-group">
<label>Replace:</label>
<input
type="text"
id="replaceInput"
placeholder="Replace with..."
/>
</div>
<div class="find-replace-options">
<label class="checkbox-label">
<input type="checkbox" id="findMatchCase" />
Match case
</label>
<label class="checkbox-label">
<input type="checkbox" id="findWholeCell" />
Match entire cell contents
</label>
<label class="checkbox-label">
<input type="checkbox" id="findRegex" />
Use regular expressions
</label>
</div>
<div class="find-results" id="findResults">
<span>0 matches found</span>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="findPrevBtn">Previous</button>
<button class="btn-secondary" id="findNextBtn">Next</button>
<button class="btn-primary" id="replaceBtn">Replace</button>
<button class="btn-primary" id="replaceAllBtn">
Replace All
</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="conditionalFormatModal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>Conditional Formatting</h3>
<button class="btn-close" id="closeConditionalFormatModal">
×
</button>
</div>
<div class="modal-body">
<div class="cf-section">
<label>Apply to range:</label>
<input type="text" id="cfRange" placeholder="e.g., A1:D10" />
</div>
<div class="cf-section">
<label>Format cells if:</label>
<select id="cfRuleType">
<option value="greater_than">Greater than</option>
<option value="less_than">Less than</option>
<option value="equal_to">Equal to</option>
<option value="between">Between</option>
<option value="text_contains">Text contains</option>
<option value="text_starts">Text starts with</option>
<option value="text_ends">Text ends with</option>
<option value="duplicate">Duplicate values</option>
<option value="unique">Unique values</option>
<option value="blank">Is blank</option>
<option value="not_blank">Is not blank</option>
<option value="top_n">Top N values</option>
<option value="bottom_n">Bottom N values</option>
<option value="above_average">Above average</option>
<option value="below_average">Below average</option>
<option value="color_scale">Color scale</option>
<option value="data_bar">Data bars</option>
<option value="icon_set">Icon set</option>
</select>
</div>
<div class="cf-section cf-values" id="cfValuesSection">
<input type="text" id="cfValue1" placeholder="Value" />
<input
type="text"
id="cfValue2"
placeholder="and"
class="hidden"
/>
</div>
<div class="cf-section">
<label>Formatting style:</label>
<div class="cf-style-options">
<div class="cf-style-row">
<label>Background:</label>
<input type="color" id="cfBgColor" value="#ffeb3b" />
</div>
<div class="cf-style-row">
<label>Text color:</label>
<input type="color" id="cfTextColor" value="#000000" />
</div>
<div class="cf-style-row">
<label>Bold:</label>
<input type="checkbox" id="cfBold" />
</div>
<div class="cf-style-row">
<label>Italic:</label>
<input type="checkbox" id="cfItalic" />
</div>
</div>
</div>
<div class="cf-preview">
<label>Preview:</label>
<div class="cf-preview-cell" id="cfPreviewCell">Sample</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelCfBtn">Cancel</button>
<button class="btn-primary" id="applyCfBtn">Apply</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="dataValidationModal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>Data Validation</h3>
<button class="btn-close" id="closeDataValidationModal">×</button>
</div>
<div class="modal-body">
<div class="dv-tabs">
<button class="dv-tab active" data-tab="settings">
Settings
</button>
<button class="dv-tab" data-tab="input">Input Message</button>
<button class="dv-tab" data-tab="error">Error Alert</button>
</div>
<div class="dv-tab-content active" id="dvSettingsTab">
<div class="dv-section">
<label>Apply to range:</label>
<input
type="text"
id="dvRange"
placeholder="e.g., A1:A100"
/>
</div>
<div class="dv-section">
<label>Allow:</label>
<select id="dvType">
<option value="any">Any value</option>
<option value="whole_number">Whole number</option>
<option value="decimal">Decimal</option>
<option value="list">List</option>
<option value="date">Date</option>
<option value="time">Time</option>
<option value="text_length">Text length</option>
<option value="custom">Custom formula</option>
</select>
</div>
<div class="dv-section dv-criteria" id="dvCriteriaSection">
<label>Data:</label>
<select id="dvOperator">
<option value="between">between</option>
<option value="not_between">not between</option>
<option value="equal">equal to</option>
<option value="not_equal">not equal to</option>
<option value="greater">greater than</option>
<option value="less">less than</option>
<option value="greater_equal">
greater than or equal to
</option>
<option value="less_equal">
less than or equal to
</option>
</select>
</div>
<div class="dv-section dv-values" id="dvValuesSection">
<div class="dv-value-row">
<label id="dvValue1Label">Minimum:</label>
<input type="text" id="dvValue1" />
</div>
<div class="dv-value-row" id="dvValue2Row">
<label>Maximum:</label>
<input type="text" id="dvValue2" />
</div>
</div>
<div class="dv-section dv-list hidden" id="dvListSection">
<label>Source (comma-separated):</label>
<textarea
id="dvListSource"
placeholder="Option 1, Option 2, Option 3"
></textarea>
</div>
<div class="dv-section">
<label class="checkbox-label">
<input type="checkbox" id="dvIgnoreBlank" checked />
Ignore blank
</label>
<label class="checkbox-label">
<input type="checkbox" id="dvShowDropdown" checked />
In-cell dropdown
</label>
</div>
</div>
<div class="dv-tab-content" id="dvInputTab">
<div class="dv-section">
<label class="checkbox-label">
<input type="checkbox" id="dvShowInput" checked />
Show input message when cell is selected
</label>
</div>
<div class="dv-section">
<label>Title:</label>
<input
type="text"
id="dvInputTitle"
placeholder="Input title"
/>
</div>
<div class="dv-section">
<label>Input message:</label>
<textarea
id="dvInputMessage"
placeholder="Enter instructions for the user"
></textarea>
</div>
</div>
<div class="dv-tab-content" id="dvErrorTab">
<div class="dv-section">
<label class="checkbox-label">
<input type="checkbox" id="dvShowError" checked />
Show error alert after invalid data is entered
</label>
</div>
<div class="dv-section">
<label>Style:</label>
<select id="dvErrorStyle">
<option value="stop">Stop</option>
<option value="warning">Warning</option>
<option value="information">Information</option>
</select>
</div>
<div class="dv-section">
<label>Title:</label>
<input
type="text"
id="dvErrorTitle"
placeholder="Error title"
/>
</div>
<div class="dv-section">
<label>Error message:</label>
<textarea
id="dvErrorMessage"
placeholder="Enter error message"
></textarea>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="clearDvBtn">Clear All</button>
<button class="btn-secondary" id="cancelDvBtn">Cancel</button>
<button class="btn-primary" id="applyDvBtn">OK</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="printPreviewModal">
<div class="modal-content modal-fullscreen">
<div class="modal-header">
<h3>Print Preview</h3>
<div class="print-toolbar">
<select id="printOrientation">
<option value="portrait">Portrait</option>
<option value="landscape">Landscape</option>
</select>
<select id="printPaperSize">
<option value="letter">Letter (8.5" x 11")</option>
<option value="a4">A4 (210mm x 297mm)</option>
<option value="legal">Legal (8.5" x 14")</option>
<option value="tabloid">Tabloid (11" x 17")</option>
</select>
<select id="printScale">
<option value="100">100%</option>
<option value="fit_width">Fit to width</option>
<option value="fit_page">Fit to page</option>
<option value="75">75%</option>
<option value="50">50%</option>
</select>
<label class="checkbox-label">
<input type="checkbox" id="printGridlines" />
Gridlines
</label>
<label class="checkbox-label">
<input type="checkbox" id="printHeaders" />
Row/Column headers
</label>
</div>
<button class="btn-close" id="closePrintPreviewModal">×</button>
</div>
<div class="modal-body print-preview-body">
<div class="print-preview-container" id="printPreviewContainer">
<div class="print-page" id="printPage">
<div class="print-content" id="printContent"></div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelPrintBtn">Cancel</button>
<button class="btn-primary" id="printBtn">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
Print
</button>
</div>
</div>
</div>
<div class="modal hidden" id="customNumberFormatModal">
<div class="modal-content">
<div class="modal-header">
<h3>Custom Number Format</h3>
<button class="btn-close" id="closeCustomFormatModal">×</button>
</div>
<div class="modal-body">
<div class="cnf-section">
<label>Category:</label>
<select id="cnfCategory">
<option value="number">Number</option>
<option value="currency">Currency</option>
<option value="date">Date</option>
<option value="time">Time</option>
<option value="percentage">Percentage</option>
<option value="fraction">Fraction</option>
<option value="scientific">Scientific</option>
<option value="text">Text</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="cnf-section">
<label>Format code:</label>
<input type="text" id="cnfFormatCode" placeholder="#,##0.00" />
</div>
<div class="cnf-section">
<label>Preview:</label>
<div class="cnf-preview" id="cnfPreview">1,234.56</div>
</div>
<div class="cnf-section">
<label>Common formats:</label>
<div class="cnf-formats-list" id="cnfFormatsList">
<div class="cnf-format-item" data-format="#,##0">1,235</div>
<div class="cnf-format-item" data-format="#,##0.00">
1,234.56
</div>
<div class="cnf-format-item" data-format="$#,##0.00">
$1,234.56
</div>
<div class="cnf-format-item" data-format="0%">12%</div>
<div class="cnf-format-item" data-format="0.00%">
12.34%
</div>
<div class="cnf-format-item" data-format="0.00E+00">
1.23E+03
</div>
<div class="cnf-format-item" data-format="MM/DD/YYYY">
01/15/2024
</div>
<div class="cnf-format-item" data-format="MMMM D, YYYY">
January 15, 2024
</div>
<div class="cnf-format-item" data-format="HH:MM:SS">
14:30:00
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelCnfBtn">Cancel</button>
<button class="btn-primary" id="applyCnfBtn">Apply</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="insertImageModal">
<div class="modal-content">
<div class="modal-header">
<h3>Insert Image</h3>
<button class="btn-close" id="closeInsertImageModal">×</button>
</div>
<div class="modal-body">
<div class="img-tabs">
<button class="img-tab active" data-tab="url">From URL</button>
<button class="img-tab" data-tab="upload">Upload</button>
</div>
<div class="img-tab-content active" id="imgUrlTab">
<div class="img-section">
<label>Image URL:</label>
<input type="text" id="imgUrl" placeholder="https://..." />
</div>
</div>
<div class="img-tab-content" id="imgUploadTab">
<div class="img-section">
<label>Select file:</label>
<input type="file" id="imgFile" accept="image/*" />
</div>
<div class="img-drop-zone" id="imgDropZone">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
ry="2"
></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
<p>Drag and drop an image here</p>
</div>
</div>
<div class="img-preview-container hidden" id="imgPreviewContainer">
<label>Preview:</label>
<img id="imgPreview" src="" alt="Preview" />
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelImgBtn">Cancel</button>
<button class="btn-primary" id="insertImgBtn">Insert</button>
</div>
</div>
</div>
</div>
<script src="sheet/sheet.js"></script> <script src="sheet/sheet.js"></script>

File diff suppressed because it is too large Load diff

View file

@ -118,7 +118,9 @@
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
color: var(--sentient-text-secondary, #666666); color: var(--sentient-text-secondary, #666666);
transition: background 0.15s, color 0.15s; transition:
background 0.15s,
color 0.15s;
} }
.btn-icon:hover { .btn-icon:hover {
@ -302,7 +304,9 @@
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 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 { .slide-thumbnail:hover {
@ -349,7 +353,10 @@
color: var(--sentient-text-secondary, #666666); color: var(--sentient-text-secondary, #666666);
font-size: 13px; font-size: 13px;
cursor: pointer; 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 { .btn-add-slide:hover {
@ -453,14 +460,50 @@
cursor: pointer; cursor: pointer;
} }
.handle.nw { top: -5px; left: -5px; cursor: nwse-resize; } .handle.nw {
.handle.n { top: -5px; left: 50%; transform: translateX(-50%); cursor: ns-resize; } top: -5px;
.handle.ne { top: -5px; right: -5px; cursor: nesw-resize; } left: -5px;
.handle.w { top: 50%; left: -5px; transform: translateY(-50%); cursor: ew-resize; } cursor: nwse-resize;
.handle.e { top: 50%; right: -5px; transform: translateY(-50%); cursor: ew-resize; } }
.handle.sw { bottom: -5px; left: -5px; cursor: nesw-resize; } .handle.n {
.handle.s { bottom: -5px; left: 50%; transform: translateX(-50%); cursor: ns-resize; } top: -5px;
.handle.se { bottom: -5px; right: -5px; cursor: nwse-resize; } 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 { .rotate-handle {
position: absolute; position: absolute;
@ -497,7 +540,9 @@
position: absolute; position: absolute;
width: 16px; width: 16px;
height: 16px; height: 16px;
transition: left 0.1s, top 0.1s; transition:
left 0.1s,
top 0.1s;
} }
.cursor-indicator::after { .cursor-indicator::after {
@ -524,7 +569,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 0;
transition: width 0.2s ease, opacity 0.2s ease; transition:
width 0.2s ease,
opacity 0.2s ease;
} }
.chat-panel.collapsed { .chat-panel.collapsed {
@ -663,7 +710,10 @@
font-size: 12px; font-size: 12px;
color: var(--sentient-text-secondary, #666666); color: var(--sentient-text-secondary, #666666);
cursor: pointer; 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 { .suggestion-btn:hover {
@ -932,7 +982,10 @@
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
color: var(--sentient-text-secondary, #666666); 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 { .shape-btn:hover {
@ -1034,3 +1087,731 @@
background: transparent; background: transparent;
border-radius: 8px; border-radius: 8px;
cursor: pointer; 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;
}
}

View file

@ -111,6 +111,46 @@
</button> </button>
</div> </div>
<span class="toolbar-divider"></span> <span class="toolbar-divider"></span>
<div class="toolbar-group">
<button
class="btn-icon"
id="transitionsBtn"
title="Slide Transitions"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="2" y="3" width="8" height="14" rx="1"></rect>
<rect x="14" y="7" width="8" height="14" rx="1"></rect>
<path d="M10 10l4 4m0-4l-4 4"></path>
</svg>
</button>
<button class="btn-icon" id="animationsBtn" title="Animations">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 2v4m0 12v4m10-10h-4M6 12H2"></path>
<path
d="M19.07 4.93l-2.83 2.83m-8.48 8.48l-2.83 2.83"
></path>
<path
d="M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83"
></path>
</svg>
</button>
</div>
<span class="toolbar-divider"></span>
<div class="toolbar-group"> <div class="toolbar-group">
<select <select
class="toolbar-select font-family" class="toolbar-select font-family"
@ -250,6 +290,63 @@
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<div class="collaborators" id="collaborators"></div> <div class="collaborators" id="collaborators"></div>
<button
class="btn-icon"
id="masterSlideBtn"
title="Edit Master Slide"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="14" rx="2"></rect>
<path d="M3 7h18"></path>
<circle cx="7" cy="5" r="1" fill="currentColor"></circle>
<circle cx="10" cy="5" r="1" fill="currentColor"></circle>
</svg>
</button>
<button
class="btn-icon"
id="slideSorterBtn"
title="Slide Sorter View"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="5" rx="1"></rect>
<rect x="14" y="3" width="7" height="5" rx="1"></rect>
<rect x="3" y="10" width="7" height="5" rx="1"></rect>
<rect x="14" y="10" width="7" height="5" rx="1"></rect>
<rect x="3" y="17" width="7" height="5" rx="1"></rect>
<rect x="14" y="17" width="7" height="5" rx="1"></rect>
</svg>
</button>
<button class="btn-icon" id="exportPdfBtn" title="Export to PDF">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
></path>
<polyline points="14 2 14 8 20 8"></polyline>
<path d="M9 15h6"></path>
<path d="M12 12v6"></path>
</svg>
</button>
<button class="btn-icon" id="presentBtn" title="Present (F5)"> <button class="btn-icon" id="presentBtn" title="Present (F5)">
<svg <svg
width="18" width="18"
@ -805,4 +902,524 @@
</div> </div>
</div> </div>
<div class="modal hidden" id="transitionsModal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>Slide Transitions</h3>
<button class="btn-close" id="closeTransitionsModal">×</button>
</div>
<div class="modal-body">
<div class="transitions-section">
<label>Transition Effect:</label>
<div class="transitions-grid">
<button
class="transition-btn active"
data-transition="none"
title="None"
>
<div class="transition-preview none"></div>
<span>None</span>
</button>
<button
class="transition-btn"
data-transition="fade"
title="Fade"
>
<div class="transition-preview fade"></div>
<span>Fade</span>
</button>
<button
class="transition-btn"
data-transition="slide-left"
title="Slide Left"
>
<div class="transition-preview slide-left"></div>
<span>Slide Left</span>
</button>
<button
class="transition-btn"
data-transition="slide-right"
title="Slide Right"
>
<div class="transition-preview slide-right"></div>
<span>Slide Right</span>
</button>
<button
class="transition-btn"
data-transition="slide-up"
title="Slide Up"
>
<div class="transition-preview slide-up"></div>
<span>Slide Up</span>
</button>
<button
class="transition-btn"
data-transition="slide-down"
title="Slide Down"
>
<div class="transition-preview slide-down"></div>
<span>Slide Down</span>
</button>
<button
class="transition-btn"
data-transition="zoom-in"
title="Zoom In"
>
<div class="transition-preview zoom-in"></div>
<span>Zoom In</span>
</button>
<button
class="transition-btn"
data-transition="zoom-out"
title="Zoom Out"
>
<div class="transition-preview zoom-out"></div>
<span>Zoom Out</span>
</button>
<button
class="transition-btn"
data-transition="flip"
title="Flip"
>
<div class="transition-preview flip"></div>
<span>Flip</span>
</button>
<button
class="transition-btn"
data-transition="cube"
title="Cube"
>
<div class="transition-preview cube"></div>
<span>Cube</span>
</button>
</div>
</div>
<div class="transitions-section">
<label>Duration:</label>
<div class="duration-control">
<input
type="range"
id="transitionDuration"
min="0.1"
max="3"
step="0.1"
value="0.5"
/>
<span id="durationValue">0.5s</span>
</div>
</div>
<div class="transitions-section">
<label class="checkbox-label">
<input type="checkbox" id="applyToAllSlides" />
Apply to all slides
</label>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelTransitionsBtn">
Cancel
</button>
<button class="btn-primary" id="applyTransitionsBtn">
Apply
</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="animationsModal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>Animations</h3>
<button class="btn-close" id="closeAnimationsModal">×</button>
</div>
<div class="modal-body">
<div class="animations-section">
<label>Select element on slide to animate</label>
<div class="selected-element-info" id="selectedElementInfo">
No element selected
</div>
</div>
<div class="animations-section">
<label>Entrance Animation:</label>
<select id="entranceAnimation">
<option value="none">None</option>
<option value="fade-in">Fade In</option>
<option value="fly-in-left">Fly In (Left)</option>
<option value="fly-in-right">Fly In (Right)</option>
<option value="fly-in-top">Fly In (Top)</option>
<option value="fly-in-bottom">Fly In (Bottom)</option>
<option value="zoom-in">Zoom In</option>
<option value="bounce-in">Bounce In</option>
<option value="spin-in">Spin In</option>
<option value="wipe-left">Wipe (Left)</option>
<option value="wipe-right">Wipe (Right)</option>
</select>
</div>
<div class="animations-section">
<label>Emphasis Animation:</label>
<select id="emphasisAnimation">
<option value="none">None</option>
<option value="pulse">Pulse</option>
<option value="shake">Shake</option>
<option value="bounce">Bounce</option>
<option value="spin">Spin</option>
<option value="grow">Grow/Shrink</option>
<option value="flash">Flash</option>
</select>
</div>
<div class="animations-section">
<label>Exit Animation:</label>
<select id="exitAnimation">
<option value="none">None</option>
<option value="fade-out">Fade Out</option>
<option value="fly-out-left">Fly Out (Left)</option>
<option value="fly-out-right">Fly Out (Right)</option>
<option value="fly-out-top">Fly Out (Top)</option>
<option value="fly-out-bottom">Fly Out (Bottom)</option>
<option value="zoom-out">Zoom Out</option>
<option value="spin-out">Spin Out</option>
</select>
</div>
<div class="animations-section">
<div class="animation-timing">
<div class="timing-group">
<label>Start:</label>
<select id="animationStart">
<option value="on-click">On Click</option>
<option value="with-previous">With Previous</option>
<option value="after-previous">
After Previous
</option>
</select>
</div>
<div class="timing-group">
<label>Duration:</label>
<input
type="number"
id="animationDuration"
value="0.5"
min="0.1"
max="5"
step="0.1"
/>
<span>sec</span>
</div>
<div class="timing-group">
<label>Delay:</label>
<input
type="number"
id="animationDelay"
value="0"
min="0"
max="10"
step="0.1"
/>
<span>sec</span>
</div>
</div>
</div>
<div class="animations-section">
<label>Animation Order:</label>
<div class="animation-order-list" id="animationOrderList">
<p class="no-animations">No animations added yet</p>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="previewAnimationBtn">
Preview
</button>
<button class="btn-secondary" id="cancelAnimationsBtn">
Cancel
</button>
<button class="btn-primary" id="applyAnimationsBtn">
Apply
</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="slideSorterModal">
<div class="modal-content modal-fullscreen">
<div class="modal-header">
<h3>Slide Sorter</h3>
<div class="sorter-toolbar">
<button class="btn-secondary" id="sorterAddSlide">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Slide
</button>
<button class="btn-secondary" id="sorterDuplicateSlide">
Duplicate
</button>
<button class="btn-secondary" id="sorterDeleteSlide">
Delete
</button>
</div>
<button class="btn-close" id="closeSlideSorterModal">×</button>
</div>
<div class="modal-body sorter-body">
<div class="sorter-grid" id="sorterGrid"></div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelSorterBtn">Cancel</button>
<button class="btn-primary" id="applySorterBtn">Apply</button>
</div>
</div>
</div>
<div class="modal hidden" id="exportPdfModal">
<div class="modal-content">
<div class="modal-header">
<h3>Export to PDF</h3>
<button class="btn-close" id="closeExportPdfModal">×</button>
</div>
<div class="modal-body">
<div class="export-section">
<label>Slide Range:</label>
<div class="export-range-options">
<label class="radio-label">
<input
type="radio"
name="slideRange"
value="all"
checked
/>
All slides
</label>
<label class="radio-label">
<input type="radio" name="slideRange" value="current" />
Current slide
</label>
<label class="radio-label">
<input type="radio" name="slideRange" value="custom" />
Custom range:
<input
type="text"
id="customRange"
placeholder="e.g., 1-5, 8"
class="range-input"
/>
</label>
</div>
</div>
<div class="export-section">
<label>Layout:</label>
<select id="pdfLayout">
<option value="full">Full page slides</option>
<option value="notes">Slides with notes</option>
<option value="handout-2">Handouts (2 per page)</option>
<option value="handout-4">Handouts (4 per page)</option>
<option value="handout-6">Handouts (6 per page)</option>
<option value="outline">Outline only</option>
</select>
</div>
<div class="export-section">
<label>Orientation:</label>
<select id="pdfOrientation">
<option value="landscape">Landscape</option>
<option value="portrait">Portrait</option>
</select>
</div>
<div class="export-section">
<label class="checkbox-label">
<input type="checkbox" id="pdfIncludeHidden" />
Include hidden slides
</label>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="cancelExportPdfBtn">
Cancel
</button>
<button class="btn-primary" id="exportPdfBtnConfirm">
Export PDF
</button>
</div>
</div>
</div>
</div>
<div class="modal hidden" id="masterSlideModal">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>Edit Master Slide</h3>
<button class="btn-close" id="closeMasterSlideModal">×</button>
</div>
<div class="modal-body">
<div class="master-layout">
<div class="master-sidebar">
<h4>Master Layouts</h4>
<div class="master-layout-list" id="masterLayoutList">
<div
class="master-layout-item active"
data-layout="title"
>
<div class="layout-preview title-preview">
<div class="preview-title"></div>
<div class="preview-subtitle"></div>
</div>
<span>Title Slide</span>
</div>
<div
class="master-layout-item"
data-layout="title_content"
>
<div class="layout-preview title-content-preview">
<div class="preview-title small"></div>
<div class="preview-content"></div>
</div>
<span>Title and Content</span>
</div>
<div
class="master-layout-item"
data-layout="two_content"
>
<div class="layout-preview two-content-preview">
<div class="preview-title small"></div>
<div class="preview-columns">
<div class="preview-col"></div>
<div class="preview-col"></div>
</div>
</div>
<span>Two Content</span>
</div>
<div class="master-layout-item" data-layout="section">
<div class="layout-preview section-preview">
<div class="preview-section-title"></div>
</div>
<span>Section Header</span>
</div>
<div class="master-layout-item" data-layout="blank">
<div class="layout-preview blank-preview"></div>
<span>Blank</span>
</div>
</div>
</div>
<div class="master-editor">
<div class="master-section">
<h4>Theme Colors</h4>
<div class="color-grid">
<div class="color-item">
<label>Primary:</label>
<input
type="color"
id="masterPrimaryColor"
value="#4285f4"
/>
</div>
<div class="color-item">
<label>Secondary:</label>
<input
type="color"
id="masterSecondaryColor"
value="#34a853"
/>
</div>
<div class="color-item">
<label>Accent:</label>
<input
type="color"
id="masterAccentColor"
value="#fbbc04"
/>
</div>
<div class="color-item">
<label>Background:</label>
<input
type="color"
id="masterBgColor"
value="#ffffff"
/>
</div>
<div class="color-item">
<label>Text:</label>
<input
type="color"
id="masterTextColor"
value="#212121"
/>
</div>
<div class="color-item">
<label>Text Light:</label>
<input
type="color"
id="masterTextLightColor"
value="#666666"
/>
</div>
</div>
</div>
<div class="master-section">
<h4>Theme Fonts</h4>
<div class="font-grid">
<div class="font-item">
<label>Heading Font:</label>
<select id="masterHeadingFont">
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">
Times New Roman
</option>
<option value="Verdana">Verdana</option>
<option value="Trebuchet MS">
Trebuchet MS
</option>
<option value="Impact">Impact</option>
</select>
</div>
<div class="font-item">
<label>Body Font:</label>
<select id="masterBodyFont">
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">
Times New Roman
</option>
<option value="Verdana">Verdana</option>
<option value="Calibri">Calibri</option>
</select>
</div>
</div>
</div>
<div class="master-section">
<h4>Preview</h4>
<div class="master-preview" id="masterPreview">
<div class="preview-slide">
<h2 class="preview-heading" id="previewHeading">
Title Text
</h2>
<p class="preview-body" id="previewBody">
Body text example
</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-secondary" id="resetMasterBtn">
Reset to Default
</button>
<button class="btn-secondary" id="cancelMasterBtn">
Cancel
</button>
<button class="btn-primary" id="applyMasterBtn">
Apply to All Slides
</button>
</div>
</div>
</div>
</div>
<script src="slides/slides.js"></script> <script src="slides/slides.js"></script>

View file

@ -141,6 +141,123 @@
.getElementById("shareBtn") .getElementById("shareBtn")
?.addEventListener("click", () => showModal("shareModal")); ?.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("zoomInBtn")?.addEventListener("click", zoomIn);
document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut);
@ -2008,6 +2125,780 @@
return localStorage.getItem("gb-user-name") || "Anonymous"; 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 = '<p class="no-animations">No animations added yet</p>';
return;
}
listEl.innerHTML = animations
.map(
(item, index) => `
<div class="animation-item" data-index="${index}">
<div>
<div class="animation-name">${item.animation.type || "Animation"}</div>
<div class="animation-element">${item.elementType}</div>
</div>
<button class="animation-remove" data-element="${item.elementId}">×</button>
</div>
`,
)
.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 `
<div class="sorter-slide ${isSelected ? "selected" : ""}"
data-position="${position}"
data-slide-index="${slideIndex}"
draggable="true">
<div class="sorter-slide-content">
${renderSorterSlidePreview(slide)}
</div>
<div class="sorter-slide-number">${position + 1}</div>
<div class="sorter-slide-actions">
<button data-action="duplicate" title="Duplicate"></button>
<button data-action="delete" title="Delete">×</button>
</div>
</div>
`;
})
.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 = `<div style="width:100%;height:100%;background:${bgColor};padding:8px;font-size:6px;">`;
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 += `<div style="margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(text)}</div>`;
}
});
}
html += "</div>";
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 = `
<!DOCTYPE html>
<html>
<head>
<title>${state.presentationName} - PDF Export</title>
<style>
@page { size: ${orientation}; margin: 0.5in; }
@media print {
.page-break { page-break-after: always; }
}
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
.slide-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
padding: 20px;
}
.slide {
background: white;
border: 1px solid #ccc;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.slide-full { width: 100%; aspect-ratio: 16/9; }
.slide-2 { width: 45%; aspect-ratio: 16/9; }
.slide-4 { width: 45%; aspect-ratio: 16/9; }
.slide-6 { width: 30%; aspect-ratio: 16/9; }
.slide-content { padding: 20px; height: 100%; box-sizing: border-box; }
.slide-number { text-align: center; font-size: 12px; color: #666; margin-top: 8px; }
.notes-section { padding: 10px; font-size: 11px; border-top: 1px solid #ccc; }
</style>
</head>
<body>
`;
let slideCount = 0;
slidesToExport.forEach((slideIndex, i) => {
const slide = state.slides[slideIndex];
if (!slide) return;
if (slideCount > 0 && slideCount % slidesPerPage === 0) {
htmlContent += '<div class="page-break"></div>';
}
if (slideCount % slidesPerPage === 0) {
htmlContent += '<div class="slide-container">';
}
const slideClass =
slidesPerPage === 1
? "slide-full"
: slidesPerPage === 2
? "slide-2"
: slidesPerPage === 4
? "slide-4"
: "slide-6";
const bgColor = slide.background?.color || "#ffffff";
htmlContent += `
<div class="slide ${slideClass}" style="background:${bgColor};">
<div class="slide-content">
${renderSlideContentForExport(slide)}
</div>
<div class="slide-number">Slide ${slideIndex + 1}</div>
${layout === "notes" && slide.notes ? `<div class="notes-section">${escapeHtml(slide.notes)}</div>` : ""}
</div>
`;
slideCount++;
if (slideCount % slidesPerPage === 0 || i === slidesToExport.length - 1) {
htmlContent += "</div>";
}
});
htmlContent += "</body></html>";
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 += `<div style="font-size:${fontSize}px;font-weight:${fontWeight};color:${color};margin-bottom:8px;">${escapeHtml(el.content.text)}</div>`;
}
});
}
return html || "<p>Empty slide</p>";
}
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 = { window.gbSlides = {
init, init,
addSlide, addSlide,
@ -2023,6 +2914,11 @@
hideModal, hideModal,
toggleChatPanel, toggleChatPanel,
savePresentation, savePresentation,
showTransitionsModal,
showAnimationsModal,
showSlideSorter,
exportToPdf,
showMasterSlideModal,
}; };
if (document.readyState === "loading") { if (document.readyState === "loading") {