Skip to content

feat: SSR tab bar#1949

Draft
KevinWu098 wants to merge 46 commits into
mainfrom
kwu/minimal-ssr-tabs-2ce8
Draft

feat: SSR tab bar#1949
KevinWu098 wants to merge 46 commits into
mainfrom
kwu/minimal-ssr-tabs-2ce8

Conversation

@KevinWu098

@KevinWu098 KevinWu098 commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

SSR the schedule tab bar in a small slice (~11 files). Real MUI tabs ship in the initial HTML at the correct position: bottom on mobile, top of the right pane on desktop.

ClientShell only existed to wrap dynamic({ ssr: false }). Layout now imports Client directly, same as Header.

How it works

Mobile vs desktop on SSRlayout.tsx parses ua via Next userAgent() and passes it through UserAgentProvider. useIsMobile() uses that as useMediaQuery defaultMatches, so the server picks the right layout before hydration.

Tab barScheduleManagement renders tabs above content on desktop and below on mobile (useIsMobile() conditionals, not CSS breakpoints). Tab labels use a composed icon + text Box.

Client-only boundariesScheduleManagementContent and ScheduleCalendar stay dynamic({ ssr: false }). Everything else in the tab shell SSRs.

Desktop split — Swapped react-split for react-resizable-panels. Gutter and panel sizes are real markup (Group / Panel / Separator), so no componentDidMount layout jump. Separator grip is markup styled from theme.palette.primary.

Files changed

  • layout.tsxUserAgentProvider, import Client directly
  • client.tsx — mobile/desktop home, resizable panels, calendar ssr: false
  • ScheduleManagement.tsx — tab placement, content ssr: false
  • ScheduleManagementTab.tsxuseIsMobile() tab styles
  • UserAgentProvider.tsx, useIsMobile.tsx — parsed UA for SSR hint
  • globals.css — remove unused .gutter rules
  • Deleted client-shell.tsx

Test plan

  • Desktop: tabs in right pane, correct size/position on first paint (no horizontal jump)
  • Mobile: tabs at bottom
  • Double-click split separator resets to default proportions
  • Tab navigation still works after hydration

Follow-ups

  • SSR search tab content
  • SSR added tab, map, calendar — one subtree at a time
Open in Web Open in Cursor 

Review in cubic

devin-ai-integration Bot and others added 7 commits June 26, 2026 05:37
… removing ssr:false boundary

- useActiveTab: removed useIsMobile, just reads route segment directly
- ScheduleManagementTab: replaced useIsMobile branching with CSS responsive sx
  (flexDirection, sizing, display via custom breakpoints default/sm)
- ScheduleManagement: replaced dual tab render (!isMobile/isMobile) with single
  render using CSS order for mobile-bottom/desktop-top placement
- Client: replaced isMobile ternary (MobileHome/DesktopHome) with CSS responsive
  display, rendering both layouts with mutual exclusion via breakpoints
- ClientShell: removed dynamic({ ssr: false }) wrapper, Client now SSRs normally

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
ColumnStore calls getLocalStorageColumnToggles() at module evaluation time.
With ssr:false removed, this now runs during SSR prerender where window
is undefined. Added typeof window guard (same pattern as SettingsStore
and SectionThemeStore).

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
Added a getStorage() helper that returns window.localStorage when
available and undefined during SSR. All localStorage access functions
now use getStorage()?.method() with ?? null fallbacks for getters.

This eliminates the need for per-call-site typeof window guards and
makes the entire localStorage API safe to call during SSR/prerender.

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
Leaflet accesses window at module scope, so it cannot be imported
during SSR prerender. Replace React.lazy with next/dynamic({ ssr: false })
and move the loading fallback into the dynamic loading option, removing
the Suspense wrapper.

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
…dration mismatch

Remove useIsMobile() from 16 components that now SSR after removing the
ssr:false boundary. Replace JS conditional rendering with CSS responsive
sx patterns (display: { default: X, sm: Y }) so server and client render
identical DOM.

Components fixed:
- CalendarToolbar: dual finals button with CSS display toggle
- CalendarRoot: always use desktop date format (calendar is desktop-only)
- TbaCalendarCard: dual text spans with CSS display toggle
- NotificationSnackbar: responsive marginBottom
- SectionTable: responsive color strip width
- SectionTableBodyRowColorStrip: responsive cursor/pointerEvents
- GpaCell/GradesPopover: remove isMobile prop, use responsive dimensions
- RestrictionsCell: dual Popover/Tooltip with CSS display toggle
- EnrollmentCell: always render Tooltip with disableTouchListener
- CourseInfoBar: responsive startIcon visibility
- CourseInfoButton: responsive text visibility
- EnrollmentColumnHeader: responsive Help icon visibility
- EnrollmentHistoryPopover: responsive dimensions
- PastSyllabiPopover: responsive dimensions
- ScheduleManagement: use window.matchMedia in useEffect for redirect

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
…React #418)

- Remove Math.random() from createSkeletonEvents() — always use variation[0]
  as the SSR-safe default so server and client render identical skeleton events
- Move localStorage blueprint read into a useEffect in CalendarRoot so it only
  runs after hydration (SSR gets null → deterministic default, client hydrates
  with same default, then effect updates to stored blueprint)
- Remove unused getLocalStorageSkeletonBlueprint import from skeletonHelpers

Co-Authored-By: Kevin Wu <kevinwu098@gmail.com>
Remove the blanket dynamic({ ssr: false }) on Client so ScheduleManagement
tabs render on the server. Keep tab content and calendar client-only via
targeted dynamic imports, use CSS order for mobile-bottom/desktop-top tab
placement, and replace useIsMobile with responsive sx in tab components.
Move desktop /calendar redirect to proxy.ts using user-agent detection so
the route never renders before redirect. Restore scroll position comments
in ScheduleManagement and drop the client matchMedia redirect.
useActiveTab dropped useIsMobile for SSR hydration but that let /calendar
highlight on desktop. Seed isMobile from request user-agent in layout and
restore calendar -> search mapping without useMediaQuery.
…p layout

Gutter and panel sizes render in SSR markup via Group/Panel/Separator,
eliminating the margin hack and react-split componentDidMount CLS.
@KevinWu098

Copy link
Copy Markdown
Member Author

/deploy

@cursor cursor Bot changed the title feat: SSR tab bar with minimal scope feat: SSR schedule tab bar (minimal) Jun 27, 2026
@KevinWu098 KevinWu098 changed the title feat: SSR schedule tab bar (minimal) feat: SSR tab bar Jun 27, 2026
Remove asymmetric paddingRight that shifted the grip bar off-center.
Make the Separator a flex container that stretches to full pane height,
and center the grip glyph with grid place-items.
Derive Panel defaultSize strings from DEFAULT_LAYOUT so percentages are
explicit. Use minSize="400px". Drop redundant getBoundingClientRect
width sync in favor of onResize only. Style the Separator directly and
remove the inner Box wrapper.
Render ScheduleManagementTabs once and use flex order to place it above
content on desktop and below on mobile, matching the sm (800px) breakpoint.
Resolve pnpm-lock.yaml by regenerating from merged package.json after
integrating main's deploy perf, next-pwa removal, and get-data script.
CSS order with nested sm breakpoint keys did not apply correctly with
the theme's custom breakpoint scale. Use column-reverse below sm (800px)
and column at sm+ so tabs render at the bottom on mobile and top on
desktop.
Wire explicit onDoubleClick to groupRef.setLayout(DEFAULT_LAYOUT) since
the library built-in only resizes a single panel. Restore paddingRight: 1px
and boxSizing: border-box from the old react-split gutter styling.
Replace MobileHome/DesktopHome branching with one Group: calendar panel and
separator hide below sm via GlobalStyles, schedule panel always hosts the
single ScheduleManagement instance. Sync panel percentages on viewport
changes (0/100 mobile, 42.5/57.5 desktop). Defer desktop split calendar
with noSsr media query to avoid loading it on mobile.

/calendar on desktop: show search content and tab via responsive CSS and
tab index logic without router redirect. Remove UserAgentProvider and revert
useIsMobile to plain useMediaQuery. Use responsive sx on tab labels.
@cursor cursor Bot changed the base branch from main to devin/1782451547-ssr-tabs June 27, 2026 20:27
@cursor cursor Bot temporarily deployed to staging-1949 June 27, 2026 20:27 Inactive
@KevinWu098 KevinWu098 changed the base branch from devin/1782451547-ssr-tabs to main June 27, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants