-
-
-
-
-
-
- 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 = `
+
+
+
-
-
-
+
+
+
-
-
+ 0:00
+
+
+
+
+
+ `;
+
+ // 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
+
+
+
+
+
+
+ Page 1 of 1
+
+
+
+
+
+
+
+
+
+
+
+ chars/page
+
+
+ 0 characters
+