Files
emba-course-solver/openspec/changes/desktop-layout-redesign/tasks.md
T
Bill 2ebfb9d2ec v1.4.0: Desktop layout redesign + mobile tabs
Specializations move from a 340px left rail to a horizontal 2-row chip
grid at the top (drag L→R to rank). Each chip shows rank, spec-colored
abbreviation tag matching the tags used in plans/schedule, full name on
its own row, status glyph, and a micro credit bar. Hover/tap a chip to
see full status, allocated/threshold credits, and contributing-courses
breakdown in a popover.

The right pane splits into two side-by-side columns on desktop: Top
Plans (left) and Schedule (right), each scrolling independently. The
search progress bar hoists into a global strip below the spec grid so
it stays visible regardless of which column is scrolled.

Schedule blocks render their course choices as a horizontal row of
equal-width buttons (3-5 per set) instead of stacked rows. Pinned sets
collapse to a single line with the course name inline next to the set
title. Term headers (Spring/Summer/Fall) remain as section dividers.

On mobile, the layout becomes a 3-tab segmented control
(Specializations / Plans / Courses) with the search progress strip
above the tabs. The previous floating MobileStatusBanner and
MobileCourseBanner are dropped — tabs replace their navigation
function.
2026-05-09 17:45:28 -04:00

68 lines
8.8 KiB
Markdown

## 1. Layout reflow scaffolding
- [x] 1.1 In `app/src/App.tsx`, replace the desktop branch's 2-column grid (`340px 1fr`) with a vertical flex container holding: header section, mode toggle, mode comparison banner, specialization strip placeholder, progress strip placeholder, and a 1fr 2-column workspace (`1fr 1fr`) for plans/schedule.
- [x] 1.2 Ensure each workspace column has `overflowY: auto; min-height: 0` and the parent grid has `flex: 1; min-height: 0` so independent scroll works inside the existing 100vh container.
- [x] 1.3 Verify mobile branch JSX is byte-equivalent to today (no regressions to mobile stack, MobileStatusBanner / MobileCourseBanner refs, or scroll-into-view behavior).
- [x] 1.4 Move the `CreditLegend` rendering out of the Schedule column. It will be invoked from the spec strip header in step 2.
## 2. Specialization horizontal strip
- [x] 2.1 In `app/src/components/SpecializationRanking.tsx`, branch on `useMediaQuery() === 'desktop'` (or the existing `isMobile` derivation if shared) to render a strip variant.
- [x] 2.2 Implement the desktop strip container: single-row flex (`display: flex; gap: 6px; overflowX: auto`) so narrow viewports get a horizontal scrollbar on the strip rather than wrap.
- [x] 2.3 Implement compact `Chip` element: ~70px wide, vertical stack of {rank number small, abbreviation bold, micro credit bar}. Background color from `STATUS_STYLES[status].bg`. Use the abbreviation field from `SPECIALIZATIONS`.
- [x] 2.4 Reuse `CreditBar` rendering at smaller dimensions (height ~4px, no tick marks for the compact variant; only the threshold marker).
- [x] 2.5 Replace the `verticalListSortingStrategy` import with `horizontalListSortingStrategy` in the desktop branch (keep vertical for mobile). Reuse the existing `DndContext`, sensors, and `arrayMove` handler.
- [x] 2.6 Render the achievement summary ("X specializations achieved" / "No specializations achieved yet") in a strip header row above the chip strip on desktop. Right-align the `[▸ legend]` toggle.
- [x] 2.7 Mount the existing `CreditLegend` content as the expandable panel triggered by `[▸ legend]`. The legend toggle and panel can be rendered directly in `App.tsx`'s spec-strip header area, or as a child of `SpecializationRanking`'s strip header — pick one and stay consistent.
## 3. Specialization chip hover popover
- [x] 3.1 Build a `SpecChipPopover` component (can live in `SpecializationRanking.tsx`) that takes `specId`, `anchorRect`, `result`, and an `onClose` handler.
- [x] 3.2 Compute popover position with the same smart-flip logic as `CourseInfoPopover` (use `spaceBelow`, `spaceAbove`, clamp `left` to viewport).
- [x] 3.3 Popover content: full spec name (bold), status word ("Achieved" / "Achievable" / "Missing Required" / "Unreachable") with same color as the badge today, allocated/threshold numeric (e.g., "5.5 / 9.0"), and — only when the spec is achieved — a list of contributing courses with their credit amounts (reuse the existing `AllocationBreakdown` logic).
- [x] 3.4 Implement hover-open / hover-close with a 150ms close delay so the cursor can move from chip to popover. Mirror the pattern in `CourseSelection.tsx` (`hoverCloseTimer`, `cancelHoverClose`, `handleHoverOpen`, `handleHoverLeave`).
- [x] 3.5 Implement tap-toggle for touch devices: `onClick` toggles open/closed; clicking outside closes (use the same `mousedown` outside-click handler the existing popover uses).
- [x] 3.6 Open popover via `onMouseEnter` only when `window.matchMedia('(hover: hover)').matches` (consistent with how the course info popover handles touch).
## 4. Hoist search progress strip
- [x] 4.1 In `app/src/components/TopPlans.tsx`, remove the animated progress bar JSX and the percent/iteration sub-line. Keep the static "Search complete · N explored" / "Search incomplete · cap hit at N" status text in the plan header (it's a per-panel summary).
- [x] 4.2 Adjust `TopPlansProps` if needed so the parent can still pass `progress` and `loading` (only consumed for the static status text now).
- [x] 4.3 Create `app/src/components/SearchProgressStrip.tsx` exporting a component that takes `progress: { iterations; iterationsTotal } | null` and `loading: boolean` and renders the animated bar + iteration count when `loading && progress`. Render nothing otherwise (so it collapses to zero height when idle).
- [x] 4.4 Render `SearchProgressStrip` in `App.tsx`'s desktop branch between the spec strip and the workspace columns.
- [x] 4.5 On mobile, render the existing inline-progress-bar JSX inside `TopPlans` (gate it on `isMobile`). The desktop strip only renders when `!isMobile`.
## 5. Schedule horizontal course buttons
- [x] 5.1 In `app/src/components/CourseSelection.tsx`, change `ElectiveSet`'s unpinned branch container from `display: flex; flexDirection: column; gap: 4px` to `display: flex; gap: 4px` (no flex direction) on desktop. Keep the column layout on mobile via a `useMediaQuery` branch.
- [x] 5.2 Each course button gets `flex: '1 1 0'` so buttons stretch to equal width within the row. Set `min-width: 0` on the button to allow text truncation.
- [x] 5.3 Restructure the button content into top/middle/bottom rows: top row contains the info icon (left) and the recommended star (right). Middle is the course name with `WebkitLineClamp: 3`. Bottom is the spec ceiling tag row.
- [x] 5.4 Add `title={course.name}` so the full name is available on hover for truncated names.
- [x] 5.5 Allow the spec-tag row to wrap (`flex-wrap: wrap`); use `flex-grow: 1` on the wrapping container so button heights stretch to match the tallest button in the set.
- [x] 5.6 Map cancelled and already-selected states to button styling: gray background, strikethrough name (cancelled only), tiny footer label "(Cancelled)" or "(Already selected)" below the spec-tag row.
- [x] 5.7 Map per-course searching state to a pulsing skeleton band where the spec tags would render (reuse the `cell-pulse` keyframe).
- [x] 5.8 Map the required-for-spec note to a small amber footer line below the spec tags.
- [x] 5.9 Pinned-set branch is unchanged. Term headers (Spring/Summer/Fall) are unchanged.
- [x] 5.10 Verify the existing `CourseInfoPopover` invocation (and its hover/click/keyboard handlers) still works inside the new button anatomy — the info icon remains a clickable child element with `stopPropagation`.
## 6. Wiring and styles
- [x] 6.1 Confirm `App.tsx` passes `searchProgress` and `treeLoading` down to `SearchProgressStrip` and `TopPlans` correctly with the new responsibilities split.
- [x] 6.2 Confirm `useAppState` shape is unchanged (no state-machine edits needed for this refactor).
- [x] 6.3 Re-check the `courseSectionRef` and `specSectionRef` IntersectionObserver hooks in `App.tsx`: the refs must still anchor to mobile-only sections (the floating MobileStatusBanner / MobileCourseBanner depend on them).
## 7. Verification
- [x] 7.1 Type-check (`npm run build` or `tsc --noEmit`) clean. _Note: vite build passes; `tsc` has pre-existing errors in `feasibility.ts`, `optimizer.ts`, `decisionTree.ts`, `appState.ts`, and an unused `scorer` prop in `CourseSelection.tsx` — all present at v1.3.3 baseline, none introduced by this change._
- [x] 7.2 Lint (`npm run lint`) clean. _Note: same pre-existing 11 errors (setState-in-effect in `appState.ts`/`App.tsx`/`SpecializationRanking.tsx` SortableItem, unused vars in solver files, fast-refresh warning on existing `STATUS_STYLES` export). No new errors from this change._
- [ ] 7.3 Manual desktop check at 1200px+ viewport: spec strip in single row, chip hover opens popover with correct content, drag-to-reorder works left-right, progress strip animates during search, 2 columns scroll independently, schedule blocks render horizontal button rows, all states render correctly (default / cancelled / already-selected / searching / recommended / required-for). _Deferred — Playwright browser binary not available in this sandbox; user should verify in their dev environment._
- [ ] 7.4 Manual desktop check at narrower viewport (~1024px): chip strip gets internal horizontal scrollbar without wrapping; column split still readable; schedule buttons still legible at minimum width. _Deferred — same as 7.3._
- [ ] 7.5 Manual mobile check (≤768px): vertical specialization list with arrows + drag, vertical-stacked schedule rows, progress bar inside Top Plans (not hoisted), MobileStatusBanner and MobileCourseBanner still appear when their respective sections scroll out of view. _Deferred — same as 7.3._
- [x] 7.6 Existing unit tests pass (`npm test`). _Verified: 84 tests pass across 6 test files._
## 8. Release
- [x] 8.1 Bump version to `1.4.0` in `app/vite.config.ts`.
- [x] 8.2 Add a `1.4.0` entry to `CHANGELOG.md` summarizing the desktop redesign (horizontal spec strip, hoisted progress, 2-col workspace, horizontal schedule buttons, mobile unchanged).
- [x] 8.3 Build (`npm run build`) and verify the production bundle. _Verified via `npx vite build`: 403 KB JS bundle, 0.5 KB CSS, built in ~1.3s._