From 5d48688ab0c86f8e7e9124af624b801827360317 Mon Sep 17 00:00:00 2001 From: remsky Date: Wed, 5 Feb 2025 07:53:28 -0700 Subject: [PATCH] Refactor responsive styles, enhance layout structure, and integrate text editor functionality --- web/index.html | 99 +++---- web/src/App.js | 61 ++--- web/src/components/TextEditor.js | 250 +++++++++++++++++ web/styles/forms.css | 455 ++++++++++++++++++++++++++----- web/styles/layout.css | 69 ++++- web/styles/player.css | 82 +++--- web/styles/responsive.css | 44 +-- 7 files changed, 823 insertions(+), 237 deletions(-) create mode 100644 web/src/components/TextEditor.js diff --git a/web/index.html b/web/index.html index a862b76..be4d5e7 100644 --- a/web/index.html +++ b/web/index.html @@ -45,32 +45,18 @@
-
-
- - - -
-

Tips

-
    -
  • Use <<>> to add an intentional break between chunks
  • -
-
-
- -
- - - +
+ + + +
+

Tips

+
    +
  • Use <<>> to add an intentional break between chunks
  • +
+
-
- - -
@@ -111,34 +86,44 @@ Generate Speech +
+ + +
-
-
-
- - -
- - - - -
- 0:00 -
-
+
+
+
+ + +
+ + + +
- +
+
+
+
+
+ + + +
diff --git a/web/src/App.js b/web/src/App.js index 84a9b0a..4b6c3ed 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -4,22 +4,19 @@ import PlayerState from './state/PlayerState.js'; import PlayerControls from './components/PlayerControls.js'; import VoiceSelector from './components/VoiceSelector.js'; import WaveVisualizer from './components/WaveVisualizer.js'; +import TextEditor from './components/TextEditor.js'; export class App { constructor() { this.elements = { - textInput: document.getElementById('text-input'), generateBtn: document.getElementById('generate-btn'), generateBtnText: document.querySelector('#generate-btn .btn-text'), generateBtnLoader: document.querySelector('#generate-btn .loader'), downloadBtn: document.getElementById('download-btn'), - fileInput: document.getElementById('file-input'), - uploadBtn: document.getElementById('upload-btn'), autoplayToggle: document.getElementById('autoplay-toggle'), formatSelect: document.getElementById('format-select'), status: document.getElementById('status'), - cancelBtn: document.getElementById('cancel-btn'), - clearBtn: document.getElementById('clear-btn') + cancelBtn: document.getElementById('cancel-btn') }; this.initialize(); @@ -35,6 +32,16 @@ export class App { this.playerControls = new PlayerControls(this.audioService, this.playerState); this.voiceSelector = new VoiceSelector(this.voiceService); this.waveVisualizer = new WaveVisualizer(this.playerState); + + // Initialize text editor + const editorContainer = document.getElementById('text-editor'); + this.textEditor = new TextEditor(editorContainer, { + linesPerPage: 20, + onTextChange: (text) => { + // Optional: Handle text changes here if needed + console.log('Text changed:', text); + } + }); // Initialize voice selector const voicesLoaded = await this.voiceSelector.initialize(); @@ -59,44 +66,10 @@ export class App { this.elements.cancelBtn.addEventListener('click', () => { this.audioService.cancel(); this.setGenerating(false); - this.elements.downloadBtn.style.display = 'none'; + this.elements.downloadBtn.classList.remove('ready'); this.showStatus('Generation cancelled', 'info'); }); - // Clear text button - this.elements.clearBtn.addEventListener('click', () => { - this.elements.textInput.value = ''; - this.elements.textInput.focus(); - }); - - // Upload button - this.elements.uploadBtn.addEventListener('click', () => { - this.elements.fileInput.click(); - }); - - // File input change - this.elements.fileInput.addEventListener('change', async (event) => { - const file = event.target.files[0]; - if (!file) return; - - if (file.size > 1024 * 1024) { // 1MB limit - this.showStatus('File too large. Please choose a file under 1MB', 'error'); - return; - } - - try { - const text = await file.text(); - this.elements.textInput.value = text; - this.showStatus('File loaded successfully', 'success'); - } catch (error) { - console.error('Error reading file:', error); - this.showStatus('Error reading file', 'error'); - } - - // Clear the input so the same file can be loaded again - this.elements.fileInput.value = ''; - }); - // Handle page unload window.addEventListener('beforeunload', () => { this.audioService.cleanup(); @@ -108,7 +81,7 @@ export class App { setupAudioEvents() { // Handle download button visibility this.audioService.addEventListener('downloadReady', () => { - this.elements.downloadBtn.style.display = 'flex'; + this.elements.downloadBtn.classList.add('ready'); }); // Handle buffer errors @@ -172,7 +145,7 @@ export class App { } validateInput() { - const text = this.elements.textInput.value.trim(); + const text = this.textEditor.getText().trim(); if (!text) { this.showStatus('Please enter some text', 'error'); return false; @@ -192,12 +165,12 @@ export class App { return; } - const text = this.elements.textInput.value.trim(); + const text = this.textEditor.getText().trim(); const voice = this.voiceService.getSelectedVoiceString(); const speed = this.playerState.getState().speed; this.setGenerating(true); - this.elements.downloadBtn.style.display = 'none'; + this.elements.downloadBtn.classList.remove('ready'); // Just reset progress bar, don't do full cleanup this.waveVisualizer.updateProgress(0, 1); diff --git a/web/src/components/TextEditor.js b/web/src/components/TextEditor.js new file mode 100644 index 0000000..6889540 --- /dev/null +++ b/web/src/components/TextEditor.js @@ -0,0 +1,250 @@ +export default class TextEditor { + constructor(container, options = {}) { + this.options = { + charsPerPage: 500, // Default to 500 chars per page + onTextChange: null, + ...options + }; + + this.container = container; + this.currentPage = 1; + this.pages = ['']; + this.charCount = 0; + this.fullText = ''; + this.isTyping = false; + + this.setupDOM(); + this.bindEvents(); + } + + setupDOM() { + this.container.innerHTML = ` +
+
+ + + +
+
+ `; + + // Cache DOM elements + this.elements = { + pageContent: this.container.querySelector('.page-content'), + prevBtn: this.container.querySelector('.prev-btn'), + nextBtn: this.container.querySelector('.next-btn'), + pageInfo: this.container.querySelector('.page-info'), + fileInput: this.container.querySelector('.file-input'), + uploadBtn: this.container.querySelector('.upload-btn'), + clearBtn: this.container.querySelector('.clear-btn'), + charCount: this.container.querySelector('.char-count'), + charsPerPage: this.container.querySelector('.chars-input'), + formatBtn: this.container.querySelector('.format-btn') + }; + + // Set initial chars per page value + this.elements.charsPerPage.value = this.options.charsPerPage; + } + + bindEvents() { + // Handle page content changes + this.elements.pageContent.addEventListener('input', (e) => { + const newContent = e.target.value; + this.pages[this.currentPage - 1] = newContent; + + // Only handle empty pages, otherwise just update the text + if (!newContent.trim() && this.pages.length > 1) { + // Remove the empty page and adjust + this.pages.splice(this.currentPage - 1, 1); + this.currentPage = Math.min(this.currentPage, this.pages.length); + this.updatePageDisplay(); + } + + // Update full text and char count + this.fullText = this.pages.join('\n'); + this.updateCharCount(); + + if (this.options.onTextChange) { + this.options.onTextChange(this.fullText); + } + }); + + // Navigation + this.elements.prevBtn.addEventListener('click', () => { + if (this.currentPage > 1) { + this.currentPage--; + this.updatePageDisplay(); + } + }); + + this.elements.nextBtn.addEventListener('click', () => { + if (this.currentPage < this.pages.length) { + this.currentPage++; + this.updatePageDisplay(); + } + }); + + // File upload + this.elements.uploadBtn.addEventListener('click', () => { + this.elements.fileInput.click(); + }); + + this.elements.fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + this.setText(e.target.result); + if (this.options.onTextChange) { + this.options.onTextChange(this.getText()); + } + }; + reader.readAsText(file); + } + }); + + // Clear text + this.elements.clearBtn.addEventListener('click', () => { + this.clear(); + if (this.options.onTextChange) { + this.options.onTextChange(''); + } + }); + + // Cache format button + this.elements.formatBtn = this.container.querySelector('.format-btn'); + + // Characters per page control - just update the value + this.elements.charsPerPage.addEventListener('change', (e) => { + const value = parseInt(e.target.value); + if (value >= 100 && value <= 2000) { + this.options.charsPerPage = value; + } + }); + + // Format pages button - trigger the split + this.elements.formatBtn.addEventListener('click', () => { + const value = parseInt(this.elements.charsPerPage.value); + if (value >= 100 && value <= 2000) { + this.options.charsPerPage = value; + this.splitIntoPages(this.fullText); + } + }); + } + + splitIntoPages(text) { + if (!text || !text.trim()) { + this.pages = ['']; + this.fullText = ''; + this.currentPage = 1; + this.updatePageDisplay(); + this.updateCharCount(); + return; + } + + this.fullText = text; + const words = text.split(/\s+/); + this.pages = []; + let currentPage = ''; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const potentialPage = currentPage + (currentPage ? ' ' : '') + word; + + if (potentialPage.length >= this.options.charsPerPage && currentPage) { + this.pages.push(currentPage); + currentPage = word; + } else { + currentPage = potentialPage; + } + } + + if (currentPage) { + this.pages.push(currentPage); + } + + if (this.pages.length === 0) { + this.pages = ['']; + this.currentPage = 1; + } else { + // Keep current page in bounds + this.currentPage = Math.min(this.currentPage, this.pages.length); + } + + this.updatePageDisplay(); + this.updateCharCount(); + } + + setText(text) { + // Just set the text without splitting into pages + this.fullText = text; + this.pages = [text]; + this.currentPage = 1; + this.updatePageDisplay(); + this.updateCharCount(); + } + + updatePageDisplay() { + this.elements.pageContent.value = this.pages[this.currentPage - 1] || ''; + this.elements.pageInfo.textContent = `Page ${this.currentPage} of ${this.pages.length}`; + + // Update button states + this.elements.prevBtn.disabled = this.currentPage === 1; + this.elements.nextBtn.disabled = this.currentPage === this.pages.length; + } + + updateCharCount() { + const totalChars = this.fullText.length; + this.elements.charCount.textContent = `${totalChars} characters`; + } + + prevPage() { + if (this.currentPage > 1) { + this.currentPage--; + this.updatePageDisplay(); + } + } + + nextPage() { + if (this.currentPage < this.pages.length) { + this.currentPage++; + this.updatePageDisplay(); + } + } + + getText() { + return this.fullText; + } + + clear() { + this.setText(''); + } +} \ No newline at end of file diff --git a/web/styles/forms.css b/web/styles/forms.css index 64e479a..9d82fb5 100644 --- a/web/styles/forms.css +++ b/web/styles/forms.css @@ -1,7 +1,184 @@ -.textarea-container { - position: relative; +.text-editor { + display: flex; + flex-direction: column; + gap: 0.5rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem; + overflow: hidden; +} + +.editor-view { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + min-height: 0; +} + +.page-content { + flex: 1; width: 100%; - height: 100%; + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + background: rgba(15, 23, 42, 0.3); + color: var(--text); + font-family: var(--font-family); + font-size: 1rem; + resize: none; + transition: border-color 0.2s ease; + min-height: 300px; +} + +.page-content:focus { + outline: none; + border-color: var(--fg-color); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); +} + +.page-navigation { + display: flex; + justify-content: center; + align-items: center; + padding: 0.25rem; + border-bottom: 1px solid var(--border); + margin-bottom: 0.5rem; + background: rgba(15, 23, 42, 0.3); + border-radius: 0.5rem 0.5rem 0 0; +} + +.page-navigation .pagination { + transform: scale(0.9); +} + +.page-navigation .pagination button { + padding: 0.25rem 0.75rem; +} + +.editor-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.5rem 0; +} + +.chars-per-page { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.format-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--text-light); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + width: auto; + font-size: 0.875rem; +} + +.format-btn:hover { + background: rgba(99, 102, 241, 0.1); + border-color: var(--fg-color); + transform: none; + box-shadow: none; +} + +.chars-input { + width: 70px; + padding: 0.25rem 0.5rem; + border: 1px solid var(--border); + border-radius: 0.25rem; + background: rgba(15, 23, 42, 0.3); + color: var(--text); + font-size: 0.875rem; + text-align: center; +} + +.chars-input:focus { + outline: none; + border-color: var(--fg-color); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); +} + +.chars-label { + color: var(--text-light); + font-size: 0.875rem; +} + +.pagination { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.pagination button { + background: transparent; + border: 1px solid var(--border); + color: var(--text); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + width: auto; +} + +.pagination button:hover:not(:disabled) { + background: rgba(99, 102, 241, 0.1); + border-color: var(--fg-color); + transform: none; + box-shadow: none; +} + +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.page-info { + color: var(--text-light); + font-size: 0.875rem; + min-width: 100px; + text-align: center; +} + +.char-count { + color: var(--text-light); + font-size: 0.875rem; + min-width: 100px; + text-align: right; +} + +.file-controls { + display: flex; + gap: 0.5rem; +} + +.upload-btn, +.clear-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--text-light); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + width: auto; + font-size: 0.875rem; +} + +.upload-btn:hover, +.clear-btn:hover { + background: rgba(99, 102, 241, 0.1); + border-color: var(--fg-color); + transform: none; + box-shadow: none; } .help-icon { @@ -33,6 +210,7 @@ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); z-index: 1000; + transition: visibility 0s linear 0.3s, opacity 0.3s ease; } .help-icon:hover .tooltip-content { @@ -41,10 +219,6 @@ transition: visibility 0s linear 0.2s, opacity 0.3s ease 0.2s; } -.tooltip-content { - transition: visibility 0s linear 0.3s, opacity 0.3s ease; -} - .tooltip-content h4 { margin: 0 0 0.5rem 0; color: var(--text); @@ -63,47 +237,137 @@ font-family: monospace; } -textarea { +main { + display: grid; + grid-template-columns: 1fr 300px; + grid-template-rows: 1fr auto; + gap: 1.25rem; + height: auto; + min-height: calc(100vh - 3rem); + max-width: 1200px; + margin: 0 auto; + padding: 1rem 1rem 2rem 1rem; + align-items: start; +} + +.text-editor, +.controls, +.player-container { + margin-bottom: 0.5rem; +} + +.generation-progress { width: 100%; - height: calc(100% - 50px); - padding: 1rem; + height: 4px; + background: rgba(99, 102, 241, 0.1); + border-radius: 2px; + margin: 0.75rem 0; + overflow: hidden; + position: relative; +} + +.generation-progress::after { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 30%; + background: var(--fg-color); + border-radius: 2px; + animation: progress 1.5s ease-in-out infinite; +} + +@keyframes progress { + 0% { + left: -30%; + } + 100% { + left: 100%; + } +} + +/* Custom scrollbar styles */ +::-webkit-scrollbar { + width: 8px; + height: 8px; + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.2); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.3); +} + +::-webkit-scrollbar-track { + background: transparent; +} + +.text-editor { + grid-column: 1; + grid-row: 1; + min-height: 0; + height: calc(100vh - 14rem); + overflow: auto; + scrollbar-width: thin; + scrollbar-color: rgba(99, 102, 241, 0.2) transparent; + margin: 0; +} + +.controls { + grid-column: 2; + grid-row: 1; + min-height: 0; + height: calc(100vh - 14rem); + display: flex; + flex-direction: column; + gap: 1.25rem; + background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; - background: rgba(15, 23, 42, 0.3); - color: var(--text); - font-size: 1rem; - transition: border-color 0.2s ease; - font-family: var(--font-family); - resize: none; - margin-bottom: 0.75rem; -} - -textarea:focus { - outline: none; - border-color: var(--fg-color); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); -} - -textarea::placeholder { - color: var(--text-light); -} - -.text-controls { - display: flex; - gap: 0.75rem; - margin-top: auto; -} - -.text-controls button { - flex: 1; + padding: 1.25rem; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: rgba(99, 102, 241, 0.2) transparent; + margin: 0; + position: relative; } .voice-select-container { position: relative; display: flex; flex-direction: column; + gap: 0.75rem; + background: rgba(15, 23, 42, 0.3); + border-radius: 0.5rem; + padding: 1rem; + height: auto; + min-height: 120px; + max-height: 200px; + flex-shrink: 0; + margin: 0.5rem 0; + overflow: visible; +} + +.selected-voices { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.5rem; - z-index: 1001; + margin-top: 0.25rem; + height: auto; + min-height: 60px; + max-height: none; + overflow-y: visible; + padding: 0.75rem; + background: rgba(15, 23, 42, 0.3); + border-radius: 0.25rem; + scrollbar-width: thin; + scrollbar-color: rgba(99, 102, 241, 0.2) transparent; } .voice-search { @@ -136,12 +400,15 @@ textarea::placeholder { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; - margin-top: 0.5rem; - max-height: 200px; + max-height: 400px; overflow-y: auto; - z-index: 1000; + z-index: 1002; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + scrollbar-width: thin; + scrollbar-color: rgba(99, 102, 241, 0.2) transparent; + padding: 0.25rem; + margin-top: 0.5rem; } .voice-select-container:focus-within .voice-dropdown, @@ -152,39 +419,34 @@ textarea::placeholder { .voice-option { display: flex; align-items: center; - padding: 0.5rem; + padding: 0.875rem 1rem; cursor: pointer; - border-radius: 0.25rem; transition: background-color 0.2s ease; color: var(--text); + border-radius: 0.25rem; } .voice-option:hover { background: rgba(99, 102, 241, 0.1); } -.selected-voices { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - margin-top: 0.5rem; -} - .selected-voice-tag { background: rgba(99, 102, 241, 0.2); - padding: 0.25rem 0.4rem; + padding: 0.375rem 0.75rem; border-radius: 1rem; - font-size: 0.875rem; - display: flex; + font-size: 0.75rem; + display: inline-flex; align-items: center; - gap: 0.25rem; + gap: 0.375rem; border: 1px solid rgba(99, 102, 241, 0.3); + white-space: nowrap; + flex-shrink: 0; } .selected-voice-tag input { - width: 3em; - padding: 0.1rem 0.2rem; - min-height: 1.5em; + width: 2.5em; + padding: 0.1rem; + min-height: 1.25em; background: transparent; border: none; color: inherit; @@ -303,8 +565,72 @@ textarea::placeholder { color: var(--text); } +.container { + display: flex; + flex-direction: column; + height: 100vh; + padding: 0.75rem 1.25rem 1.5rem 1.25rem; + gap: 1rem; +} + +.player-container { + grid-column: 1 / -1; + grid-row: 2; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1.25rem 1.5rem; + height: auto; + min-height: 90px; + display: flex; + flex-direction: column; + justify-content: center; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + margin: 1rem 0; + align-self: start; + width: 100%; + position: relative; + z-index: 1; +} + +.options { + margin: 1rem 0; + padding: 1rem 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + .button-group { - margin-top: auto; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.generation-options { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + padding: 0.5rem 0; + margin-top: 0.5rem; + border-top: 1px solid var(--border); +} + +.generation-options label { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-light); + cursor: pointer; + font-size: 0.875rem; +} + +.button-group { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem; } button { @@ -360,17 +686,4 @@ button:disabled { .loading .btn-text { display: none; -} - -.clear-btn { - background: transparent !important; - border: 1px solid var(--border) !important; - color: var(--text-light) !important; - padding: 0.5rem 1rem !important; -} - -.clear-btn:hover { - background: rgba(99, 102, 241, 0.1) !important; - transform: none !important; - box-shadow: none !important; } \ No newline at end of file diff --git a/web/styles/layout.css b/web/styles/layout.css index fed3308..7a1c5f9 100644 --- a/web/styles/layout.css +++ b/web/styles/layout.css @@ -7,11 +7,14 @@ } main { - display: flex; + display: grid; + grid-template-columns: 1fr 300px; gap: 1rem; width: 100%; - max-width: 900px; + max-width: 1200px; margin: 0 auto; + height: calc(100vh - 6rem); + position: relative; } .input-section { @@ -24,8 +27,9 @@ main { 0 2px 4px -1px rgba(0, 0, 0, 0.06); display: flex; flex-direction: column; - width: 400px; - height: 400px; + height: 100%; + max-height: calc(100vh - 8rem); + overflow: auto; } .controls { @@ -36,10 +40,25 @@ main { backdrop-filter: blur(12px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - width: 400px; - height: 400px; display: flex; flex-direction: column; + gap: 1rem; + height: 100%; + max-height: calc(100vh - 8rem); + overflow: auto; +} + +.player-container { + background: var(--surface); + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid var(--border); + backdrop-filter: blur(12px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + grid-column: 1 / -1; + margin-top: 1rem; + height: 120px; } #upload-btn { @@ -48,13 +67,45 @@ main { @media (max-width: 850px) { main { - flex-direction: column; - align-items: center; + grid-template-columns: 1fr; + height: auto; + min-height: calc(100vh - 6rem); } .input-section, .controls { width: 100%; - max-width: 400px; + max-width: 600px; + margin-left: auto; + margin-right: auto; + height: auto; + min-height: 400px; + max-height: none; + } + + .player-container { + width: 100%; + max-width: 600px; + margin-left: auto; + margin-right: auto; + position: sticky; + bottom: 1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 1rem; + } + + main { + gap: 0.75rem; + } + + .input-section, + .controls, + .player-container { + max-width: 100%; + padding: 0.75rem; } } \ No newline at end of file diff --git a/web/styles/player.css b/web/styles/player.css index dac9bcf..bbaceb4 100644 --- a/web/styles/player.css +++ b/web/styles/player.css @@ -1,34 +1,22 @@ -.audio-controls { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-top: 1rem; - flex: 1; -} - .player-container { - display: flex; - flex-direction: column; - gap: 0.75rem; - gap: 0.75rem; - width: 100%; - background: rgba(15, 23, 42, 0.3); - padding: 0.75rem; - border-radius: 0.5rem; - border: 1px solid var(--border); - flex: 1; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr auto; + gap: 0.5rem; position: relative; - min-height: 140px; + overflow: hidden; } .player-controls { - display: flex; + display: grid; + grid-template-columns: auto minmax(100px, 1fr) 160px 70px 32px; align-items: center; - gap: 0.75rem; + gap: 1rem; width: 100%; - padding: 0.5rem; - border-radius: 0.5rem; height: 40px; + padding: 0 0.75rem; + border-radius: 0.5rem; + background: rgba(15, 23, 42, 0.2); } .seek-slider, @@ -43,7 +31,8 @@ } .seek-slider { - flex: 1; + width: 100%; + min-width: 0; } .volume-slider { @@ -85,15 +74,17 @@ .volume-control { display: flex; align-items: center; - gap: 0.75rem; + gap: 0.5rem; padding-left: 0.75rem; - border-left: 1px solid var(--border); + border-left: 1px solid rgba(99, 102, 241, 0.2); } .volume-icon { color: var(--fg-color); opacity: 0.8; transition: opacity 0.2s ease; + display: flex; + align-items: center; } .volume-icon:hover { @@ -109,9 +100,11 @@ font-weight: 500; cursor: pointer; transition: all 0.2s ease; - flex: 0 0 auto; - min-width: 60px; height: 32px; + display: flex; + align-items: center; + justify-content: center; + min-width: 60px; } .player-btn:hover { @@ -121,23 +114,25 @@ .wave-container { width: 100%; + height: 40px; background: rgba(15, 23, 42, 0.3); border-radius: 0.5rem; overflow: hidden; position: relative; - flex: 1; - min-height: 60px; + display: flex; + align-items: center; + justify-content: center; } .time-display { - min-width: 80px; font-size: 0.875rem; color: var(--text-light); text-align: right; font-variant-numeric: tabular-nums; + padding-left: 0.75rem; + border-left: 1px solid rgba(99, 102, 241, 0.2); } -/* Progress bar styles */ .generation-progress { -webkit-appearance: none; appearance: none; @@ -146,7 +141,7 @@ border: none; background: rgba(99, 102, 241, 0.1); border-radius: 3px; - margin: 1rem 0; + margin: 0.5rem 0; display: block; } @@ -178,15 +173,23 @@ .wave-container canvas { position: absolute; - top: 0; + top: 50%; left: 0; width: 100%; - height: 100%; + height: 200%; + transform: translateY(-50%) scale(0.5); +} + +.download-placeholder { + width: 32px; + height: 32px; + margin: 0.75rem; + visibility: hidden; } .download-button { position: absolute; - bottom: 0.75rem; + top: 0.75rem; right: 0.75rem; width: 32px; height: 32px; @@ -195,6 +198,13 @@ align-items: center; justify-content: center; transition: transform 0.2s ease; + opacity: 0; + pointer-events: none; +} + +.download-button.ready { + opacity: 1; + pointer-events: auto; } .download-glow { diff --git a/web/styles/responsive.css b/web/styles/responsive.css index b339fc6..6ae7713 100644 --- a/web/styles/responsive.css +++ b/web/styles/responsive.css @@ -32,7 +32,18 @@ width: 6px; } - .input-section, .player-section { + main { + grid-template-columns: 1fr; + max-width: 400px; + } + + .input-section, + .controls, + .player-container { + width: 100%; + } + + .input-section { padding: 1.5rem; } @@ -58,38 +69,31 @@ } .player-container { - flex-direction: column; - align-items: stretch; - gap: 0.75rem; + min-height: 180px; } .player-controls { - flex-direction: column; - gap: 0.75rem; + grid-template-columns: auto 1fr auto !important; + grid-template-rows: auto auto; + gap: 0.5rem !important; } - .player-btn { - width: 100%; + .seek-slider { + grid-column: 1 / -1; + grid-row: 2; } .volume-control { - border-left: none; - border-top: 1px solid var(--border); - padding-left: 0; - padding-top: 0.75rem; - width: 100%; + display: flex; + align-items: center; + gap: 0.5rem; } .volume-slider { - flex: 1; - width: auto; + width: 60px; } .wave-container { - height: 80px; - } - - .time-display { - text-align: center; + height: 60px; } } \ No newline at end of file