Skip to main content

max / goingson

UX audit: onboarding hints, timer icon, bulk project/priority - Add one-time hints: "Press ? for shortcuts" after first session, "Shift-click to select range" on first bulk selection - Show green play icon on started task rows as time tracking entry point - Add "Set Project" and "Set Priority" buttons to task bulk actions bar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-03 13:06 UTC
Commit: 2d4b9e18735b4b72f2dd66e4ebf9087f26c11745
Parent: ba6d5eb
7 files changed, +171 insertions, -5 deletions
@@ -139,8 +139,8 @@ Audit run: `/use-fuzz GoingsOn`. Overall grade: B+. Items already tracked elsewh
139 139 - [x] Add overflow/kebab icon on hover for item rows — surfaces context menu actions without right-click
140 140 - [x] Add visible Quick Add button or input in Tasks header — `q` shortcut is invisible
141 141 - [x] Add "Create Event" to email right-click context menu — parity with existing "Create Task"
142 - - [ ] Add one-time onboarding hints: "Press ? for shortcuts" on first launch, "Shift-click to select range" on first bulk selection
143 - - [ ] Add play/timer icon to task rows for started tasks — time tracking entry point is buried
142 + - [x] Add one-time onboarding hints: "Press ? for shortcuts" on first launch, "Shift-click to select range" on first bulk selection
143 + - [x] Add play/timer icon to task rows for started tasks — time tracking entry point is buried
144 144 - [ ] Add touch gesture hints on first mobile use (long-press, swipe, pull-to-refresh)
145 145
146 146 ### Learnability (B)
@@ -167,7 +167,7 @@ Audit run: `/use-fuzz GoingsOn`. Overall grade: B+. Items already tracked elsewh
167 167 - [ ] Manual time entry — log time retroactively, not just live timer
168 168 - [ ] Contacts export to vCard — import exists but no export (asymmetric)
169 169 - [ ] Bulk operations for contacts (tag, delete) and events (delete)
170 - - [ ] Bulk "Set Project" and "Set Priority" in task bulk actions bar
170 + - [x] Bulk "Set Project" and "Set Priority" in task bulk actions bar
171 171 - [ ] Daily review notes: persist to SQLite + sync (currently localStorage only, lost on reinstall)
172 172 - [ ] Monthly review: add explicit "Complete Review" action (weekly has it, monthly does not)
173 173 - [ ] Workload guardrails in day planner — warn when scheduled hours exceed target
@@ -1561,6 +1561,30 @@ body {
1561 1561 margin-bottom: 1.25rem;
1562 1562 }
1563 1563
1564 + .form-more-toggle {
1565 + display: block;
1566 + background: none;
1567 + border: none;
1568 + cursor: pointer;
1569 + font-size: 0.85rem;
1570 + font-weight: 600;
1571 + color: var(--accent-blue);
1572 + padding: 0.25rem 0;
1573 + margin-bottom: 0.75rem;
1574 + }
1575 +
1576 + .form-more-toggle::before {
1577 + content: '+ ';
1578 + }
1579 +
1580 + .form-more-toggle.expanded::before {
1581 + content: '- ';
1582 + }
1583 +
1584 + .form-extended-fields.hidden {
1585 + display: none;
1586 + }
1587 +
1564 1588 .form-label {
1565 1589 display: block;
1566 1590 font-size: 0.9rem;
@@ -2271,6 +2295,15 @@ kbd {
2271 2295 outline-offset: -2px;
2272 2296 }
2273 2297
2298 + .context-menu-header {
2299 + font-size: 0.7rem;
2300 + font-weight: 700;
2301 + color: var(--text-secondary);
2302 + text-transform: uppercase;
2303 + letter-spacing: 0.05em;
2304 + padding: 0.5rem 1rem 0.25rem;
2305 + }
2306 +
2274 2307 .context-menu-item-icon {
2275 2308 width: 1.25rem;
2276 2309 text-align: center;
@@ -2281,6 +2314,13 @@ kbd {
2281 2314 flex: 1;
2282 2315 }
2283 2316
2317 + .context-menu-item-subtitle {
2318 + display: block;
2319 + font-size: 0.7rem;
2320 + color: var(--text-secondary);
2321 + font-weight: normal;
2322 + }
2323 +
2284 2324 .context-menu-item-shortcut {
2285 2325 font-size: 0.75rem;
2286 2326 color: var(--text-muted);
@@ -3051,6 +3091,29 @@ kbd {
3051 3091 margin-right: 0.5rem;
3052 3092 }
3053 3093
3094 + .task-kebab-btn {
3095 + background: none;
3096 + border: none;
3097 + cursor: pointer;
3098 + font-size: 1.1rem;
3099 + line-height: 1;
3100 + padding: 0.2rem 0.4rem;
3101 + border-radius: var(--radius-sm);
3102 + color: var(--text-secondary);
3103 + opacity: 0;
3104 + transition: opacity 0.15s ease;
3105 + }
3106 +
3107 + .task-row:hover .task-kebab-btn,
3108 + .task-row:focus-within .task-kebab-btn {
3109 + opacity: 1;
3110 + }
3111 +
3112 + .task-kebab-btn:hover {
3113 + background: var(--bg-hover);
3114 + color: var(--text-primary);
3115 + }
3116 +
3054 3117 .task-recurrence {
3055 3118 font-size: 0.85rem;
3056 3119 color: var(--text-secondary);
@@ -6050,6 +6113,10 @@ button.milestone-reorder-btn.btn {
6050 6113 display: none;
6051 6114 }
6052 6115
6116 + .task-kebab-btn {
6117 + opacity: 1;
6118 + }
6119 +
6053 6120 /* --- Mobile Sort & Filter --- */
6054 6121 .mobile-sort-bar {
6055 6122 display: flex;
@@ -6800,6 +6867,24 @@ button.milestone-reorder-btn.btn {
6800 6867 border-color: var(--accent-red);
6801 6868 }
6802 6869
6870 + /* Play icon for started tasks — entry point into time tracking */
6871 + .task-started-icon {
6872 + display: inline-block;
6873 + width: 0;
6874 + height: 0;
6875 + border-style: solid;
6876 + border-width: 5px 0 5px 8px;
6877 + border-color: transparent transparent transparent var(--accent-green, #22c55e);
6878 + margin-right: 0.375rem;
6879 + vertical-align: middle;
6880 + cursor: pointer;
6881 + opacity: 0.8;
6882 + flex-shrink: 0;
6883 + }
6884 + .task-started-icon:hover {
6885 + opacity: 1;
6886 + }
6887 +
6803 6888 /* Pulsing indicator for running timer */
6804 6889 .task-timer-active {
6805 6890 display: inline-block;
@@ -54,12 +54,15 @@
54 54 <button class="view-toggle-btn active" data-mode="list" onclick="GoingsOn.tasks.setViewMode('list')">List</button>
55 55 <button class="view-toggle-btn" data-mode="board" onclick="GoingsOn.tasks.setViewMode('board')">Board</button>
56 56 </div>
57 + <button class="btn btn-secondary" onclick="GoingsOn.keyboard.openQuickAddModal()" title="Quick add (q)">Quick Add</button>
57 58 <button class="btn btn-primary" onclick="GoingsOn.tasks.openNew()">+ New Task</button>
58 59 </div>
59 60 </div>
60 61 <div id="task-bulk-actions" class="bulk-actions-bar hidden" role="toolbar" aria-label="Bulk task actions">
61 62 <span id="task-bulk-count" class="bulk-count">0 selected</span>
62 63 <button class="btn btn-sm" onclick="GoingsOn.bulk.completeTasks()">Complete</button>
64 + <button class="btn btn-sm" onclick="GoingsOn.bulk.setProjectTasks()">Set Project</button>
65 + <button class="btn btn-sm" onclick="GoingsOn.bulk.setPriorityTasks()">Set Priority</button>
63 66 <button class="btn btn-sm" onclick="GoingsOn.bulk.snoozeTasks()">Snooze</button>
64 67 <button class="btn btn-sm" onclick="GoingsOn.bulk.deleteTasks()">Delete</button>
65 68 <button class="btn btn-sm bulk-select-all" onclick="GoingsOn.bulk.selectAllTasks()">Select All</button>
@@ -114,7 +117,7 @@
114 117 Project <span class="sort-arrow"></span>
115 118 </div>
116 119 <div class="task-cell sortable" data-sort="priority" onclick="GoingsOn.tasks.sort('priority')" role="columnheader" tabindex="0" aria-label="Priority">
117 - Pri <span class="sort-arrow"></span>
120 + Priority <span class="sort-arrow"></span>
118 121 </div>
119 122 <div class="task-cell sortable" data-sort="due" onclick="GoingsOn.tasks.sort('due')" role="columnheader" tabindex="0">
120 123 Due <span class="sort-arrow"></span>
@@ -216,6 +219,7 @@
216 219 <button class="pill active" data-subview="day-plan">Day</button>
217 220 <button class="pill" data-subview="weekly-review">Week</button>
218 221 <button class="pill" data-subview="monthly-review">Month</button>
222 + <button class="pill" data-subview="events">Events</button>
219 223 <button class="pill" data-subview="timer">Timer</button>
220 224 </div>
221 225
@@ -236,6 +240,7 @@
236 240 <div class="day-plan-content">
237 241 <div class="day-plan-main" id="day-plan-container">
238 242 <div class="timeline-container" id="timeline-container">
243 + <p class="timeline-hint" style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin: 0.25rem 0 0;">Drag across time slots to block time</p>
239 244 <div class="timeline-scroll-area">
240 245 <div class="timeline-current-time" id="timeline-current-time"></div>
241 246 <div id="timeline-slots"></div>
@@ -50,6 +50,9 @@ document.addEventListener('DOMContentLoaded', async () => {
50 50 // First-run welcome
51 51 if (!localStorage.getItem('go-welcomed')) {
52 52 showWelcome();
53 + } else if (!localStorage.getItem('go-hint-shortcuts')) {
54 + // One-time hint after first session
55 + setTimeout(() => showHint('go-hint-shortcuts', 'Press ? anytime to see keyboard shortcuts'), 2000);
53 56 }
54 57
55 58 // Check weekly review nudge on startup
@@ -264,12 +267,22 @@ function showWelcome() {
264 267 localStorage.setItem('go-welcomed', '1');
265 268 }
266 269
270 + /**
271 + * Show a one-time dismissible hint toast. Sets localStorage key so it only shows once.
272 + */
273 + function showHint(storageKey, message) {
274 + if (localStorage.getItem(storageKey)) return;
275 + localStorage.setItem(storageKey, '1');
276 + GoingsOn.ui.showToast(message, 'info', { duration: 5000 });
277 + }
278 +
267 279 GoingsOn.app = {
268 280 toggleSidebar,
269 281 syncAllEmailAccounts,
270 282 openAboutModal,
271 283 refreshCurrentViewData,
272 284 showWelcome,
285 + showHint,
273 286 };
274 287
275 288 })();
@@ -145,6 +145,58 @@
145 145 });
146 146 }
147 147
148 + async function setProjectTasks() {
149 + const selectedTaskIds = GoingsOn.tasks?.getSelected?.() || new Set();
150 + if (selectedTaskIds.size === 0) return;
151 +
152 + const projects = GoingsOn.projects?.getCache?.() || [];
153 + let optionsHtml = `<p style="margin-bottom: 0.75rem; color: var(--text-secondary);">Set project for ${selectedTaskIds.size} tasks:</p>`;
154 + optionsHtml += `<button class="btn btn-sm btn-ghost" style="width: 100%; text-align: left; margin-bottom: 0.25rem;" onclick="GoingsOn.bulk._applyProject(null)">No Project</button>`;
155 + for (const p of projects) {
156 + optionsHtml += `<button class="btn btn-sm btn-ghost" style="width: 100%; text-align: left; margin-bottom: 0.25rem;" onclick="GoingsOn.bulk._applyProject('${GoingsOn.utils.escapeAttr(p.id)}')">${GoingsOn.utils.escapeHtml(p.name)}</button>`;
157 + }
158 + GoingsOn.ui.openModal('Set Project', `<div style="max-height: 300px; overflow-y: auto;">${optionsHtml}</div>`);
159 + }
160 +
161 + async function setPriorityTasks() {
162 + const selectedTaskIds = GoingsOn.tasks?.getSelected?.() || new Set();
163 + if (selectedTaskIds.size === 0) return;
164 +
165 + const content = `
166 + <p style="margin-bottom: 0.75rem; color: var(--text-secondary);">Set priority for ${selectedTaskIds.size} tasks:</p>
167 + <div style="display: flex; gap: 0.5rem;">
168 + <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('High')">High</button>
169 + <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('Medium')">Medium</button>
170 + <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('Low')">Low</button>
171 + </div>
172 + `;
173 + GoingsOn.ui.openModal('Set Priority', content);
174 + }
175 +
176 + async function _applyProject(projectId) {
177 + const selectedTaskIds = GoingsOn.tasks?.getSelected?.() || new Set();
178 + await executeBulkAction({
179 + selectedIds: selectedTaskIds,
180 + apiCall: id => GoingsOn.api.tasks.update(id, { projectId }),
181 + successMessage: 'Updated project for {count} tasks',
182 + errorMessage: 'Failed to update some tasks',
183 + reloadFn: () => GoingsOn.tasks.load(),
184 + closeModalAfter: true
185 + });
186 + }
187 +
188 + async function _applyPriority(priority) {
189 + const selectedTaskIds = GoingsOn.tasks?.getSelected?.() || new Set();
190 + await executeBulkAction({
191 + selectedIds: selectedTaskIds,
192 + apiCall: id => GoingsOn.api.tasks.update(id, { priority }),
193 + successMessage: 'Updated priority for {count} tasks',
194 + errorMessage: 'Failed to update some tasks',
195 + reloadFn: () => GoingsOn.tasks.load(),
196 + closeModalAfter: true
197 + });
198 + }
199 +
148 200 // ============ Email Bulk Actions ============
149 201
150 202 async function archiveEmails() {
@@ -220,6 +272,8 @@
220 272 completeTasks,
221 273 deleteTasks,
222 274 snoozeTasks,
275 + setProjectTasks,
276 + setPriorityTasks,
223 277 selectAllTasks,
224 278 // Emails
225 279 archiveEmails,
@@ -229,6 +283,8 @@
229 283 selectAllEmails,
230 284 // Internal
231 285 _snoozeCallback: null,
286 + _applyProject,
287 + _applyPriority,
232 288 };
233 289
234 290 })();
@@ -100,6 +100,11 @@ class SelectionManager {
100 100 this.lastClickedIndex = currentIndex;
101 101 this.lastClickedId = id;
102 102 this.updateBulkActionsBar();
103 +
104 + // One-time hint about shift-click range selection
105 + if (this.selectedIds.size === 1 && GoingsOn.app?.showHint) {
106 + GoingsOn.app.showHint('go-hint-shift-select', 'Shift-click to select a range of items');
107 + }
103 108 }
104 109
105 110 /**
@@ -84,6 +84,7 @@
84 84 const progress = t.subtaskProgress ?? 0;
85 85 const displayDesc = t.displayDescription || t.description;
86 86 const isSelected = GoingsOn.tasks.selection.isSelected(t.id);
87 + const isStarted = t.status === 'Started';
87 88
88 89 return `
89 90 <div class="task-row task-${t.status.toLowerCase()} ${t.isSnoozed ? 'task-snoozed' : ''} ${t.isOverdue ? 'task-overdue' : ''} ${isSelected ? 'selected' : ''}"
@@ -91,6 +92,7 @@
91 92 oncontextmenu="GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')"
92 93 tabindex="0" role="row">
93 94 <div class="task-cell task-description" onclick="GoingsOn.tasks.openSubtasks('${escAttr(t.id)}')">
95 + ${isStarted ? `<span class="task-started-icon" title="Started - click to track time" onclick="event.stopPropagation(); GoingsOn.timeTracking.startTimer('${escAttr(t.id)}')"></span>` : ''}
94 96 <span class="task-description-text">${esc(displayDesc)}</span>
95 97 ${renderTimeBadge(t)}
96 98 ${renderTaskBadges(t)}
@@ -115,7 +117,7 @@
115 117 ${isSelected ? 'checked' : ''}
116 118 onchange="GoingsOn.tasks.toggleSelection('${escAttr(t.id)}', this, event)"
117 119 aria-label="Select task">
118 - <button class="btn btn-sm btn-secondary" onclick="GoingsOn.tasks.openActions('${escAttr(t.id)}')" title="Actions" aria-label="Task actions">...</button>
120 + <button class="task-kebab-btn" onclick="GoingsOn.contextMenus.showTask(event, '${escAttr(t.id)}')" title="Actions" aria-label="Task actions">&#x22EE;</button>
119 121 </div>
120 122 </div>
121 123 `;