Skip to main content

max / goingson

Say what the review pane is doing Reflection leads the weekly finish-review modal instead of sitting under the events recap. Days Off toggles carry aria-pressed, a check mark, and a stateful title rather than fill colour alone. Suggested focus tasks render as slot-shaped selector chips, not as action buttons. Reflection autosave flashes "Draft saved" in the card header for day, week, and month. Each review modal states what its terminal button does, beside the button and as its title; a confirm dialog would have torn down the open modal and its unsaved notes.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-29 16:27 UTC
Signed with PGP, not checked
Commit: d9e2628af59442c66b0e8dbe8dff91ca14c870df
Parent: 8ab4189
6 files changed, +160 insertions, -14 deletions
@@ -5400,6 +5400,7 @@
5400 5400 .review-actions-grid,
5401 5401 .focus-section .btn,
5402 5402 .focus-slot .btn,
5403 + .focus-suggestion,
5403 5404 .tab-badge,
5404 5405 .tab-status-dot,
5405 5406 .card-badge,
@@ -5848,6 +5849,7 @@
5848 5849
5849 5850
5850 5851 .vacation-toggle {
5852 + position: relative;
5851 5853 width: 2.5rem;
5852 5854 height: 2.5rem;
5853 5855 border-radius: var(--radius-sm);
@@ -5866,10 +5868,21 @@
5866 5868 background: var(--hover-surface);
5867 5869 }
5868 5870
5871 + /* Pressed reads three ways: fill, inset ring, and the check. Fill alone left
5872 + the marked days indistinguishable to anyone who could not see the color. */
5869 5873 .vacation-toggle.active {
5870 5874 background: var(--category-five);
5871 5875 color: var(--content-on-action);
5872 5876 border-color: var(--category-five);
5877 + box-shadow: inset 0 0 0 var(--border-width) var(--surface-raised);
5878 + }
5879 +
5880 + .vacation-toggle-mark {
5881 + position: absolute;
5882 + right: 0.15rem;
5883 + bottom: 0.05rem;
5884 + font-size: var(--font-size-xxs);
5885 + line-height: 1;
5873 5886 }
5874 5887
5875 5888 /* Vacation Timeline Day */
@@ -6066,6 +6079,49 @@
6066 6079 background: linear-gradient(135deg, var(--surface-raised) 0%, color-mix(in srgb, var(--warning) 10%, var(--surface-raised)) 100%);
6067 6080 }
6068 6081
6082 + .focus-slot-remove {
6083 + margin-top: auto;
6084 + align-self: flex-start;
6085 + }
6086 +
6087 + /* Suggested focus tasks are choices, not commands: a slot-shaped chip with the
6088 + same dashed frame as an empty slot, so picking one reads as filling the slot
6089 + rather than as firing an action. */
6090 + .focus-suggestions {
6091 + display: flex;
6092 + flex-wrap: wrap;
6093 + gap: var(--gap-peer);
6094 + }
6095 +
6096 + .focus-suggestion {
6097 + display: inline-flex;
6098 + align-items: center;
6099 + gap: var(--gap-bound);
6100 + padding: var(--gap-bound) var(--gap-peer);
6101 + border: var(--border-width) dashed var(--border);
6102 + border-radius: var(--radius-md);
6103 + background: var(--surface-overlay);
6104 + color: var(--content-secondary);
6105 + font-family: var(--font-sans);
6106 + font-size: var(--font-size-sm);
6107 + font-weight: 600;
6108 + cursor: pointer;
6109 + text-align: left;
6110 + }
6111 +
6112 + .focus-suggestion:hover {
6113 + border-style: solid;
6114 + border-color: var(--action);
6115 + background: var(--surface-sunken);
6116 + color: var(--content);
6117 + }
6118 +
6119 + .focus-suggestion-mark {
6120 + font-size: var(--font-size-md);
6121 + line-height: 1;
6122 + color: var(--action);
6123 + }
6124 +
6069 6125 /* Legacy aliases, keep so existing JS selectors and styles still apply.
6070 6126 New code should use the .scope-slot* classes. */
6071 6127 .focus-grid { /* alias of .scope-slots */ }
@@ -6105,6 +6161,25 @@
6105 6161 }
6106 6162
6107 6163
6164 + .reflection-card-header {
6165 + display: flex;
6166 + align-items: baseline;
6167 + justify-content: space-between;
6168 + gap: var(--gap-peer);
6169 + }
6170 +
6171 + /* Autosave feedback: silent while idle, one flash on write. Held in the layout
6172 + at all times so the header does not shift when it appears. */
6173 + .reflection-autosave {
6174 + font-size: var(--font-size-xs);
6175 + color: var(--content-muted);
6176 + visibility: hidden;
6177 + }
6178 +
6179 + .reflection-autosave.visible {
6180 + visibility: visible;
6181 + }
6182 +
6108 6183 .reflection-prompt {
6109 6184 padding: var(--gap-group);
6110 6185 background: var(--surface-overlay);
@@ -6129,11 +6204,31 @@
6129 6204 .review-actions-grid {
6130 6205 grid-column: 1 / -1;
6131 6206 display: flex;
6207 + align-items: center;
6132 6208 justify-content: flex-end;
6133 6209 gap: var(--gap-peer);
6134 6210 padding-top: var(--gap-section);
6135 6211 }
6136 6212
6213 + /* Says what the terminal button does before it is pressed. */
6214 + .review-action-hint {
6215 + margin: 0 auto 0 0;
6216 + max-width: 34rem;
6217 + font-size: var(--font-size-sm);
6218 + color: var(--content-secondary);
6219 + }
6220 +
6221 + /* Too narrow to sit beside the button, so the hint takes its own line above. */
6222 + .ui-mode-mobile .review-actions-grid {
6223 + flex-direction: column;
6224 + align-items: stretch;
6225 + }
6226 +
6227 + .ui-mode-mobile .review-action-hint {
6228 + margin: 0;
6229 + max-width: none;
6230 + }
6231 +
6137 6232 /* Event items */
6138 6233 .event-item {
6139 6234 display: flex;
@@ -301,12 +301,17 @@
301 301 ? `<div class="past-review-banner">You are reviewing a past day (${esc(displayDate)}), not today.</div>`
302 302 : '';
303 303
304 + const saveHint = isPast
305 + ? 'Saves your notes against this past day and marks it reviewed.'
306 + : 'Saves your notes and marks today reviewed. Nothing else closes or resets.';
307 +
304 308 const content = `
305 309 <div class="finish-review-modal-content">
306 310 ${banner}
307 311 ${reflectionHtml}
308 312 <div class="review-actions-grid">
309 - <button type="button" class="btn btn-primary" data-act="dayPlan.saveDailyReview">
313 + <p class="review-action-hint">${esc(saveHint)}</p>
314 + <button type="button" class="btn btn-primary" data-act="dayPlan.saveDailyReview" title="${escAttr(saveHint)}">
310 315 ${isReviewed ? 'Update Review' : 'Save Review'}
311 316 </button>
312 317 </div>
@@ -139,12 +139,17 @@
139 139 ? `<div class="past-review-banner">You are reviewing a past month (${GoingsOn.utils.escapeHtml(r.monthDisplay)}), not the current one.</div>`
140 140 : '';
141 141
142 + const completeHint = isPast
143 + ? 'Saves your notes against this past month and marks it reviewed.'
144 + : 'Saves your notes and marks this month reviewed. Nothing else closes or resets.';
145 +
142 146 const content = `
143 147 <div class="finish-review-modal-content">
144 148 ${banner}
145 149 ${R.renderReflection(r)}
146 150 <div class="review-actions-grid">
147 - <button class="btn btn-primary" data-act="monthlyReview.complete">
151 + <p class="review-action-hint">${GoingsOn.utils.escapeHtml(completeHint)}</p>
152 + <button class="btn btn-primary" data-act="monthlyReview.complete" title="${GoingsOn.utils.escapeAttrValue(completeHint)}">
148 153 ${isPast ? 'Save Review' : 'Complete Review'}
149 154 </button>
150 155 </div>
@@ -75,12 +75,32 @@
75 75
76 76 return `
77 77 <div class="card card--static review-card reflection-card">
78 - <h3 class="review-card-title">Reflection</h3>
78 + <div class="reflection-card-header">
79 + <h3 class="review-card-title">Reflection</h3>
80 + <span class="reflection-autosave" id="${idPrefix}-reflection-autosave" role="status" aria-live="polite"></span>
81 + </div>
79 82 <div class="reflection-fields">${fields}</div>
80 83 </div>
81 84 `;
82 85 }
83 86
87 + // Per-prefix timers, so two open reflections never clear each other's message.
88 + const draftSavedTimers = {};
89 +
90 + /**
91 + * Flash "Draft saved" in the reflection card header. Typing is saved without
92 + * a save button, so the write needs to say so or it reads as lost.
93 + * @param {string} idPrefix - Same prefix the card was rendered with
94 + */
95 + function markDraftSaved(idPrefix) {
96 + const el = document.getElementById(`${idPrefix}-reflection-autosave`);
97 + if (!el) return;
98 + el.textContent = 'Draft saved';
99 + el.classList.add('visible');
100 + clearTimeout(draftSavedTimers[idPrefix]);
101 + draftSavedTimers[idPrefix] = setTimeout(() => el.classList.remove('visible'), 2500);
102 + }
103 +
84 104 /**
85 105 * Wire input listeners to the reflection textareas. Calls `onChange` with a
86 106 * map of {key: value} whenever the user types (debounced).
@@ -93,6 +113,7 @@
93 113 values[p.key] = el ? el.value : '';
94 114 }
95 115 onChange(values);
116 + markDraftSaved(idPrefix);
96 117 }, debounceMs);
97 118
98 119 for (const p of prompts) {
@@ -261,7 +261,7 @@
261 261 ${task.projectName ? esc(task.projectName) : 'No project'}
262 262 ${task.dueFormatted ? ' &middot; Due ' + task.dueFormatted : ''}
263 263 </span>
264 - <button class="btn btn-sm btn-secondary" style="margin-top: auto; align-self: flex-start;"
264 + <button class="btn btn-sm btn-secondary focus-slot-remove"
265 265 data-act="weeklyReview.toggleFocus" data-a1="${escAttr(task.id)}" data-args='["@a1", false]'>
266 266 Remove
267 267 </button>
@@ -302,11 +302,13 @@
302 302 <h4 class="review-history-heading">
303 303 Suggested tasks to focus on:
304 304 </h4>
305 - <div style="display: flex; gap: var(--gap-peer); flex-wrap: wrap;">
305 + <div class="focus-suggestions">
306 306 ${available.slice(0, 5).map(t => `
307 - <button class="btn btn-sm btn-secondary"
307 + <button class="focus-suggestion"
308 + title="Add &quot;${escAttr(t.title)}&quot; to this week's focus"
308 309 data-act="weeklyReview.toggleFocus" data-a1="${escAttr(t.id)}" data-args='["@a1", true]'>
309 - + ${esc(truncate(t.title, 30))}
310 + <span class="focus-suggestion-mark" aria-hidden="true">+</span>
311 + <span class="focus-suggestion-label">${esc(truncate(t.title, 30))}</span>
310 312 </button>
311 313 `).join('')}
312 314 </div>
@@ -409,13 +411,21 @@
409 411 <span class="card-title">Days Off</span>
410 412 </div>
411 413 <div class="row-flex row-flex-peer">
412 - ${dayLabels.map((label, i) => `
413 - <button class="vacation-toggle ${vacationDays.includes(i) ? 'active' : ''}"
414 + ${dayLabels.map((label, i) => {
415 + const dayName = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'][i];
416 + const isOff = vacationDays.includes(i);
417 + // aria-pressed plus the check mark carry the state without
418 + // relying on the fill color alone.
419 + return `
420 + <button class="vacation-toggle ${isOff ? 'active' : ''}"
421 + aria-pressed="${isOff}"
414 422 data-act="weeklyReview.toggleVacationDay" data-args='[${i}]'
415 - title="${['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'][i]}">
416 - ${label}
423 + title="${dayName}${isOff ? ': day off, click to unmark' : ': working, click to mark as a day off'}">
424 + <span class="vacation-toggle-day">${label}</span>
425 + <span class="vacation-toggle-mark" aria-hidden="true">${isOff ? '&#x2713;' : ''}</span>
417 426 </button>
418 - `).join('')}
427 + `;
428 + }).join('')}
419 429 </div>
420 430 </div>
421 431 `;
@@ -188,18 +188,28 @@
188 188 const wr = GoingsOn.weeklyReviewRender;
189 189 const isPast = currentPeriodState() === 'past';
190 190 const esc = GoingsOn.utils.escapeHtml;
191 + const escAttr = GoingsOn.utils.escapeAttrValue;
191 192
192 193 const banner = isPast
193 194 ? `<div class="past-review-banner">You are reviewing a past week (${esc(r.weekDisplay)}), not the current one.</div>`
194 195 : '';
195 196
197 + const completeHint = r.isCompleted
198 + ? 'This week is already reviewed. Your notes stay editable as a draft.'
199 + : (isPast
200 + ? 'Saves your notes against this past week and marks it reviewed.'
201 + : 'Saves your notes and marks this week reviewed. Nothing else closes or resets.');
202 +
203 + // Reflection leads: it is what the modal is for, and the recap below it
204 + // is reference. Behind the recap it sat under the fold and got skipped.
196 205 const content = `
197 206 <div class="finish-review-modal-content">
198 207 ${banner}
199 - ${wr.renderTimelineEvents(r)}
200 208 ${wr.renderReflection(r, getDraft)}
209 + ${wr.renderTimelineEvents(r)}
201 210 <div class="review-actions-grid">
202 - <button class="btn btn-primary" data-act="weeklyReview.complete" ${r.isCompleted ? 'disabled' : ''}>
211 + <p class="review-action-hint">${esc(completeHint)}</p>
212 + <button class="btn btn-primary" data-act="weeklyReview.complete" title="${escAttr(completeHint)}" ${r.isCompleted ? 'disabled' : ''}>
203 213 ${r.isCompleted ? 'Review Completed' : (isPast ? 'Save Review' : 'Complete Review')}
204 214 </button>
205 215 </div>