diff --git a/app/static/css/v2/base.css b/app/static/css/v2/base.css index 24d7a48..3ea15e2 100644 --- a/app/static/css/v2/base.css +++ b/app/static/css/v2/base.css @@ -1892,3 +1892,90 @@ tr:hover .row-actions { .md-content th { background: var(--bg-tertiary); font-weight: 600; color: var(--text-primary); } .md-content a { color: var(--accent); text-decoration: none; } .md-content a:hover { text-decoration: underline; } + +/* ========================================================================== + INBOX PAGE - Two-Pane Layout & Inspector + ========================================================================== */ +/* Icon buttons for Inspector action bar */ +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + font-size: 16px; + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: 6px; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + flex-shrink: 0; +} + +.btn-icon:hover:not(:disabled) { + background: var(--bg-overlay); + border-color: var(--accent); +} + +.btn-icon:disabled { + cursor: not-allowed; +} + +/* Content switcher (Summary vs Transcript) */ +.content-switcher-btn { + padding: var(--space-2) var(--space-4); + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + border: none; + background: none; + border-bottom: 2px solid transparent; + transition: all 0.15s; +} + +.content-switcher-btn:hover { + color: var(--text-secondary); +} + +.content-switcher-btn.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* List item selected state with accent border */ +.list-item.selected { + position: relative; +} + +.list-item.selected::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--accent); +} + +/* Dropdown menu */ +.dropdown-menu { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: var(--space-2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.dropdown-item { + padding: var(--space-2); + cursor: pointer; + border-radius: 4px; + font-size: 0.8125rem; + transition: background var(--duration-fast) var(--ease-out); +} + +.dropdown-item:hover { + background: var(--bg-elevated); +} diff --git a/app/static/js/v2/app.js b/app/static/js/v2/app.js index 82cc7a5..d7c7a6b 100644 --- a/app/static/js/v2/app.js +++ b/app/static/js/v2/app.js @@ -154,7 +154,7 @@ document.addEventListener('alpine:init', function() { /** * Note list component */ - Alpine.data('noteList', function(initialNotes, initialTagFilter) { + Alpine.data('inboxView', function(initialNotes, initialTagFilter) { return { notes: initialNotes || [], selectedId: null, @@ -162,6 +162,18 @@ document.addEventListener('alpine:init', function() { searchQuery: '', tagFilter: initialTagFilter || '', loading: false, + knownProjects: [], + + init: function() { + // Load known projects + var self = this; + fetch('/v2/api/projects') + .then(function(res) { return res.json(); }) + .then(function(data) { + self.knownProjects = (data.projects || []).map(function(p) { return p.name; }); + }) + .catch(function(err) { console.warn('Failed to load projects:', err); }); + }, get filteredNotes() { var filtered = this.notes; @@ -225,6 +237,113 @@ document.addEventListener('alpine:init', function() { .finally(function() { self.loading = false; }); }, + // Inspector Actions + openAudio: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note && note.audio_url) { + window.open(note.audio_url, '_blank'); + } + }, + + openNote: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note) { + window.location.href = note.link || ('/v2/note/' + note.id); + } + }, + + copyTranscript: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note && note.transcript) { + navigator.clipboard.writeText(note.transcript) + .then(function() { + console.log('Transcript copied to clipboard'); + // TODO: Show toast notification + }) + .catch(function(err) { + console.error('Failed to copy transcript:', err); + }); + } + }, + + downloadTranscript: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note && note.source === 'registry' && note.transcript) { + window.open('/v2/api/registry/' + note.id + '/download/transcript', '_blank'); + } + }, + + copyBreakdown: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note && note.content) { + navigator.clipboard.writeText(note.content) + .then(function() { + console.log('Breakdown copied to clipboard'); + // TODO: Show toast notification + }) + .catch(function(err) { + console.error('Failed to copy breakdown:', err); + }); + } + }, + + downloadBreakdown: function() { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (note && note.source === 'registry' && note.content) { + window.open('/v2/api/registry/' + note.id + '/download/note', '_blank'); + } + }, + + // Project assignment + assignProject: function(projectName) { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (!note || note.source !== 'registry') return; + + var self = this; + var projects = projectName ? [projectName] : []; + + // Optimistic update + note.projects = projects; + + fetch('/v2/api/registry/' + note.id + '/projects', { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ projects: projects }) + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + self.toast('Project updated'); + }) + .catch(function(err) { + self.toast('Failed to update project. Please try again.'); + console.error('Project update error:', err); + }); + }, + + createAndAssignProject: function(projectName) { + var note = this.filteredNotes.find(function(n) { return n.id === this.selectedId; }.bind(this)); + if (!note || note.source !== 'registry') return; + + projectName = (projectName || '').trim(); + if (!projectName) return; + + var self = this; + + // Add to known projects + if (!this.knownProjects.includes(projectName)) { + this.knownProjects.push(projectName); + } + + // Assign the project + this.assignProject(projectName); + }, + + toast: function(msg) { + // Simple toast notification - TODO: Implement proper toast UI + // For now, just log to console as a placeholder + console.log('Toast:', msg); + }, + // Format helpers for templates formatDate: function(d) { return window.formatDate ? window.formatDate(d) : (d || ''); }, formatDuration: function(s) { return window.formatDuration ? window.formatDuration(s) : (s || ''); }, diff --git a/app/templates/v2/inbox.html b/app/templates/v2/inbox.html index ff4de2a..a7f87b1 100644 --- a/app/templates/v2/inbox.html +++ b/app/templates/v2/inbox.html @@ -8,7 +8,7 @@ {% endblock %} {% block content %} -
+
- +
- +
@@ -92,33 +92,21 @@

Inbox