diff --git a/static/css/style.css b/static/css/style.css index 0103f57..65ba98a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -212,51 +212,308 @@ body { .floating-btn:hover .tooltip-text { opacity: 1; } /* ============================================ - Chapter/Section Markers + Chapter/Section Markers - Base Styles ============================================= */ .editor-marker { - padding: 15px; + padding: 12px 16px; margin: 20px 0; - border-radius: 10px; + border-radius: 12px; user-select: none; cursor: default; position: relative; - border: 1px solid rgba(0,0,0,0.1); + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } .chapter-marker { - background: #fff0f0; - border-left: 5px solid #FF6B6B; + background: linear-gradient(135deg, #fff5f5 0%, #ffe3e3 100%); + border-left: 4px solid #FF6B6B; } .section-marker { - background: #f0f0ff; - border-left: 5px solid #4834d4; + background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); + border-left: 4px solid #4834d4; margin-left: 20px; } -.marker-header { - display: flex; - align-items: center; - gap: 15px; +/* ============================================ + Section Marker Toolbar Styles +============================================= */ +.editor-marker .marker-header { + display: flex; flex-wrap: wrap; + align-items: center; + gap: 12px; } -.marker-title { - font-family: 'Poppins', sans-serif; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 1px; - font-size: 14px; +.editor-marker .marker-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + flex: 1; } -.chapter-marker .marker-title { color: #d63031; } -.section-marker .marker-title { color: #4834d4; } +.editor-marker .marker-title { + font-family: 'Poppins', sans-serif; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 12px; + min-width: 65px; + padding: 4px 0; +} -.marker-controls { - display: flex; - align-items: center; - gap: 10px; +.chapter-marker .marker-title { + color: #dc2626; +} + +.section-marker .marker-title { + color: #5b21b6; +} + +/* Toolbar Groups */ +.toolbar-group { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: rgba(255, 255, 255, 0.8); + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.06); +} + +.toolbar-group-label { + font-size: 9px; + font-weight: 700; + color: #9ca3af; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 6px; + white-space: nowrap; +} + +/* Toolbar Buttons */ +.toolbar-btn { + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #e5e7eb; + background: white; + border-radius: 6px; + color: #4b5563; + font-size: 14px; + transition: all 0.2s ease; + cursor: pointer; +} + +.toolbar-btn:hover { + background: #f9fafb; + border-color: #d1d5db; + color: #1f2937; +} + +.toolbar-btn:active { + background: #f3f4f6; + transform: scale(0.95); +} + +.toolbar-btn.dropdown-toggle { + width: auto; + padding: 0 10px; + gap: 4px; + font-size: 11px; + font-weight: 600; +} + +.toolbar-btn.dropdown-toggle::after { + font-size: 8px; + margin-left: 2px; + vertical-align: middle; +} + +/* Dropdown Menus in Toolbar */ +.editor-marker .dropdown-menu { + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); + border: 1px solid rgba(0, 0, 0, 0.08); + padding: 6px; + min-width: 170px; + font-size: 13px; +} + +.editor-marker .dropdown-item { + border-radius: 6px; + padding: 9px 12px; + font-size: 13px; + font-weight: 500; + transition: all 0.15s ease; + color: #374151; +} + +.editor-marker .dropdown-item:hover { + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%); + color: white; +} + +.editor-marker .dropdown-item i { + width: 20px; + text-align: center; + font-size: 14px; +} + +/* Voice Select */ +.voice-select { + min-width: 140px; + max-width: 160px; + height: 32px; + font-size: 11px; + font-weight: 500; + border-radius: 6px; + border: 1px solid #e5e7eb; + padding: 0 8px; + background: white; + color: #374151; + cursor: pointer; +} + +.voice-select:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); + outline: none; +} + +/* Number Input */ +.marker-number-input { + width: 50px !important; + height: 32px; + font-size: 13px; + font-weight: 600; + text-align: center; + border-radius: 0 6px 6px 0 !important; + border: 1px solid #e5e7eb; + border-left: none; +} + +.marker-number-input:focus { + border-color: var(--accent-primary); + box-shadow: none; + outline: none; +} + +.marker-number-group { + display: flex; + align-items: center; +} + +.marker-number-group .input-group-text { + height: 32px; + padding: 0 10px; + font-size: 12px; + font-weight: 700; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-right: none; + border-radius: 6px 0 0 6px; + color: #6b7280; +} + +/* Action Buttons */ +.toolbar-action-btn { + height: 32px; + padding: 0 14px; + font-size: 11px; + font-weight: 600; + border-radius: 6px; + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + transition: all 0.2s ease; + cursor: pointer; + border: none; +} + +.toolbar-action-btn i { + font-size: 13px; +} + +.toolbar-action-btn.btn-primary { + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%); + border: none; + color: white; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); +} + +.toolbar-action-btn.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.toolbar-action-btn.btn-danger { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + border: none; + color: white; + box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3); +} + +.toolbar-action-btn.btn-danger:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); +} + +.toolbar-action-btn.btn-outline-danger { + border: 1px solid #fca5a5; + color: #dc2626; + background: white; +} + +.toolbar-action-btn.btn-outline-danger:hover { + background: #fef2f2; + border-color: #f87171; +} + +/* Image Button */ +.toolbar-btn.image-btn { + color: #059669; + border-color: #a7f3d0; + background: #ecfdf5; +} + +.toolbar-btn.image-btn:hover { + background: #d1fae5; + border-color: #6ee7b7; + color: #047857; +} + +/* TTS Button */ +.toolbar-btn.tts-btn { + width: auto; + padding: 0 10px; + gap: 4px; + color: #7c3aed; + border-color: #ddd6fe; + background: #f5f3ff; + font-weight: 700; + font-size: 10px; +} + +.toolbar-btn.tts-btn:hover { + background: #ede9fe; + border-color: #c4b5fd; + color: #6d28d9; +} + +/* Divider */ +.toolbar-divider { + width: 1px; + height: 24px; + background: #e5e7eb; + margin: 0 2px; + flex-shrink: 0; } /* ============================================ @@ -589,23 +846,23 @@ body { Image Upload Styles for Section Markers ============================================= */ .section-image-container { - margin: 10px 0; - padding: 10px; - background: #f8f9fa; - border-radius: 8px; - border: 2px dashed #dee2e6; + margin: 12px 0 0 0; + padding: 12px; + background: rgba(255, 255, 255, 0.6); + border-radius: 10px; + border: 2px dashed #d1d5db; transition: all 0.3s ease; } .section-image-container.drag-over { - border-color: #667eea; - background: #f0f4ff; + border-color: var(--accent-primary); + background: rgba(102, 126, 234, 0.05); } .section-image-container.has-image { border-style: solid; - border-color: #28a745; - background: #f8fff8; + border-color: #10b981; + background: rgba(16, 185, 129, 0.05); } .image-drop-zone { @@ -613,24 +870,25 @@ body { flex-direction: column; align-items: center; justify-content: center; - min-height: 80px; + min-height: 70px; cursor: pointer; - color: #6c757d; - font-size: 14px; + color: #9ca3af; + font-size: 13px; + transition: all 0.2s; } .image-drop-zone i { - font-size: 24px; - margin-bottom: 8px; - color: #adb5bd; + font-size: 22px; + margin-bottom: 6px; + color: #d1d5db; } .image-drop-zone:hover { - color: #495057; + color: #6b7280; } .image-drop-zone:hover i { - color: #667eea; + color: var(--accent-primary); } .image-preview-wrapper { @@ -681,8 +939,8 @@ body { .image-info { font-size: 11px; - color: #6c757d; - margin-top: 5px; + color: #6b7280; + margin-top: 6px; text-align: center; } @@ -693,7 +951,7 @@ body { /* Save Button Styles */ .save-project-btn { - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; color: white; font-weight: bold; @@ -704,7 +962,7 @@ body { .save-project-btn:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); color: white; } @@ -758,3 +1016,111 @@ body { .dropdown-divider { margin: 8px 0; } + +/* ============================================ + Responsive Adjustments for Toolbar +============================================= */ +@media (max-width: 1400px) { + .editor-marker .marker-controls { + gap: 8px; + } + + .toolbar-group { + padding: 3px 8px; + } + + .toolbar-group-label { + display: none; + } + + .voice-select { + min-width: 120px; + max-width: 140px; + } +} + +@media (max-width: 1200px) { + .toolbar-btn { + width: 30px; + height: 30px; + font-size: 13px; + } + + .toolbar-action-btn { + height: 30px; + padding: 0 12px; + font-size: 10px; + } + + .voice-select { + height: 30px; + font-size: 10px; + } + + .marker-number-input { + height: 30px; + } + + .marker-number-group .input-group-text { + height: 30px; + } +} + +@media (max-width: 992px) { + .editor-marker .marker-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .editor-marker .marker-controls { + width: 100%; + justify-content: flex-start; + } + + .section-marker { + margin-left: 10px; + } +} + +@media (max-width: 768px) { + .toolbar-group { + padding: 3px 6px; + } + + .toolbar-divider { + display: none; + } + + .editor-marker .marker-controls { + gap: 6px; + } + + .toolbar-action-btn span { + display: none; + } + + .toolbar-action-btn { + padding: 0 10px; + } + + .voice-select { + min-width: 100px; + max-width: 120px; + } +} + +@media (max-width: 576px) { + .toolbar-group { + flex-wrap: wrap; + } + + .editor-marker { + padding: 10px 12px; + margin: 15px 0; + } + + .section-marker { + margin-left: 5px; + } +} diff --git a/static/js/editor.js b/static/js/editor.js index 8806241..0663289 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -134,7 +134,6 @@ window.insertSectionMarker = insertSectionMarker; function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) { const id = markerId || (Date.now() + '_' + Math.random().toString(36).substr(2, 9)); const title = type.toUpperCase(); - const btnClass = type === 'chapter' ? 'btn-danger' : 'btn-primary'; const voiceOpts = VOICES.map(v => `` @@ -146,26 +145,71 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) { if (type === 'section') { extraControls = ` -
-
- - - - - -
- + +
+ + + + + +
+ Tools + + +
+ + +
+ Media + -
+ +
+ + + + `; imageSection = ` @@ -186,6 +230,18 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) { `; + } else { + // Chapter marker - simpler controls + extraControls = ` +
+ + + `; } return ` @@ -193,21 +249,15 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
${title}
-
+ +
# - +
- ${voiceOpts} ${extraControls} -
- -
${imageSection} @@ -573,6 +623,170 @@ function formatBlock(tag) { // Make it globally available window.formatBlock = formatBlock; +// ========================================== +// CASE CHANGE FUNCTIONS +// ========================================== + +/** + * Change the case of selected text + * @param {string} caseType - Type of case: 'sentence', 'lower', 'upper', 'capitalized', 'title' + */ +function changeCase(caseType) { + const selection = window.getSelection(); + + // Check if there's a valid selection with actual content + if (!selection || selection.isCollapsed || selection.toString().trim().length === 0) { + alert('Please select some text first to change its case.'); + return; + } + + const selectedText = selection.toString(); + let transformedText = ''; + + switch (caseType) { + case 'sentence': + transformedText = toSentenceCase(selectedText); + break; + case 'lower': + transformedText = selectedText.toLowerCase(); + break; + case 'upper': + transformedText = selectedText.toUpperCase(); + break; + case 'capitalized': + transformedText = toCapitalizedCase(selectedText); + break; + case 'title': + transformedText = toTitleCase(selectedText); + break; + default: + transformedText = selectedText; + } + + // Replace the selected text with transformed text + replaceSelectedText(selection, transformedText); +} + +// Make it globally available +window.changeCase = changeCase; + +/** + * Convert text to Sentence case + * First letter of each sentence is capitalized, rest is lowercase + * @param {string} text - Input text + * @returns {string} Transformed text + */ +function toSentenceCase(text) { + // First convert everything to lowercase + let result = text.toLowerCase(); + + // Capitalize the first letter + result = result.charAt(0).toUpperCase() + result.slice(1); + + // Capitalize letter after sentence-ending punctuation (. ! ?) + result = result.replace(/([.!?]\s*)([a-z])/g, (match, punctuation, letter) => { + return punctuation + letter.toUpperCase(); + }); + + // Also handle newlines as sentence breaks + result = result.replace(/(\n\s*)([a-z])/g, (match, newline, letter) => { + return newline + letter.toUpperCase(); + }); + + return result; +} + +/** + * Convert text to Capitalized Case + * First letter of every word is capitalized + * @param {string} text - Input text + * @returns {string} Transformed text + */ +function toCapitalizedCase(text) { + return text.toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +/** + * Convert text to Title Case + * Like Capitalized Case but keeps small words (articles, prepositions, conjunctions) lowercase + * unless they're the first or last word + * @param {string} text - Input text + * @returns {string} Transformed text + */ +function toTitleCase(text) { + // Words that should remain lowercase (unless first or last) + const smallWords = [ + 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'in', 'nor', + 'of', 'on', 'or', 'so', 'the', 'to', 'up', 'yet', 'is', 'be', + 'with', 'from', 'into', 'over', 'after', 'under', 'above' + ]; + + const words = text.toLowerCase().split(/(\s+)/); // Split but keep whitespace + + let isFirstWord = true; + let result = []; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + + // If it's whitespace, just add it + if (/^\s+$/.test(word)) { + result.push(word); + continue; + } + + // Check if this is the last actual word + let isLastWord = true; + for (let j = i + 1; j < words.length; j++) { + if (!/^\s+$/.test(words[j])) { + isLastWord = false; + break; + } + } + + // Capitalize if it's first word, last word, or not a small word + if (isFirstWord || isLastWord || !smallWords.includes(word.toLowerCase())) { + result.push(word.charAt(0).toUpperCase() + word.slice(1)); + } else { + result.push(word); + } + + isFirstWord = false; + } + + return result.join(''); +} + +/** + * Replace selected text with new text while preserving cursor position + * @param {Selection} selection - Current selection + * @param {string} newText - Text to insert + */ +function replaceSelectedText(selection, newText) { + if (!selection.rangeCount) return; + + const range = selection.getRangeAt(0); + + // Delete the selected content + range.deleteContents(); + + // Create a text node with the new text + const textNode = document.createTextNode(newText); + + // Insert the new text + range.insertNode(textNode); + + // Move cursor to end of inserted text + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); +} + +// ========================================== +// NORMALIZE FUNCTIONS +// ========================================== + /** * Normalize section text (clean up formatting) * If text is selected, only normalize the selection