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.
8.8 KiB
8.8 KiB
1. Layout reflow scaffolding
- 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. - 1.2 Ensure each workspace column has
overflowY: auto; min-height: 0and the parent grid hasflex: 1; min-height: 0so independent scroll works inside the existing 100vh container. - 1.3 Verify mobile branch JSX is byte-equivalent to today (no regressions to mobile stack, MobileStatusBanner / MobileCourseBanner refs, or scroll-into-view behavior).
- 1.4 Move the
CreditLegendrendering out of the Schedule column. It will be invoked from the spec strip header in step 2.
2. Specialization horizontal strip
- 2.1 In
app/src/components/SpecializationRanking.tsx, branch onuseMediaQuery() === 'desktop'(or the existingisMobilederivation if shared) to render a strip variant. - 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. - 2.3 Implement compact
Chipelement: ~70px wide, vertical stack of {rank number small, abbreviation bold, micro credit bar}. Background color fromSTATUS_STYLES[status].bg. Use the abbreviation field fromSPECIALIZATIONS. - 2.4 Reuse
CreditBarrendering at smaller dimensions (height ~4px, no tick marks for the compact variant; only the threshold marker). - 2.5 Replace the
verticalListSortingStrategyimport withhorizontalListSortingStrategyin the desktop branch (keep vertical for mobile). Reuse the existingDndContext, sensors, andarrayMovehandler. - 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. - 2.7 Mount the existing
CreditLegendcontent as the expandable panel triggered by[▸ legend]. The legend toggle and panel can be rendered directly inApp.tsx's spec-strip header area, or as a child ofSpecializationRanking's strip header — pick one and stay consistent.
3. Specialization chip hover popover
- 3.1 Build a
SpecChipPopovercomponent (can live inSpecializationRanking.tsx) that takesspecId,anchorRect,result, and anonClosehandler. - 3.2 Compute popover position with the same smart-flip logic as
CourseInfoPopover(usespaceBelow,spaceAbove, clampleftto viewport). - 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
AllocationBreakdownlogic). - 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). - 3.5 Implement tap-toggle for touch devices:
onClicktoggles open/closed; clicking outside closes (use the samemousedownoutside-click handler the existing popover uses). - 3.6 Open popover via
onMouseEnteronly whenwindow.matchMedia('(hover: hover)').matches(consistent with how the course info popover handles touch).
4. Hoist search progress strip
- 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). - 4.2 Adjust
TopPlansPropsif needed so the parent can still passprogressandloading(only consumed for the static status text now). - 4.3 Create
app/src/components/SearchProgressStrip.tsxexporting a component that takesprogress: { iterations; iterationsTotal } | nullandloading: booleanand renders the animated bar + iteration count whenloading && progress. Render nothing otherwise (so it collapses to zero height when idle). - 4.4 Render
SearchProgressStripinApp.tsx's desktop branch between the spec strip and the workspace columns. - 4.5 On mobile, render the existing inline-progress-bar JSX inside
TopPlans(gate it onisMobile). The desktop strip only renders when!isMobile.
5. Schedule horizontal course buttons
- 5.1 In
app/src/components/CourseSelection.tsx, changeElectiveSet's unpinned branch container fromdisplay: flex; flexDirection: column; gap: 4pxtodisplay: flex; gap: 4px(no flex direction) on desktop. Keep the column layout on mobile via auseMediaQuerybranch. - 5.2 Each course button gets
flex: '1 1 0'so buttons stretch to equal width within the row. Setmin-width: 0on the button to allow text truncation. - 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. - 5.4 Add
title={course.name}so the full name is available on hover for truncated names. - 5.5 Allow the spec-tag row to wrap (
flex-wrap: wrap); useflex-grow: 1on the wrapping container so button heights stretch to match the tallest button in the set. - 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.
- 5.7 Map per-course searching state to a pulsing skeleton band where the spec tags would render (reuse the
cell-pulsekeyframe). - 5.8 Map the required-for-spec note to a small amber footer line below the spec tags.
- 5.9 Pinned-set branch is unchanged. Term headers (Spring/Summer/Fall) are unchanged.
- 5.10 Verify the existing
CourseInfoPopoverinvocation (and its hover/click/keyboard handlers) still works inside the new button anatomy — the info icon remains a clickable child element withstopPropagation.
6. Wiring and styles
- 6.1 Confirm
App.tsxpassessearchProgressandtreeLoadingdown toSearchProgressStripandTopPlanscorrectly with the new responsibilities split. - 6.2 Confirm
useAppStateshape is unchanged (no state-machine edits needed for this refactor). - 6.3 Re-check the
courseSectionRefandspecSectionRefIntersectionObserver hooks inApp.tsx: the refs must still anchor to mobile-only sections (the floating MobileStatusBanner / MobileCourseBanner depend on them).
7. Verification
- 7.1 Type-check (
npm run buildortsc --noEmit) clean. Note: vite build passes;tschas pre-existing errors infeasibility.ts,optimizer.ts,decisionTree.ts,appState.ts, and an unusedscorerprop inCourseSelection.tsx— all present at v1.3.3 baseline, none introduced by this change. - 7.2 Lint (
npm run lint) clean. Note: same pre-existing 11 errors (setState-in-effect inappState.ts/App.tsx/SpecializationRanking.tsxSortableItem, unused vars in solver files, fast-refresh warning on existingSTATUS_STYLESexport). 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.
- 7.6 Existing unit tests pass (
npm test). Verified: 84 tests pass across 6 test files.
8. Release
- 8.1 Bump version to
1.4.0inapp/vite.config.ts. - 8.2 Add a
1.4.0entry toCHANGELOG.mdsummarizing the desktop redesign (horizontal spec strip, hoisted progress, 2-col workspace, horizontal schedule buttons, mobile unchanged). - 8.3 Build (
npm run build) and verify the production bundle. Verified vianpx vite build: 403 KB JS bundle, 0.5 KB CSS, built in ~1.3s.