Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

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

Merge pull request 'feat(calendar): add keyboard navigation and mini calendar sidebar' (#319) from feat/calendar-keyboard-nav into main

scott 274aab87 dfbcc1c9

+337 -4
+24 -2
src/calendar/index.html
··· 50 50 <main class="calendar-main" id="main-content"> 51 51 <!-- Calendar toolbar --> 52 52 <div class="cal-toolbar" id="calendar-toolbar"> 53 + <button class="cal-nav-btn cal-sidebar-toggle" id="btn-sidebar-toggle" title="Toggle mini calendar (S)" aria-label="Toggle mini calendar sidebar" aria-expanded="true"> 54 + <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="2" width="14" height="12" rx="1.5"/><line x1="5.5" y1="2" x2="5.5" y2="14"/><line x1="1" y1="6" x2="5.5" y2="6"/></svg> 55 + </button> 53 56 <button class="cal-nav-btn" id="btn-today" title="Go to today (T)">Today</button> 54 57 <button class="cal-nav-btn" id="btn-prev" title="Previous period (&larr;)">&#9664;</button> 55 58 <button class="cal-nav-btn" id="btn-next" title="Next period (&rarr;)">&#9654;</button> ··· 65 68 </div> 66 69 </div> 67 70 68 - <!-- Calendar grid --> 69 - <div class="calendar-grid" id="calendar-grid"></div> 71 + <!-- Calendar body: sidebar + grid --> 72 + <div class="cal-body"> 73 + <!-- Mini calendar sidebar --> 74 + <aside class="cal-sidebar" id="cal-sidebar" aria-label="Mini calendar"> 75 + <div class="cal-mini" id="cal-mini"></div> 76 + <div class="cal-sidebar-shortcuts"> 77 + <div class="cal-shortcuts-title">Keyboard shortcuts</div> 78 + <div class="cal-shortcut"><kbd>T</kbd> Today</div> 79 + <div class="cal-shortcut"><kbd>M</kbd> Month</div> 80 + <div class="cal-shortcut"><kbd>W</kbd> Week</div> 81 + <div class="cal-shortcut"><kbd>D</kbd> Day</div> 82 + <div class="cal-shortcut"><kbd>A</kbd> Agenda</div> 83 + <div class="cal-shortcut"><kbd>N</kbd> New event</div> 84 + <div class="cal-shortcut"><kbd>&larr;</kbd><kbd>&rarr;</kbd> Navigate</div> 85 + <div class="cal-shortcut"><kbd>S</kbd> Sidebar</div> 86 + </div> 87 + </aside> 88 + 89 + <!-- Calendar grid --> 90 + <div class="calendar-grid" id="calendar-grid"></div> 91 + </div> 70 92 </main> 71 93 72 94 <!-- Event modal -->
+144 -2
src/calendar/main.ts
··· 106 106 const titleInput = document.getElementById('calendar-title') as HTMLInputElement; 107 107 const dateLabel = document.getElementById('current-label')!; 108 108 const modalBackdrop = document.getElementById('event-modal-backdrop') as HTMLElement; 109 + const calSidebar = document.getElementById('cal-sidebar') as HTMLElement; 110 + const calMini = document.getElementById('cal-mini') as HTMLElement; 111 + const sidebarToggle = document.getElementById('btn-sidebar-toggle') as HTMLButtonElement; 109 112 110 113 // Modal form fields 111 114 const modalTitle = document.getElementById('event-title') as HTMLInputElement; ··· 282 285 283 286 function renderView(): void { 284 287 updateDateLabel(); 288 + syncMiniToState(); 289 + renderMiniCalendar(); 285 290 switch (state.view) { 286 291 case 'month': renderMonthView(); break; 287 292 case 'week': renderWeekView(); break; ··· 881 886 }); 882 887 883 888 // --------------------------------------------------------------------------- 889 + // Mini Calendar Sidebar 890 + // --------------------------------------------------------------------------- 891 + 892 + /** Month displayed in the mini calendar (independent of main view). */ 893 + let miniMonth = state.currentDate.getMonth(); 894 + let miniYear = state.currentDate.getFullYear(); 895 + 896 + function syncMiniToState(): void { 897 + miniMonth = state.currentDate.getMonth(); 898 + miniYear = state.currentDate.getFullYear(); 899 + } 900 + 901 + function renderMiniCalendar(): void { 902 + const firstDay = new Date(miniYear, miniMonth, 1).getDay(); 903 + const totalDays = daysInMonth(miniYear, miniMonth); 904 + const prevDays = daysInMonth(miniYear, miniMonth - 1); 905 + const totalCells = Math.ceil((firstDay + totalDays) / 7) * 7; 906 + 907 + let html = '<div class="cal-mini-header">'; 908 + html += `<button class="cal-mini-nav" id="mini-prev" aria-label="Previous month">&#9664;</button>`; 909 + html += `<span class="cal-mini-title">${MONTHS_SHORT[miniMonth]} ${miniYear}</span>`; 910 + html += `<button class="cal-mini-nav" id="mini-next" aria-label="Next month">&#9654;</button>`; 911 + html += '</div>'; 912 + 913 + html += '<div class="cal-mini-grid">'; 914 + 915 + // Day-of-week headers (single letter) 916 + const dayLetters = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; 917 + for (const letter of dayLetters) { 918 + html += `<div class="cal-mini-dow">${letter}</div>`; 919 + } 920 + 921 + for (let i = 0; i < totalCells; i++) { 922 + const dayNum = i - firstDay + 1; 923 + let cellDate: Date; 924 + let otherMonth = false; 925 + let displayNum: number; 926 + 927 + if (dayNum < 1) { 928 + displayNum = prevDays + dayNum; 929 + cellDate = new Date(miniYear, miniMonth - 1, displayNum); 930 + otherMonth = true; 931 + } else if (dayNum > totalDays) { 932 + displayNum = dayNum - totalDays; 933 + cellDate = new Date(miniYear, miniMonth + 1, displayNum); 934 + otherMonth = true; 935 + } else { 936 + displayNum = dayNum; 937 + cellDate = new Date(miniYear, miniMonth, dayNum); 938 + } 939 + 940 + const dateStr = formatDate(cellDate); 941 + const todayCls = isToday(cellDate) ? ' cal-mini-today' : ''; 942 + const otherCls = otherMonth ? ' cal-mini-other' : ''; 943 + const selectedCls = isSameDay(cellDate, state.currentDate) ? ' cal-mini-selected' : ''; 944 + const dayEvents = eventsOnDate(dateStr); 945 + const hasDot = dayEvents.length > 0 ? ' cal-mini-has-events' : ''; 946 + 947 + html += `<button class="cal-mini-day${todayCls}${otherCls}${selectedCls}${hasDot}" data-mini-date="${dateStr}" type="button">`; 948 + html += `${displayNum}`; 949 + if (dayEvents.length > 0) { 950 + html += '<span class="cal-mini-dot"></span>'; 951 + } 952 + html += '</button>'; 953 + } 954 + 955 + html += '</div>'; 956 + calMini.innerHTML = html; 957 + 958 + // Wire mini calendar nav 959 + document.getElementById('mini-prev')?.addEventListener('click', (e) => { 960 + e.stopPropagation(); 961 + miniMonth--; 962 + if (miniMonth < 0) { miniMonth = 11; miniYear--; } 963 + renderMiniCalendar(); 964 + }); 965 + 966 + document.getElementById('mini-next')?.addEventListener('click', (e) => { 967 + e.stopPropagation(); 968 + miniMonth++; 969 + if (miniMonth > 11) { miniMonth = 0; miniYear++; } 970 + renderMiniCalendar(); 971 + }); 972 + } 973 + 974 + // Mini calendar date clicks 975 + calMini.addEventListener('click', (e) => { 976 + const btn = (e.target as HTMLElement).closest('[data-mini-date]') as HTMLElement | null; 977 + if (!btn) return; 978 + const dateStr = btn.dataset.miniDate!; 979 + state.currentDate = parseEventDate(dateStr); 980 + syncMiniToState(); 981 + renderView(); 982 + renderMiniCalendar(); 983 + }); 984 + 985 + // Sidebar toggle 986 + let sidebarVisible = window.innerWidth >= 768; 987 + 988 + function updateSidebarVisibility(): void { 989 + calSidebar.classList.toggle('cal-sidebar-hidden', !sidebarVisible); 990 + sidebarToggle.setAttribute('aria-expanded', String(sidebarVisible)); 991 + } 992 + 993 + function toggleSidebar(): void { 994 + sidebarVisible = !sidebarVisible; 995 + localStorage.setItem('tools-cal-sidebar', sidebarVisible ? 'open' : 'closed'); 996 + updateSidebarVisibility(); 997 + } 998 + 999 + // Restore sidebar preference 1000 + const savedSidebar = localStorage.getItem('tools-cal-sidebar'); 1001 + if (savedSidebar === 'closed') { 1002 + sidebarVisible = false; 1003 + } else if (savedSidebar === 'open') { 1004 + sidebarVisible = true; 1005 + } 1006 + // On mobile, always start hidden 1007 + if (window.innerWidth < 768) { 1008 + sidebarVisible = false; 1009 + } 1010 + updateSidebarVisibility(); 1011 + 1012 + sidebarToggle.addEventListener('click', toggleSidebar); 1013 + 1014 + // --------------------------------------------------------------------------- 884 1015 // Navigation button wiring 885 1016 // --------------------------------------------------------------------------- 886 1017 ··· 1034 1165 setView('agenda'); 1035 1166 break; 1036 1167 case 'n': 1168 + case 'Enter': 1037 1169 e.preventDefault(); 1038 - openModal({ date: formatDate(new Date()) }); 1170 + openModal({ date: formatDate(state.currentDate) }); 1171 + break; 1172 + case 's': 1173 + e.preventDefault(); 1174 + toggleSidebar(); 1039 1175 break; 1040 1176 case 'ArrowLeft': 1041 1177 e.preventDefault(); ··· 1086 1222 createCommandPalette({ 1087 1223 actions: [ 1088 1224 { id: 'back', label: 'Back to Documents', category: 'action', icon: '\u2190', action: () => { window.location.href = '/'; } }, 1089 - { id: 'new-event', label: 'New Event', category: 'action', icon: '+', shortcut: 'N', action: () => openModal({ date: formatDate(new Date()) }) }, 1225 + { id: 'new-event', label: 'New Event', category: 'action', icon: '+', shortcut: 'N', action: () => openModal({ date: formatDate(state.currentDate) }) }, 1090 1226 { id: 'today', label: 'Today', category: 'action', icon: '\u25C9', shortcut: 'T', action: goToday }, 1091 1227 { id: 'month-view', label: 'Month View', category: 'action', icon: '\u2588', shortcut: 'M', action: () => setView('month') }, 1092 1228 { id: 'week-view', label: 'Week View', category: 'action', icon: '\u2630', shortcut: 'W', action: () => setView('week') }, 1093 1229 { id: 'day-view', label: 'Day View', category: 'action', icon: '\u25A1', shortcut: 'D', action: () => setView('day') }, 1094 1230 { id: 'agenda-view', label: 'Agenda View', category: 'action', icon: '\u2261', shortcut: 'A', action: () => setView('agenda') }, 1231 + { id: 'toggle-sidebar', label: 'Toggle Sidebar', category: 'action', icon: '\u2759', shortcut: 'S', action: toggleSidebar }, 1095 1232 ], 1096 1233 }); 1097 1234 ··· 1102 1239 function handleResize(): void { 1103 1240 if (window.innerWidth < 768 && state.view !== 'agenda') { 1104 1241 setView('agenda'); 1242 + } 1243 + // Auto-hide sidebar on mobile 1244 + if (window.innerWidth < 768 && sidebarVisible) { 1245 + sidebarVisible = false; 1246 + updateSidebarVisibility(); 1105 1247 } 1106 1248 } 1107 1249
+169
src/css/app.css
··· 9332 9332 min-height: 0; /* allow flex children to shrink below content size */ 9333 9333 } 9334 9334 9335 + /* Calendar body: sidebar + grid wrapper */ 9336 + .cal-body { 9337 + flex: 1; 9338 + display: flex; 9339 + min-height: 0; 9340 + overflow: hidden; 9341 + } 9342 + 9343 + /* ── Mini Calendar Sidebar ──────────────────────────────────────────── */ 9344 + 9345 + .cal-sidebar { 9346 + width: 200px; 9347 + flex-shrink: 0; 9348 + border-right: 1px solid var(--color-border); 9349 + padding: var(--space-sm); 9350 + overflow-y: auto; 9351 + transition: width var(--transition-fast), padding var(--transition-fast), border var(--transition-fast); 9352 + } 9353 + 9354 + .cal-sidebar-hidden { 9355 + width: 0; 9356 + padding: 0; 9357 + border-right: none; 9358 + overflow: hidden; 9359 + } 9360 + 9361 + .cal-mini-header { 9362 + display: flex; 9363 + align-items: center; 9364 + justify-content: space-between; 9365 + margin-bottom: var(--space-xs); 9366 + } 9367 + 9368 + .cal-mini-title { 9369 + font-size: 0.75rem; 9370 + font-weight: 600; 9371 + color: var(--color-text); 9372 + } 9373 + 9374 + .cal-mini-nav { 9375 + background: none; 9376 + border: none; 9377 + color: var(--color-text-muted); 9378 + cursor: pointer; 9379 + font-size: 0.6rem; 9380 + padding: 2px 4px; 9381 + border-radius: var(--radius-sm); 9382 + line-height: 1; 9383 + } 9384 + 9385 + .cal-mini-nav:hover { 9386 + background: var(--color-hover); 9387 + color: var(--color-text); 9388 + } 9389 + 9390 + .cal-mini-grid { 9391 + display: grid; 9392 + grid-template-columns: repeat(7, 1fr); 9393 + gap: 1px; 9394 + } 9395 + 9396 + .cal-mini-dow { 9397 + font-size: 0.55rem; 9398 + color: var(--color-text-muted); 9399 + text-align: center; 9400 + padding: 2px 0; 9401 + font-weight: 600; 9402 + } 9403 + 9404 + .cal-mini-day { 9405 + display: flex; 9406 + flex-direction: column; 9407 + align-items: center; 9408 + justify-content: center; 9409 + font-size: 0.65rem; 9410 + padding: 2px 0; 9411 + border: none; 9412 + background: none; 9413 + color: var(--color-text); 9414 + cursor: pointer; 9415 + border-radius: var(--radius-sm); 9416 + line-height: 1.2; 9417 + position: relative; 9418 + } 9419 + 9420 + .cal-mini-day:hover { 9421 + background: var(--color-hover); 9422 + } 9423 + 9424 + .cal-mini-other { 9425 + color: var(--color-text-muted); 9426 + opacity: 0.5; 9427 + } 9428 + 9429 + .cal-mini-today { 9430 + font-weight: 700; 9431 + color: var(--color-accent); 9432 + } 9433 + 9434 + .cal-mini-selected { 9435 + background: var(--color-accent); 9436 + color: var(--color-on-accent, #fff); 9437 + } 9438 + 9439 + .cal-mini-selected:hover { 9440 + background: var(--color-accent); 9441 + opacity: 0.9; 9442 + } 9443 + 9444 + .cal-mini-dot { 9445 + width: 3px; 9446 + height: 3px; 9447 + border-radius: 50%; 9448 + background: var(--color-accent); 9449 + position: absolute; 9450 + bottom: 1px; 9451 + } 9452 + 9453 + .cal-mini-selected .cal-mini-dot { 9454 + background: var(--color-on-accent, #fff); 9455 + } 9456 + 9457 + /* Keyboard shortcuts reference */ 9458 + .cal-sidebar-shortcuts { 9459 + margin-top: var(--space-md); 9460 + padding-top: var(--space-sm); 9461 + border-top: 1px solid var(--color-border); 9462 + } 9463 + 9464 + .cal-shortcuts-title { 9465 + font-size: 0.6rem; 9466 + font-weight: 600; 9467 + color: var(--color-text-muted); 9468 + text-transform: uppercase; 9469 + letter-spacing: 0.05em; 9470 + margin-bottom: var(--space-xs); 9471 + } 9472 + 9473 + .cal-shortcut { 9474 + font-size: 0.65rem; 9475 + color: var(--color-text-muted); 9476 + padding: 1px 0; 9477 + display: flex; 9478 + align-items: center; 9479 + gap: var(--space-xs); 9480 + } 9481 + 9482 + .cal-shortcut kbd { 9483 + font-family: var(--font-mono, monospace); 9484 + font-size: 0.6rem; 9485 + padding: 0 3px; 9486 + border: 1px solid var(--color-border); 9487 + border-radius: 2px; 9488 + background: var(--color-surface); 9489 + min-width: 1.2em; 9490 + text-align: center; 9491 + } 9492 + 9493 + .cal-sidebar-toggle svg { 9494 + display: block; 9495 + } 9496 + 9497 + /* Hide sidebar on mobile */ 9498 + @media (max-width: 767px) { 9499 + .cal-sidebar { 9500 + display: none; 9501 + } 9502 + } 9503 + 9335 9504 /* Calendar grid container — fills remaining space below toolbar. 9336 9505 overflow: hidden here so each view child handles its own scroll. */ 9337 9506 .calendar-app .calendar-grid {