max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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 ? ' · 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 "${escAttr(t.title)}" 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 ? '✓' : ''}</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> |