2ebfb9d2ec
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.
130 lines
11 KiB
Markdown
130 lines
11 KiB
Markdown
## Context
|
||
|
||
The desktop layout in `App.tsx` today is a 2-column grid: a 340px specialization rail on the left and a 1fr right pane on the right that stacks `TopPlans` over `CourseSelection` and scrolls together. The right pane must accommodate three concerns at once — search progress, plan list, and a 12-set schedule — and ranking the specs ties up significant width that the schedule could use.
|
||
|
||
Within `CourseSelection`, each elective set renders its courses as full-width vertically stacked rows. On desktop the right pane is ~836px wide, so each row uses lots of horizontal space for one course; the schedule extends well past the viewport.
|
||
|
||
The search progress bar lives inside `TopPlans`, so when a user is scrolled down looking at the schedule it disappears.
|
||
|
||
This change reflows the desktop layout to:
|
||
1. A horizontal spec strip at the top (drag L→R to rank).
|
||
2. A hoisted, full-width progress bar.
|
||
3. A 2-column workspace below: Top Plans (left) | Schedule (right), each with independent scroll.
|
||
4. Schedule blocks render their course choices as a horizontal flex row of buttons rather than stacked rows.
|
||
|
||
Mobile (≤768px in the project's `useMediaQuery` hook) keeps its current vertical arrangement — this is a desktop-only redesign.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Eliminate the "Top Plans pushes Schedule below the fold" problem by giving each its own column with independent scroll.
|
||
- Free vertical space in the schedule by laying course choices out horizontally per elective set.
|
||
- Keep search progress visible from any scroll position by hoisting the progress bar.
|
||
- Preserve all existing semantics: pinning, recommended star, ceiling tags, info popover, cancelled/already-selected states, per-course searching, mode comparison banner, achievement count.
|
||
- Preserve mobile UX exactly — no regression in the mobile experience.
|
||
|
||
**Non-Goals:**
|
||
- No solver, worker, or data-layer changes. The decision-tree pipeline and leaf cache are untouched.
|
||
- No new features (e.g., new filters, new modes, search of plans). Pure UI reflow.
|
||
- No tablet-specific rework. Tablet is treated the same as desktop in `useMediaQuery` (returns `desktop` for ≥768px), so it inherits the new layout. No separate intermediate layout.
|
||
- The horizontal chip strip is desktop-only; mobile is not getting horizontal chips.
|
||
|
||
## Decisions
|
||
|
||
### D1: Specs go to a horizontal strip with hover popover detail
|
||
|
||
Compact 15-chip strip in a single row, each ~70px. Each chip shows rank number, 3-letter abbreviation (already in `SPECIALIZATIONS`), and a micro credit bar. Background color encodes status using the existing `STATUS_STYLES` palette.
|
||
|
||
Detail (full name, status word, allocated/threshold numeric, contributing-courses breakdown) lives in a hover popover anchored to the chip. Touch users on hybrid devices get the same popover via tap-toggle (`onClick`).
|
||
|
||
**Why not vertical-with-collapsed-detail?** Considered, but the user's mental model is "left = highest priority" — horizontal flow matches reading direction and frees the entire left rail for the workspace below. A wrap-to-2-rows variant was rejected because it breaks the left-to-right priority continuity.
|
||
|
||
**Why not include the full row content (name, numbers, status word) in each chip?** 70px is too narrow. Hover popover gets the detail without crowding the strip.
|
||
|
||
### D2: Drag strategy switches to horizontal
|
||
|
||
`@dnd-kit/sortable` is already a dependency. Switch the `SortableContext` strategy from `verticalListSortingStrategy` to `horizontalListSortingStrategy` on desktop. Mobile keeps `verticalListSortingStrategy`. We branch on `isMobile` from the existing `useMediaQuery` hook inside `SpecializationRanking`.
|
||
|
||
### D3: Progress bar hoisted into a global strip
|
||
|
||
Today `TopPlans` renders both the progress bar and the "search complete / cap hit" status text. Extract a small `SearchProgressStrip` component (or inline JSX in `App.tsx`) that consumes the same `searchProgress` and `treeLoading` state from `useAppState`. `TopPlans` keeps the static "search complete" text in its header (it pertains to the plan list specifically), but the animated progress bar moves out.
|
||
|
||
On mobile the progress bar stays inside `TopPlans` to avoid stealing precious vertical space at the top.
|
||
|
||
### D4: 2-column workspace with independent scroll
|
||
|
||
```
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
flex: 1;
|
||
min-height: 0; /* required so children can shrink and scroll */
|
||
|
||
each child: overflowY: auto; min-height: 0;
|
||
```
|
||
|
||
This is a small reuse of the existing pattern in `App.tsx` (the current right pane already uses `overflowY: auto; min-height: 0`).
|
||
|
||
**Why 1fr / 1fr (50/50)?** Plans rows render 12 mini-set buttons that benefit from width; schedule blocks render 3-5 buttons per row that also benefit from width. Neither dominates. Asymmetric splits (e.g., 40/60) showed plan mini-cards wrapping awkwardly in the 40 case.
|
||
|
||
### D5: Schedule course buttons go horizontal (flex stretch)
|
||
|
||
Each non-pinned elective set replaces its column-of-rows with:
|
||
|
||
```jsx
|
||
<div style={{ display: 'flex', gap: 4 }}>
|
||
{courses.map(c => <CourseButton style={{ flex: '1 1 0' }} ... />)}
|
||
</div>
|
||
```
|
||
|
||
Equal width per button within a set; widths vary set-to-set (3-button sets have wider buttons than 5-button sets). No flex-wrap — tested width math (109px minimum for 5-button case in a 588px column) is acceptable for the 2-line line-clamp.
|
||
|
||
**Per-button anatomy:**
|
||
- Top row: info icon (existing `i` button, top-left), recommended star (top-right when applicable).
|
||
- Middle: course name with `WebkitLineClamp: 2-3`. Title attribute carries full name for hover.
|
||
- Footer: spec-tag row (existing `SpecTag` component), allowed to wrap if a course has 4+ specs.
|
||
- Cancelled/already-selected: gray background, strikethrough name, small footer text "Cancelled" or "Already selected".
|
||
- Searching: pulsing skeleton bar where spec tags would render.
|
||
- Required-for note: small amber footer line below the spec tags.
|
||
|
||
**Pinned sets stay as today:** single line "Course Name + Clear button". Only unpinned sets get the new horizontal layout.
|
||
|
||
**Why not a CSS grid with auto-fit?** Considered `grid-template-columns: repeat(auto-fit, minmax(110px, 1fr))` which would wrap automatically. Rejected because it can produce inconsistent per-set widths that don't align with the set's actual course count, and because the courses-per-set is bounded (3–5) so flex with no wrap is simpler.
|
||
|
||
### D6: Term headings (Spring/Summer/Fall) remain as section dividers
|
||
|
||
User confirmed. They're cheap (~30px each) and orient the user temporally. No change needed.
|
||
|
||
### D7: CreditLegend collapses into the spec strip header
|
||
|
||
Today `CreditLegend` is its own block in the right pane. It would be in the way of the new column split. Rendered as a small `[▸ legend]` toggle button in the spec strip's right-aligned header area; clicking expands an inline panel below the strip with the same content the legend already provides. The component itself stays largely as-is — it's already a self-contained collapsible.
|
||
|
||
### D8: Hover popover positioning for spec chips
|
||
|
||
Anchor to the chip's bounding rect. Reuse the smart-flip logic already in `CourseInfoPopover` (`spaceBelow` / `spaceAbove`, `placeAbove`) for vertical fit. Horizontally, clamp `left` to `Math.min(rect.left, window.innerWidth - popoverWidth - 8)` so chips near the right edge don't clip the popover off-screen.
|
||
|
||
Use a 150ms close delay (same pattern as the course info popover) so the user can move the cursor from chip → popover without it disappearing.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Risk: 70px chip width doesn't render legibly at narrower desktop viewports (e.g., 1024px usable).** → Mitigation: the layout's `maxWidth: 1200px` is centered, so a 1024px viewport gives a smaller container. We allow horizontal scrolling on the chip strip if the strip content exceeds container width (`overflowX: auto`) as a safety valve. Cleaner option: enforce a min container width (e.g., 1200px → fall back to mobile-style vertical layout below it). Decision: take the `overflowX: auto` safety valve; the project uses a desktop breakpoint of `≥768px` and below ~1100px the chip strip will get a horizontal scrollbar on the strip element only.
|
||
- **Risk: Course buttons with 4+ spec tags overflow at narrow column widths.** → Mitigation: spec tag row uses `flex-wrap: wrap`; button heights flex-stretch so the row of buttons stays vertically aligned even when one button wraps to a 3-line spec-tag area.
|
||
- **Risk: Touch device on the desktop breakpoint (e.g., iPad in landscape, when `useMediaQuery` may treat it as desktop) has no real hover.** → Mitigation: tap on a chip toggles the popover open/closed; tap outside closes. Same pattern as `CourseInfoPopover`. Use `onClick` for parity with hover on desktop.
|
||
- **Risk: Drag-and-drop horizontal feels different from vertical and may need tweaked activation distance.** → Mitigation: keep the existing `PointerSensor` activation distance (5px). `horizontalListSortingStrategy` works out of the box for left-right reorders. Test with keyboard-driven reorder (arrow keys via `KeyboardSensor`).
|
||
- **Risk: Hoisted progress bar feels disconnected from "Top Plans" since the static "Search complete · X explored" text remains in the plan list header.** → Mitigation: keep the static text in `TopPlans` (it's a state summary), move only the animated bar. The user mental model is "progress = global; result counts per panel = local." Acceptable.
|
||
- **Risk: Mobile regression.** → Mitigation: `App.tsx` already branches on `isMobile`; we keep the mobile branch's JSX identical and only restructure the desktop branch. Spec strip, course button row, and column split are all gated behind `!isMobile`. The existing `MobileStatusBanner` and `MobileCourseBanner` continue to anchor to `specSectionRef` and `courseSectionRef` — those refs need to point to mobile-only sections, which we keep.
|
||
- **Trade-off: Lost at-a-glance numeric credit display in the spec strip.** Users wanting "5.5 / 9.0" must hover. Acceptable given the strip's information density goal.
|
||
- **Trade-off: Lost at-a-glance status text label ("Achieved", "Achievable", etc.) in the spec strip.** Background color carries this. Status word still visible in the hover popover. Color-only meets design goal but accessibility could improve later with optional text dot/glyph; not in scope here.
|
||
|
||
## Migration Plan
|
||
|
||
This is a UI-only change. No data migration, no API contract change, no breaking persistence changes (`localStorage` keys for ranking and pinned courses untouched).
|
||
|
||
Rollout: ship in v1.4.0 (minor bump per CHANGELOG convention). Single deployment; no flag gating needed since the behavior is purely visual.
|
||
|
||
If the redesign needs to be reverted, revert the App.tsx, SpecializationRanking.tsx, TopPlans.tsx, CourseSelection.tsx changes and restore the version bump. State and solver code are untouched.
|
||
|
||
## Open Questions
|
||
|
||
- None blocking implementation. Width edge cases (very narrow desktop viewports, very wide popover content) are handled by `overflowX: auto` + popover smart-flip.
|