Skip to content

Commit ee334b3

Browse files
committed
Merge phase-6-tasks-epochs-redesign: Tasks & Epochs clarity redesign (F4 quick wins)
UI/copy-only redesign of the day editor's most-complex screen, per the UX rubric: - decorative emoji removed; glyph status badges replaced with token-colored TEXT labels (the overloaded warning split into "Needs epochs" / "Missing camera"); - a plain-language framing intro (task vs epoch); - optional epoch-linked editors (videos / files / FsGUI) as collapsible sections (empty collapsed, populated open) with an item-count summary and a coupling note; - normalized add-button labels + heading levels. No data-model or export change: day.tasks and the YAML are unchanged (golden baselines byte-identical); the redesigned step has zero jest-axe violations. Full vitest (4575), functional e2e (90), typecheck, and lint all green.
2 parents a08acd8 + 493ff68 commit ee334b3

12 files changed

Lines changed: 293 additions & 113 deletions

‎.claude/docs/plans/design-feedback-remediation/PLAN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Design-Feedback Remediation Implementation Plan
22

3-
**Status:** Phases 1–5 complete (merged into `modern`); Phases 6–10 not started.
3+
**Status:** Phases 1–6 complete (merged into `modern`); Phases 7–10 not started.
44

55
This plan turns six pieces of user design feedback plus a re-evaluated backlog into shippable work:
66
it guarantees recording days stay date-ordered, restores guided DIO entry (a Type + Index control),
@@ -35,7 +35,7 @@ For agent invocation, **load only the slice you need**:
3535
- [phase-3-design-tokens-css-modules.md](phase-3-design-tokens-css-modules.md) — token scale (incl. z-index) + CSS-Modules scaffolding + fix hidden logo/shortcuts (F3). ✅ **Complete (merged)**. F3 grew into a single-row app-bar redesign (logo · nav · shortcuts) per user feedback.
3636
- [phase-4-remove-channel-maps-editor.md](phase-4-remove-channel-maps-editor.md) — delete the workspace channel-maps editor (F1), full reference sweep. ✅ **Complete (merged)**.
3737
- [phase-5-dayeditor-structural-prep.md](phase-5-dayeditor-structural-prep.md) — `DayEditorContext` (kill prop-drill) + shared `deviceOverrideMerge` module; behavior-preserving. ✅ **Complete (merged)**.
38-
- [phase-6-tasks-epochs-redesign.md](phase-6-tasks-epochs-redesign.md) — Tasks & Epochs in-place clarity redesign (F4 quick wins) + a11y.
38+
- [phase-6-tasks-epochs-redesign.md](phase-6-tasks-epochs-redesign.md) — Tasks & Epochs in-place clarity redesign (F4 quick wins) + a11y. ✅ **Complete (merged)**.
3939
- [phase-7-persistence-migration.md](phase-7-persistence-migration.md) — versioned persisted-blob forward migration (Post-v3 #6).
4040
- [phase-8-task-type-catalog.md](phase-8-task-type-catalog.md) — animal-level task-type catalog; days pick/order epochs (F4 full) + trodes_to_nwb integration check.
4141
- [phase-9-architecture-cleanup.md](phase-9-architecture-cleanup.md) — split `validation.js`; decompose `ValidationSummary`/`RecordingDaysTab`; continue TS; **re-arm the build gate**.

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
116116

117117
### Changed
118118

119+
- **Tasks & Epochs screen — clarity redesign (no data-model or export change).** The day editor's
120+
most-complex screen now opens with a plain-language framing that defines a *task* (one activity
121+
in one environment, with its cameras) and an *epoch* (a numbered time block of that task, each
122+
belonging to exactly one task). Decorative emoji are gone, and the glyph status badges (✓/⚠/❌)
123+
are replaced with token-colored **text labels** — the one badge that overloaded two problems is
124+
split into distinct "Needs epochs" and "Missing camera" labels (an error like a reused epoch or a
125+
missing required field reads as text, too). The optional, epoch-linked editors — associated
126+
videos, associated files, and FsGUI protocols — are now collapsible sections (an empty one is
127+
collapsed to reduce noise; a section that already holds data stays open so it is never hidden),
128+
each showing an item count, preceded by a one-line note that they reference a task's epochs and
129+
that editing or deleting a referenced task asks you to confirm before the link is cleared (so the
130+
repair dialog is expected, not a surprise). Add-button labels and section heading levels are
131+
normalized. `day.tasks` and the exported YAML are unchanged (golden baselines byte-identical), and
132+
the redesigned step has zero automated-accessibility (axe) violations.
119133
- **Day Editor structural refactor (internal; no behavior or export change).** Extracted the day
120134
device-override merge — the override > snapshot resolution AND the matching "can this override
121135
be honored cleanly?" shape classification — into one pure module

‎e2e/workspace-mistake-prevention.spec.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ test.describe('Mistake-prevention UX on high-risk edit surfaces', () => {
212212
await expect(page.getByRole('heading', { level: 2, name: 'Tasks & Epochs' })).toBeVisible();
213213

214214
// --- Associated video files: camera + epoch are <select> controls (combobox role). ---
215+
// The seeded day has video rows, so this optional section renders expanded (open-when-populated).
215216
const videos = page.getByRole('region', { name: 'Associated video files' });
216217
await expect(videos).toBeVisible();
217218
// The seeded day has video rows; the FIRST row's camera select is a controlled combobox.

‎e2e/workspace-optogenetics.spec.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ test.describe('Optogenetics export gating and the two-layer opto model', () => {
238238
await page.getByRole('button', { name: /^Tasks & Epochs — / }).click();
239239
await expect(page.getByRole('heading', { level: 2, name: 'Tasks & Epochs' })).toBeVisible();
240240

241+
// The optional FsGUI section is collapsed by default (progressive disclosure) — expand it.
242+
await page.locator('summary', { hasText: 'Optogenetics protocols' }).click();
243+
241244
// The day-level opto protocol editor is present (the two-layer model's "what was actually run").
242245
const fsGui = page.getByRole('region', { name: 'Optogenetics run this day (FsGUI protocols)' });
243246
await expect(fsGui).toBeVisible();

‎src/pages/DayEditor/FsGuiSection.jsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export default function FsGuiSection({ fsGuiYamls, cameras, epochOptions, dioOpt
181181
))}
182182

183183
<button type="button" className="btn-secondary" onClick={addItem}>
184-
Add FsGUI protocol
184+
+ Add FsGUI protocol
185185
</button>
186186
</section>
187187
);

‎src/pages/DayEditor/TasksEpochsStep.jsx‎

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,27 @@ export default function TasksEpochsStep(props) {
297297
.join(', ')
298298
: '';
299299

300+
// The repair prompt's title must name the kinds actually orphaned — a files-only edit must not
301+
// claim "videos". (The body message already lists the affected entries by name generically.)
302+
const repairHasVideos = (pendingRepair?.videos?.length ?? 0) > 0;
303+
const repairHasFiles = (pendingRepair?.files?.length ?? 0) > 0;
304+
const repairTitle =
305+
repairHasVideos && repairHasFiles
306+
? 'Repair affected videos and files?'
307+
: repairHasFiles
308+
? 'Repair affected files?'
309+
: 'Repair affected videos?';
310+
300311
return (
301312
<div className="day-editor-section tasks-epochs-step">
302313
<h2>Tasks &amp; Epochs</h2>
303314

315+
<p className="tasks-epochs-intro">
316+
Record what the animal did this day. A <strong>task</strong> describes one activity in one
317+
environment, with its cameras. An <strong>epoch</strong> is a numbered time block within
318+
that task — each epoch belongs to exactly one task.
319+
</p>
320+
304321
<MalformedCollectionNotice
305322
day={day}
306323
fields={EPOCHS_STEP_COLLECTIONS}
@@ -314,7 +331,6 @@ export default function TasksEpochsStep(props) {
314331
aria-live="polite"
315332
aria-label="Cameras recommended"
316333
>
317-
<span className="camera-info-icon" aria-hidden="true">📹</span>
318334
<div className="camera-info-text">
319335
This animal has no cameras configured. Cameras are shared animal-catalog entries that
320336
this day&apos;s tasks, videos, and opto/FsGUI protocols select from — set them up once
@@ -347,40 +363,70 @@ export default function TasksEpochsStep(props) {
347363
affectedFilesForDelete={affectedFilesForDelete}
348364
/>
349365

350-
<AssociatedVideosEditor
351-
videos={associatedVideos}
352-
cameras={cameras}
353-
tasks={tasks}
354-
onChange={(next) => onFieldUpdate('associated_video_files', next)}
355-
/>
366+
{/* Optional, epoch-linked extras. Each section is collapsed when EMPTY (progressive
367+
disclosure: lead with the Tasks table, defer empty optional surfaces) but OPEN when it
368+
already holds data, so a returning day's existing videos/files are never hidden. The
369+
item count in the summary is the scent either way. */}
370+
<div className="tasks-optional-sections">
371+
{tasks.length > 0 && (
372+
<p className="tasks-coupling-note">
373+
Associated videos and files (and FsGUI protocols, when optogenetics is enabled) each
374+
reference a task&apos;s epochs. Editing or deleting a task they use prompts you to
375+
confirm before the link is cleared — that repair dialog is expected, not an error.
376+
</p>
377+
)}
356378

357-
<AssociatedFilesEditor
358-
files={associatedFiles}
359-
tasks={tasks}
360-
onChange={(next) => onFieldUpdate('associated_files', next)}
361-
/>
379+
<details className="tasks-optional-section" open={associatedVideos.length > 0}>
380+
<summary>
381+
Associated video files{associatedVideos.length > 0 ? ` (${associatedVideos.length})` : ''}
382+
</summary>
383+
<AssociatedVideosEditor
384+
videos={associatedVideos}
385+
cameras={cameras}
386+
tasks={tasks}
387+
onChange={(next) => onFieldUpdate('associated_video_files', next)}
388+
/>
389+
</details>
362390

363-
{/* FsGUI optogenetics protocols are day-owned and only meaningful when the animal
364-
has optogenetics enabled. They reference this day's epochs + the animal's
365-
cameras as controlled choices. */}
366-
{animal?.optogenetics != null && (
367-
<FsGuiSection
368-
fsGuiYamls={getDayFsGuiYamls(day)}
369-
cameras={cameras}
370-
epochOptions={[...validEpochSet(tasks)].sort((a, b) => a - b)}
371-
// Behavioral events are day-owned and exported from the day (mergeDayMetadata reads
372-
// day.behavioral_events), and the dangling_dio_output rule validates against those —
373-
// so offer the day's behavioral events here.
374-
dioOptions={[
375-
...new Set(
376-
dayBehavioralEvents
377-
.map((e) => e?.name)
378-
.filter((n) => typeof n === 'string' && n !== '')
379-
),
380-
]}
381-
onChange={(next) => onFieldUpdate('fs_gui_yamls', next)}
382-
/>
383-
)}
391+
<details className="tasks-optional-section" open={associatedFiles.length > 0}>
392+
<summary>
393+
Associated files{associatedFiles.length > 0 ? ` (${associatedFiles.length})` : ''}
394+
</summary>
395+
<AssociatedFilesEditor
396+
files={associatedFiles}
397+
tasks={tasks}
398+
onChange={(next) => onFieldUpdate('associated_files', next)}
399+
/>
400+
</details>
401+
402+
{/* FsGUI optogenetics protocols are day-owned and only meaningful when the animal
403+
has optogenetics enabled. They reference this day's epochs + the animal's
404+
cameras as controlled choices. */}
405+
{animal?.optogenetics != null && (
406+
<details className="tasks-optional-section" open={getDayFsGuiYamls(day).length > 0}>
407+
<summary>
408+
Optogenetics protocols (FsGUI)
409+
{getDayFsGuiYamls(day).length > 0 ? ` (${getDayFsGuiYamls(day).length})` : ''}
410+
</summary>
411+
<FsGuiSection
412+
fsGuiYamls={getDayFsGuiYamls(day)}
413+
cameras={cameras}
414+
epochOptions={[...validEpochSet(tasks)].sort((a, b) => a - b)}
415+
// Behavioral events are day-owned and exported from the day (mergeDayMetadata reads
416+
// day.behavioral_events), and the dangling_dio_output rule validates against those —
417+
// so offer the day's behavioral events here.
418+
dioOptions={[
419+
...new Set(
420+
dayBehavioralEvents
421+
.map((e) => e?.name)
422+
.filter((n) => typeof n === 'string' && n !== '')
423+
),
424+
]}
425+
onChange={(next) => onFieldUpdate('fs_gui_yamls', next)}
426+
/>
427+
</details>
428+
)}
429+
</div>
384430

385431
<TaskModal
386432
isOpen={modalOpen}
@@ -396,7 +442,7 @@ export default function TasksEpochsStep(props) {
396442

397443
<ConfirmDialog
398444
isOpen={pendingRepair != null}
399-
title="Repair affected videos?"
445+
title={repairTitle}
400446
message={
401447
pendingRepair
402448
? `Saving this task removes a task epoch still referenced by: ${repairNames}. Confirm to save the task and clear the orphaned epoch reference(s) (their epoch will be unset) so no dangling reference is left. Cancel to discard this task change; the referenced file(s) stay unchanged.`

‎src/pages/DayEditor/TasksEpochsStep.scss‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,47 @@
55
*/
66

77
.tasks-epochs-step {
8+
.tasks-epochs-intro {
9+
max-width: 1200px;
10+
margin: 0 auto var(--spacing-md, 1rem);
11+
line-height: 1.5;
12+
color: var(--color-grey-700, #424242);
13+
}
14+
15+
// Optional, epoch-linked extras: a one-line coupling note + collapsible sections so the
16+
// Tasks table leads and the rest is deferred (progressive disclosure).
17+
.tasks-optional-sections {
18+
max-width: 1200px;
19+
margin: var(--spacing-lg, 1.5rem) auto 0;
20+
21+
.tasks-coupling-note {
22+
margin: 0 0 var(--spacing-md, 1rem);
23+
line-height: 1.5;
24+
color: var(--color-grey-700, #424242);
25+
}
26+
27+
.tasks-optional-section {
28+
border: 1px solid var(--color-grey-300, #bdbdbd);
29+
border-radius: var(--radius-sm, 4px);
30+
margin-bottom: var(--spacing-md, 1rem);
31+
32+
> summary {
33+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
34+
cursor: pointer;
35+
font-weight: 600;
36+
37+
&:focus-visible {
38+
outline: 2px solid var(--color-primary);
39+
outline-offset: 2px;
40+
}
41+
}
42+
43+
&[open] > summary {
44+
border-bottom: 1px solid var(--color-grey-300, #bdbdbd);
45+
}
46+
}
47+
}
48+
849
.camera-info-banner {
950
display: flex;
1051
align-items: flex-start;
@@ -18,11 +59,6 @@
1859
border-radius: 4px;
1960
color: #0d3c61; // WCAG AA on the light blue background
2061

21-
.camera-info-icon {
22-
font-size: 1.5rem;
23-
flex: none;
24-
}
25-
2662
.camera-info-text {
2763
flex: 1 1 auto;
2864
line-height: 1.5;

0 commit comments

Comments
 (0)