personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): add unified date navigator component for app bars

Create a shared date_nav.html template that provides consistent day-based
navigation across all apps with date views. The component includes prev/next
buttons, a native date picker, today button, and keyboard shortcuts (←/→/t).

- Add convey/templates/date_nav.html with self-contained CSS/HTML/JS
- Migrate tokens, todos, and calendar apps to use the new component
- Remove ~200 lines of duplicate navigation CSS from individual apps
- Remove unused macros.html (day_heading macro no longer used)
- Document date navigator usage in APPS.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+231 -291
+19 -2
APPS.md
··· 158 158 - Only rendered when app provides this template 159 159 - Great for persistent input controls across views 160 160 161 + **Date Navigator Component:** 162 + 163 + For apps with day-based navigation, include the shared date navigator: 164 + 165 + ```html 166 + {% include 'date_nav.html' %} 167 + ``` 168 + 169 + This provides a unified `← 🗓️ Today →` control with: 170 + - Previous/next day buttons 171 + - Native date picker (calendar icon) 172 + - Today button (disabled when viewing current day) 173 + - Keyboard shortcuts: ←/→ arrows, `t` for today 174 + 175 + The component reads `day` and `app` from template context to construct navigation URLs. 176 + 161 177 **Reference implementations:** 162 - - Form input: `apps/todos/app_bar.html` (date navigation, todo input) 163 - - Controls: `apps/tokens/app_bar.html` (action buttons) 178 + - Date navigation: `apps/todos/app_bar.html`, `apps/tokens/app_bar.html`, `apps/calendar/app_bar.html` 179 + 180 + **Implementation source:** `convey/templates/date_nav.html` 164 181 165 182 ### 6. `background.html` - Background Service 166 183
+2 -10
apps/calendar/_day.html
··· 1 1 {# Calendar day view - events timeline and topic tabs #} 2 - {% import 'macros.html' as macros %} 3 2 4 3 <style> 5 4 .modal { display:none; position:fixed; z-index:1000; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.5); } ··· 11 10 </style> 12 11 13 12 <div class="workspace-content-wide"> 14 - {{ macros.day_heading(title, 15 - prev_day and url_for('app:calendar.calendar_day', day=prev_day), 16 - next_day and url_for('app:calendar.calendar_day', day=next_day), 17 - url_for('app:calendar.calendar_transcript_page', day=day), 18 - None, 19 - url_for('app:todos.todos_day', day=day), 20 - None, 21 - url_for('app:calendar._dev_calendar_screens_list', day=day)) }} 13 + <h1>{{ title }}</h1> 22 14 <div class="tabs"> 23 15 <a class="tab active" href="#events" data-target="events" title="Events">Events</a> 24 16 {% for file in files %} ··· 40 32 </div> 41 33 </div> 42 34 </div> 43 - <script src="{{ url_for('home.static', filename='colors.js') }}"></script> 35 + <script src="{{ url_for('root.static', filename='colors.js') }}"></script> 44 36 <script> 45 37 let tabs = Array.from(document.querySelectorAll('.tab')); 46 38 const contents = document.querySelectorAll('.content');
+1 -5
apps/calendar/_dev_screens_detail.html
··· 1 1 {# Screen debug viewer - detailed view of a single screen.jsonl file #} 2 - {% import 'macros.html' as macros %} 3 2 4 3 <style> 5 4 .screens-detail { ··· 235 234 </style> 236 235 237 236 <div class="calendar-content"> 238 - {{ macros.day_heading(title, 239 - prev_day and url_for('app:calendar._dev_calendar_screens_list', day=prev_day), 240 - next_day and url_for('app:calendar._dev_calendar_screens_list', day=next_day), 241 - url_for('app:calendar.calendar_transcript_page', day=day)) }} 237 + <h1>{{ title }}</h1> 242 238 243 239 <div class="screens-detail"> 244 240 <a href="{{ url_for('app:calendar._dev_calendar_screens_list', day=day) }}" class="back-link">
+1 -5
apps/calendar/_dev_screens_list.html
··· 1 1 {# Screen debug viewer - list of screen.jsonl files for a day #} 2 - {% import 'macros.html' as macros %} 3 2 4 3 <style> 5 4 .screens-list { ··· 89 88 </style> 90 89 91 90 <div class="calendar-content"> 92 - {{ macros.day_heading(title, 93 - prev_day and url_for('app:calendar._dev_calendar_screens_list', day=prev_day), 94 - next_day and url_for('app:calendar._dev_calendar_screens_list', day=next_day), 95 - url_for('app:calendar.calendar_transcript_page', day=day)) }} 91 + <h1>{{ title }}</h1> 96 92 97 93 <div class="screens-list"> 98 94 <h2>
+1 -4
apps/calendar/_transcript.html
··· 1 1 {# Calendar transcript viewer - dual-timeline interface #} 2 - {% import 'macros.html' as macros %} 3 2 4 3 <style> 5 4 :root{ ··· 66 65 </style> 67 66 68 67 <div class="calendar-content"> 69 - {{ macros.day_heading(title, 70 - prev_day and url_for('app:calendar.calendar_transcript_page', day=prev_day), 71 - next_day and url_for('app:calendar.calendar_transcript_page', day=next_day)) }} 68 + <h1>{{ title }}</h1> 72 69 </div> 73 70 <div class="wrap"> 74 71 <div class="card">
+4
apps/calendar/app_bar.html
··· 1 + {% set current_view = view|default('month') %} 2 + {% if current_view != 'month' %} 3 + {% include 'date_nav.html' %} 4 + {% endif %}
+1
apps/todos/app_bar.html
··· 1 + {% include 'date_nav.html' %}
+4 -71
apps/todos/workspace.html
··· 1 1 <style> 2 - .todo-header { 3 - display: flex; 4 - justify-content: space-between; 5 - align-items: center; 6 - margin-bottom: 1.5rem; 7 - padding-bottom: 0.75rem; 8 - border-bottom: 1px solid #e5e7eb; 9 - } 10 - 11 2 .todo-title { 12 3 font-size: 1.5rem; 13 4 font-weight: 600; 14 5 color: #111827; 15 - margin: 0; 16 - } 17 - 18 - .todo-nav { 19 - display: flex; 20 - gap: 0.5rem; 21 - align-items: center; 22 - } 23 - 24 - .todo-nav-btn { 25 - padding: 0.4rem 0.8rem; 26 - background: #f3f4f6; 27 - border: 1px solid #d1d5db; 28 - border-radius: 6px; 29 - color: #374151; 30 - text-decoration: none; 31 - font-size: 0.9rem; 32 - transition: background 0.15s, border-color 0.15s; 33 - } 34 - 35 - .todo-nav-btn:hover { 36 - background: #e5e7eb; 37 - border-color: #9ca3af; 38 - } 39 - 40 - .todo-nav-btn.today { 41 - background: #2563eb; 42 - border-color: #2563eb; 43 - color: #ffffff; 44 - } 45 - 46 - .todo-nav-btn.today:hover { 47 - background: #1d4ed8; 48 - border-color: #1d4ed8; 49 - } 50 - 51 - .todo-nav-btn.today.disabled { 52 - background: #9ca3af; 53 - border-color: #9ca3af; 54 - cursor: default; 55 - opacity: 0.6; 56 - } 57 - 58 - .todo-nav-btn.today.disabled:hover { 59 - background: #9ca3af; 60 - border-color: #9ca3af; 6 + margin: 0 0 1.5rem 0; 7 + padding-bottom: 0.75rem; 8 + border-bottom: 1px solid #e5e7eb; 61 9 } 62 10 63 11 .add-form { ··· 295 243 </style> 296 244 297 245 <div class="workspace-content"> 298 - <div class="todo-header"> 299 - <h1 class="todo-title">{{ title }}</h1> 300 - <div class="todo-nav"> 301 - {% if prev_day %} 302 - <a href="{{ url_for('app:todos.todos_day', day=prev_day) }}" class="todo-nav-btn">←</a> 303 - {% endif %} 304 - {% if day == today_day %} 305 - <span class="todo-nav-btn today disabled">Today</span> 306 - {% else %} 307 - <a href="{{ url_for('app:todos.todos_day', day=today_day) }}" class="todo-nav-btn today">Today</a> 308 - {% endif %} 309 - {% if next_day %} 310 - <a href="{{ url_for('app:todos.todos_day', day=next_day) }}" class="todo-nav-btn">→</a> 311 - {% endif %} 312 - </div> 313 - </div> 246 + <h1 class="todo-title">{{ title }}</h1> 314 247 315 248 {% with messages = get_flashed_messages(with_categories=true) %} 316 249 {% if messages %}
+1 -178
apps/tokens/app_bar.html
··· 1 - <style> 2 - .tokens-nav-button { 3 - background: white; 4 - border: 1px solid #ddd; 5 - border-radius: 6px; 6 - padding: 0.6em 1.2em; 7 - font-family: inherit; 8 - font-size: 0.9em; 9 - cursor: pointer; 10 - transition: all 0.2s; 11 - } 12 - 13 - .tokens-nav-button:hover { 14 - background: #f8f9fa; 15 - border-color: #667eea; 16 - } 17 - 18 - .tokens-nav-button:active { 19 - transform: scale(0.98); 20 - } 21 - 22 - .tokens-today-button { 23 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 24 - color: white; 25 - border: none; 26 - border-radius: 6px; 27 - padding: 0.6em 1.2em; 28 - font-family: inherit; 29 - font-size: 0.9em; 30 - font-weight: 600; 31 - cursor: pointer; 32 - transition: opacity 0.2s; 33 - } 34 - 35 - .tokens-today-button:hover { 36 - opacity: 0.9; 37 - } 38 - 39 - .tokens-date-display { 40 - position: relative; 41 - display: flex; 42 - align-items: center; 43 - } 44 - 45 - .tokens-date-display input { 46 - position: absolute; 47 - opacity: 0; 48 - width: 100%; 49 - height: 100%; 50 - cursor: pointer; 51 - } 52 - 53 - .tokens-date-display span { 54 - font-family: 'SF Mono', Monaco, monospace; 55 - font-size: 1.1em; 56 - font-weight: 600; 57 - padding: 0.6em 1em; 58 - background: white; 59 - border: 1px solid #ddd; 60 - border-radius: 6px; 61 - pointer-events: none; 62 - } 63 - </style> 64 - 65 - <button id="prev-day" class="tokens-nav-button" title="Previous day">← Prev Day</button> 66 - <div class="tokens-date-display"> 67 - <input type="date" id="date-picker" value="{{ day }}" /> 68 - <span id="current-date">{{ day }}</span> 69 - </div> 70 - <button id="next-day" class="tokens-nav-button" title="Next day">Next Day →</button> 71 - <button id="today" class="tokens-today-button" title="Jump to today">Today</button> 72 - 73 - <script> 74 - (function() { 75 - // Parse day from URL or use today 76 - const pathParts = window.location.pathname.split('/'); 77 - let currentDay = pathParts[pathParts.length - 1]; 78 - 79 - // If current day doesn't look like YYYYMMDD, use today 80 - if (!/^\d{8}$/.test(currentDay)) { 81 - currentDay = new Date().toISOString().split('T')[0].replace(/-/g, ''); 82 - } 83 - 84 - // Format date for display 85 - function formatDisplayDate(day) { 86 - const year = day.substring(0, 4); 87 - const month = day.substring(4, 6); 88 - const dayNum = day.substring(6, 8); 89 - const date = new Date(year, month - 1, dayNum); 90 - return date.toLocaleDateString('en-US', { 91 - year: 'numeric', 92 - month: 'long', 93 - day: 'numeric' 94 - }); 95 - } 96 - 97 - // Convert YYYYMMDD to YYYY-MM-DD for input[type=date] 98 - function toDateInputFormat(day) { 99 - const year = day.substring(0, 4); 100 - const month = day.substring(4, 6); 101 - const dayNum = day.substring(6, 8); 102 - return `${year}-${month}-${dayNum}`; 103 - } 104 - 105 - // Convert YYYY-MM-DD to YYYYMMDD 106 - function fromDateInputFormat(dateStr) { 107 - return dateStr.replace(/-/g, ''); 108 - } 109 - 110 - // Add/subtract days 111 - function adjustDay(day, delta) { 112 - const year = parseInt(day.substring(0, 4)); 113 - const month = parseInt(day.substring(4, 6)) - 1; 114 - const dayNum = parseInt(day.substring(6, 8)); 115 - const date = new Date(year, month, dayNum); 116 - date.setDate(date.getDate() + delta); 117 - 118 - const newYear = date.getFullYear(); 119 - const newMonth = String(date.getMonth() + 1).padStart(2, '0'); 120 - const newDay = String(date.getDate()).padStart(2, '0'); 121 - return `${newYear}${newMonth}${newDay}`; 122 - } 123 - 124 - // Navigate to day 125 - function navigateToDay(day) { 126 - window.location.href = `/app/tokens/${day}`; 127 - } 128 - 129 - // Update display 130 - function updateDisplay() { 131 - document.getElementById('current-date').textContent = formatDisplayDate(currentDay); 132 - document.getElementById('date-picker').value = toDateInputFormat(currentDay); 133 - } 134 - 135 - // Event listeners 136 - document.getElementById('prev-day').addEventListener('click', () => { 137 - const newDay = adjustDay(currentDay, -1); 138 - navigateToDay(newDay); 139 - }); 140 - 141 - document.getElementById('next-day').addEventListener('click', () => { 142 - const newDay = adjustDay(currentDay, 1); 143 - navigateToDay(newDay); 144 - }); 145 - 146 - document.getElementById('today').addEventListener('click', () => { 147 - const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); 148 - navigateToDay(today); 149 - }); 150 - 151 - document.getElementById('date-picker').addEventListener('change', (e) => { 152 - const newDay = fromDateInputFormat(e.target.value); 153 - navigateToDay(newDay); 154 - }); 155 - 156 - // Keyboard shortcuts 157 - document.addEventListener('keydown', (e) => { 158 - // Left arrow = previous day 159 - if (e.key === 'ArrowLeft' && !e.target.matches('input, textarea')) { 160 - const newDay = adjustDay(currentDay, -1); 161 - navigateToDay(newDay); 162 - } 163 - // Right arrow = next day 164 - if (e.key === 'ArrowRight' && !e.target.matches('input, textarea')) { 165 - const newDay = adjustDay(currentDay, 1); 166 - navigateToDay(newDay); 167 - } 168 - // T = today 169 - if (e.key === 't' && !e.target.matches('input, textarea')) { 170 - const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); 171 - navigateToDay(today); 172 - } 173 - }); 174 - 175 - // Initialize 176 - updateDisplay(); 177 - })(); 178 - </script> 1 + {% include 'date_nav.html' %}
+197
convey/templates/date_nav.html
··· 1 + <style> 2 + .date-nav { 3 + display: flex; 4 + align-items: center; 5 + gap: 0.5rem; 6 + } 7 + 8 + .date-nav-btn { 9 + display: flex; 10 + align-items: center; 11 + justify-content: center; 12 + padding: 0.5rem 0.75rem; 13 + background: #f3f4f6; 14 + border: 1px solid #d1d5db; 15 + border-radius: 6px; 16 + color: #374151; 17 + font-size: 1rem; 18 + cursor: pointer; 19 + text-decoration: none; 20 + transition: background 0.15s, border-color 0.15s; 21 + } 22 + 23 + .date-nav-btn:hover { 24 + background: #e5e7eb; 25 + border-color: #9ca3af; 26 + } 27 + 28 + .date-nav-btn:active { 29 + transform: scale(0.98); 30 + } 31 + 32 + .date-nav-btn.today { 33 + background: #2563eb; 34 + border-color: #2563eb; 35 + color: white; 36 + font-weight: 600; 37 + } 38 + 39 + .date-nav-btn.today:hover { 40 + background: #1d4ed8; 41 + border-color: #1d4ed8; 42 + } 43 + 44 + .date-nav-btn.today.current { 45 + background: #9ca3af; 46 + border-color: #9ca3af; 47 + cursor: default; 48 + opacity: 0.7; 49 + } 50 + 51 + .date-nav-btn.today.current:hover { 52 + background: #9ca3af; 53 + border-color: #9ca3af; 54 + } 55 + 56 + .date-nav-picker { 57 + position: relative; 58 + display: flex; 59 + align-items: center; 60 + } 61 + 62 + .date-nav-picker input[type="date"] { 63 + position: absolute; 64 + opacity: 0; 65 + width: 100%; 66 + height: 100%; 67 + cursor: pointer; 68 + top: 0; 69 + left: 0; 70 + } 71 + 72 + .date-nav-picker .date-nav-btn { 73 + pointer-events: none; 74 + } 75 + </style> 76 + 77 + <div class="date-nav" id="date-nav"> 78 + <button class="date-nav-btn prev" id="date-nav-prev" title="Previous day (←)">←</button> 79 + <div class="date-nav-picker" title="Pick date"> 80 + <input type="date" id="date-nav-picker"> 81 + <span class="date-nav-btn">🗓️</span> 82 + </div> 83 + <button class="date-nav-btn today" id="date-nav-today" title="Today (t)">Today</button> 84 + <button class="date-nav-btn next" id="date-nav-next" title="Next day (→)">→</button> 85 + </div> 86 + 87 + <script> 88 + (function() { 89 + // Get day from template context or URL 90 + const pathParts = window.location.pathname.split('/'); 91 + let currentDay = '{{ day | default("") }}'; 92 + 93 + // Fallback: try to parse from URL if not provided 94 + if (!currentDay || !/^\d{8}$/.test(currentDay)) { 95 + const lastPart = pathParts[pathParts.length - 1]; 96 + if (/^\d{8}$/.test(lastPart)) { 97 + currentDay = lastPart; 98 + } else { 99 + // Default to today 100 + currentDay = new Date().toISOString().split('T')[0].replace(/-/g, ''); 101 + } 102 + } 103 + 104 + // Get base URL from app context 105 + const app = '{{ app | default("") }}'; 106 + const baseUrl = app ? `/app/${app}/` : pathParts.slice(0, -1).join('/') + '/'; 107 + 108 + // Date utilities 109 + function toInputFormat(day) { 110 + return `${day.substring(0, 4)}-${day.substring(4, 6)}-${day.substring(6, 8)}`; 111 + } 112 + 113 + function fromInputFormat(str) { 114 + return str.replace(/-/g, ''); 115 + } 116 + 117 + function adjustDay(day, delta) { 118 + const year = parseInt(day.substring(0, 4)); 119 + const month = parseInt(day.substring(4, 6)) - 1; 120 + const dayNum = parseInt(day.substring(6, 8)); 121 + const date = new Date(year, month, dayNum); 122 + date.setDate(date.getDate() + delta); 123 + 124 + const newYear = date.getFullYear(); 125 + const newMonth = String(date.getMonth() + 1).padStart(2, '0'); 126 + const newDay = String(date.getDate()).padStart(2, '0'); 127 + return `${newYear}${newMonth}${newDay}`; 128 + } 129 + 130 + function getTodayDay() { 131 + const now = new Date(); 132 + const year = now.getFullYear(); 133 + const month = String(now.getMonth() + 1).padStart(2, '0'); 134 + const day = String(now.getDate()).padStart(2, '0'); 135 + return `${year}${month}${day}`; 136 + } 137 + 138 + function isToday(day) { 139 + return day === getTodayDay(); 140 + } 141 + 142 + // Navigation 143 + function navigate(day) { 144 + window.location.href = `${baseUrl}${day}`; 145 + } 146 + 147 + // Initialize 148 + const picker = document.getElementById('date-nav-picker'); 149 + const todayBtn = document.getElementById('date-nav-today'); 150 + const prevBtn = document.getElementById('date-nav-prev'); 151 + const nextBtn = document.getElementById('date-nav-next'); 152 + 153 + // Set initial picker value 154 + picker.value = toInputFormat(currentDay); 155 + 156 + // Mark today button as current if applicable 157 + if (isToday(currentDay)) { 158 + todayBtn.classList.add('current'); 159 + } 160 + 161 + // Event handlers 162 + prevBtn.addEventListener('click', () => navigate(adjustDay(currentDay, -1))); 163 + nextBtn.addEventListener('click', () => navigate(adjustDay(currentDay, 1))); 164 + 165 + todayBtn.addEventListener('click', () => { 166 + if (!isToday(currentDay)) { 167 + navigate(getTodayDay()); 168 + } 169 + }); 170 + 171 + picker.addEventListener('change', (e) => { 172 + const newDay = fromInputFormat(e.target.value); 173 + if (newDay && newDay !== currentDay) { 174 + navigate(newDay); 175 + } 176 + }); 177 + 178 + // Keyboard shortcuts 179 + document.addEventListener('keydown', (e) => { 180 + // Don't trigger when typing in inputs 181 + if (e.target.matches('input, textarea, select')) return; 182 + 183 + if (e.key === 'ArrowLeft') { 184 + e.preventDefault(); 185 + navigate(adjustDay(currentDay, -1)); 186 + } 187 + if (e.key === 'ArrowRight') { 188 + e.preventDefault(); 189 + navigate(adjustDay(currentDay, 1)); 190 + } 191 + if (e.key === 't' || e.key === 'T') { 192 + e.preventDefault(); 193 + navigate(getTodayDay()); 194 + } 195 + }); 196 + })(); 197 + </script>
-16
convey/templates/macros.html
··· 1 - {% macro day_heading(title, prev_url=None, next_url=None, transcript_url=None, admin_url=None, todos_url=None, today_url=None, screens_url=None) %} 2 - <h1 style="display: flex; justify-content: space-between; align-items: center;"> 3 - <div> 4 - {% if prev_url %}<a href="{{ prev_url }}" aria-label="Previous day" style="text-decoration:none">&#9664;</a>{% endif %} 5 - {{ title }} 6 - {% if next_url %}<a href="{{ next_url }}" aria-label="Next day" style="text-decoration:none">&#9654;</a>{% endif %} 7 - </div> 8 - <div style="display: flex; gap: 10px;"> 9 - {% if today_url %}<a href="{{ today_url }}" title="Today" aria-label="Jump to today" class="day-heading-today">Today</a>{% endif %} 10 - {% if todos_url %}<a href="{{ todos_url }}" title="TODOs" aria-label="TODOs" style="text-decoration:none">✅</a>{% endif %} 11 - {% if transcript_url %}<a href="{{ transcript_url }}" title="Transcripts" aria-label="Transcripts" style="text-decoration:none">💬</a>{% endif %} 12 - {% if screens_url %}<a href="{{ screens_url }}" title="Screen Debug" aria-label="Screen Debug" style="text-decoration:none">🎬</a>{% endif %} 13 - {% if admin_url %}<a href="{{ admin_url }}" title="Admin" aria-label="Admin" style="text-decoration:none">⚙️</a>{% endif %} 14 - </div> 15 - </h1> 16 - {% endmacro %}