Redesign the Learn section: hub, testimony, process, why-use-maple, and a new writing-tips page#2196
Open
k-g-k wants to merge 29 commits into
Open
Redesign the Learn section: hub, testimony, process, why-use-maple, and a new writing-tips page#2196k-g-k wants to merge 29 commits into
k-g-k wants to merge 29 commits into
Conversation
Ports the Figma prototype (maple-learn-tab) into maple's stack: react-bootstrap and styled-components rather than the prototype's Tailwind, and @mui/icons-material rather than adding lucide-react. New: /learn hub page with four cards /learn/testimony consolidates the four testimony tabs into one page Reworked: /learn/legislative-process sticky journey rail + chapter accordion /why-use-maple/[slug] persona cards, hero and checklist; existing copy /learn/ai-tools header/breadcrumb only — MCP setup docs untouched The four retired tab slugs 301 to /learn/testimony. The navbar keeps its Learn dropdown, repointed at the new pages; /learn is reachable directly and via the breadcrumb. Notes: - IntroductionIcon had a cubic path command with four parameters instead of six, which Chrome rejected outright; the trailing L point is now the curve endpoint. - /why-use-maple persona switching is a shallow push: getStaticProps already loads all three namespaces, so the per-click data fetch was pure latency. - The process rail tracks the navbar's live bottom edge, because the navbar does not stay pinned at every breakpoint. - Fixed a 404 link to the MA legislative process document. - Deletes code superseded by the consolidation; Tabs.tsx last, once both of its importers were gone. Known: axe reports one serious color-contrast violation — a tinted stage colour on a closed chapter number cannot reach AA against white for green or orange. Left as designed pending a palette decision.
Replaces the "Step through the process..." intro with a shorter framing line and points readers at MAPLE from the written-testimony step.
The chapter crossing a line just under the sticky rail opens; the previous one closes. "Click here to expand all sections" overrides this and opens everything; clicking the link again, a rail node, or a chapter header hands control back to scroll. Two things this needed: - No manual scroll compensation. The browser's native scroll anchoring already holds the reader's position when a panel above them opens or closes. Compensating on top of it double-corrected, and near the bottom of the page (where scroll also clamps as the document shrinks) the two walked the accordion backwards -- a jump to y=1200 unwound to y=383. - A short settle lock. Without it a reflow can push the next header across the trigger line in the same frame, opening it, and so on down the page. Side effects also had to move out of the setOpenIdx updater: React invokes updaters twice under StrictMode, which fired the lock twice.
Scroll-driven opening is removed. It could not be made to feel right: each chapter that collapses telescopes the list upward, so the reader's scroll carries them through several chapters at once. Damping it (settle locks, one-chapter stepping, scroll snapping) either made the open chapter lag behind the reader or made the page feel sticky. Chapters open on click again. The animation from that work is kept. Panels now transition their height via grid-template-rows 0fr -> 1fr, which needs no measured height, with a delayed visibility so a collapsed panel leaves the accessibility tree and the focus order once it has finished closing (verified: links inside a closed panel cannot take focus). Expand-all staggers the panels open. Collapse-all now closes every chapter, including the one open beforehand.
Clicking a rail stage could leave the chapter's header hidden behind the sticky
rail. scrollIntoView({ block: "nearest" }) treats the whole viewport as visible
and knows nothing about the rail painted over it, so when the reader had already
scrolled the header behind the rail it considered the row in view, declined to
scroll, and scroll-margin-top never applied.
Correcting afterwards worked but read as a jump. Instead the final position is
now predicted before scrolling: the only thing that moves the row is the panel
above it collapsing, and that panel can be measured before it animates. One
scroll, computed from the card's top border against the rail's live bottom edge:
under the rail, it comes down to sit just below it; below the fold, it rises only
as far as needed and never past the rail.
Also:
- overflow-anchor: none on the rows. We compute the post-collapse position
ourselves, and the browser's own scroll anchoring correcting the same reflow
is what produced the visible jump.
- Clicking the already-open chapter now scrolls to it. openIdx did not change,
so the effect never ran and pendingScrollRef went stale.
- previousOpenRef is synced from a later effect so header toggles and expand-all
keep it accurate.
Verified across every scroll position x stage combination, and that the safety
net after the transition never fires.
Individuals: "Making your voice count..." -> "Make your voice count..." Organizations: rewritten, dropping the "civically-engage" typo.
The rail no longer opens or closes chapters. Every chapter is expanded by
default and collapses independently from its own header. As the reader scrolls,
the rail highlights the chapter they are looking at, cycling from the first.
Clicking a stage scrolls to that chapter and opens it only if it was collapsed;
it never closes anything. The expand-all / collapse-all link is gone.
This also removes a whole class of bugs. Because the rail no longer changes any
layout, a highlight can no longer reflow the page, so nothing can cascade,
oscillate, or drag a chapter out from under the reader. The scroll handler is
now pure observation.
The handover has a 180px look-ahead measured from the rail's live bottom edge,
so the next stage lights up while roughly a card's worth of the previous chapter
is still in view.
Also in this change:
- Why Use MAPLE: benefits become disclosures, all collapsed by default, each
with its supporting copy. `signUp` is a call to action, not a benefit, so it
no longer renders as one. `legislativeResearch` has no bodytext -- it is
stored around a link -- and is reassembled with an internal link to /bills.
- Print styles: every disclosure prints expanded, on all three pages. The tips
panel needs `display: block !important` because Chrome's user-agent sheet
declares `[hidden] { display: none !important }`.
- Copy: "Find your legislator's contact info", and the "multiples sides" typo.
The next stage now lights up with a little less of the previous chapter still showing.
Scrolling down, the next stage lights while some of the current chapter is still in view. Scrolling up, a stage only lights once the top of its own card is back in view below the rail -- a stage never lights while its card's top is off-screen above the reader. The highlight now steps from the current stage rather than being recomputed each frame. Recomputing made the upward handover fire about 500px late. The asymmetry between the two lines is also the hysteresis: changing the highlight back costs a whole card's height. An earlier attempt moved a single line with the direction of travel, which was much worse -- it jumped by the width of the gap whenever the reader reversed, so the highlight flickered on the smallest nudge (23 changes across 24 alternating scrolls; now 0).
Clicking Public Hearing through Governor's Action used to leave a ~150px gap between the chapter's header and the rail, while Bill Introduction and Committee Referral landed snug. The cause is the site's own layout: the navbar is sticky-top inside a fixed-height (vh-100) container, so it stays pinned only while that container is in view and then scrolls away with it. Reserving the navbar's height is right for chapters that land while it is still pinned and wrong for the deeper ones, where it is already gone. The scroll target now models that release deterministically. The navbar's bottom follows min(navHeight, containerBottom - scrollY); solving for the scroll that lands a chapter GAP below the rail gives two regimes -- reserve the navbar while it is pinned, don't once it has left -- split at the measured release point rather than a hardcoded chapter index, so it holds on any viewport. Below the 768px breakpoint the navbar swaps to a non-sticky MobileNav that is gone at once, so nothing reserves it there. That is detected by reading the rendered navbar's position rather than duplicating the breakpoint, so it stays in sync with Navbar. Verified at 1280px and 390px: all six chapters land at the same gap, none behind the header, no bounce.
Rail: on narrow screens the journey rail scrolls horizontally, and setting overflow-x to auto forces overflow-y to auto as well, which clipped the top of the active node -- it is scaled up 12% and has a shadow. The scroll strip now has vertical padding (with a compensating negative margin so the card height is unchanged) and the connecting line shifts down to match, so the active circle is no longer shaved off. Mobile nav dropdown (app-wide): inside the hamburger the dropdown menu is white, but its items carried .navLink-primary -- white text meant for the top-level links on the dark navbar -- so they were invisible, white on white. A scoped override gives items inside #basic-navbar-nav .dropdown-menu the same dark, readable text the desktop dropdown uses. Desktop is unaffected.
The persona tabs used to stack vertically on narrow screens because their
labels ("MAPLE for Individuals", etc.) were too long to sit side by side. The
"MAPLE for" prefix is redundant on the tabs, so they now read just "Individuals",
"Organizations", and "Legislators" (new tabLabel keys, kept translatable). At
that length all three stay on a single row at every width down to 320px, icon
above the short label; the full title remains as each tab's accessible name.
Hovering an inactive tab fills it with its persona color and turns the icon and label white -- the same look as the selected tab, no border. The active tab already looks this way. The hover is instant: the card's transition is removed, so the fill and text change together with no fade.
The active item in the mobile hamburger dropdown keeps Bootstrap's filled background (the brand red), but the earlier fix that darkened dropdown text for readability caught the active item too -- dark on red. Restore white, bold text for the active item so it reads against its red background.
On mobile the rail's sticky top was bound to --maple-navbar-height, which a scroll handler rewrote every frame while the non-sticky MobileNav scrolled away (82px -> 0 over the first ~96px). Each rewrite forced the browser to recompute the sticky rail, which was the janky initial scroll before the rail "locked". Desktop was unaffected because the sticky navbar keeps the value constant. The navbar has already scrolled away by the time the rail reaches the top on mobile, so the rail just sticks at top: 0 via CSS below the 768px breakpoint, and the per-frame tracking now runs only when the navbar is actually sticky (desktop). No churn, smooth initial scroll; desktop behavior is unchanged.
Bump the body copy to 140% line-height and add more padding inside the section cards and between them, so the MCP setup instructions read less cramped. Scoped to this page: DescrContainer/SectionContainer/SectionTitle are shared with other pages, so the line-height is overridden through a local styled wrapper and the padding via this page's own utility classes, with a note to revisit whether the looser spacing should become a global default.
The earlier fix removed the per-frame --maple-navbar-height churn, but mobile finger-scroll was still janky before the rail pinned. The remaining cost is the highlight itself: each change re-renders and fires a smooth horizontal auto-scroll of the rail track, and near the top the highlight advances in quick succession so those stutters bunch up and fight the touch scroll. Deeper in, tall chapters space them out -- which is why it felt smooth only after lock. Nothing needs highlighting while the rail is still travelling up to its pinned spot (the whole rail is on screen), so on mobile we hold off all tracking until the rail locks at the top. The pre-lock scroll is now a plain native scroll. Desktop keeps tracking from the start, where it was already smooth.
On refresh the browser restored the previous scroll position, and the rail's highlight -- which defaults to the first stage -- then snapped forward to match, animating every completed tint and the active fill in at once: a visible flash across the rail nodes. The page has no anchor/deep-link state worth restoring, so start at the top instead. A layout effect switches scroll restoration to "manual" and scrolls to 0 before paint, so there is no restored position for the highlight to chase and no jump. It is not a reload -- just a scroll reset on mount.
Previously the scroll-driven highlight listener was attached from the top of the page on mobile, firing a getBoundingClientRect every frame. That coincides with the jankiest moment on mobile -- the site navbar scrolling off while the browser collapses its URL bar and resizes the viewport -- so each forced layout read piled onto that churn. The highlight itself was already smooth once past it. Gate the listener behind an IntersectionObserver: a 1px sentinel at the rail's resting position tells us the instant it pins to the top, and only then do we start tracking (detaching again if the reader scrolls back up past it). The whole pre-lock phase now runs with no scroll handler at all. Desktop is unchanged -- it still tracks from the start, where it was already smooth.
On mobile the highlight could stay stuck on a later stage -- e.g. green committee referral -- after scrolling all the way back to the top, most often when flinging up fast. The per-frame retreat loop that walks the highlight back down could not keep up with a fast scroll, and tracking stopped once the rail unpinned, so it froze wherever the last frame left it. The lock observer already fires reliably when the rail unpins, and that only happens at the top of the section (past the bottom the sentinel stays above the viewport and never intersects). So snap the highlight back to the first stage there, independent of scroll speed.
The three tabs folded into /learn/testimony -- role-of-testimony, writing-effective-testimony, communicating-with-legislators -- already redirected here, but landed the reader at the top of the page no matter which one they asked for. The link in the testimony form pointing at "testimony writing tips" had the same problem: it dropped you at the top and left you to find them. Keep each old slug verbatim as an anchor id on the section that replaced it, so the redirects and any links from outside the site land on that content, and point the form's link at the tips. The tips panel opens when it is the link target -- arriving at a collapsed "Tips" toggle would be less than the page the reader followed the link from. testimony-basics was the overview tab, which the whole page now covers, so it keeps going to the top. Drop the scroll-reveal fade on the How section. It wrapped two of the anchors, so a deep link would have landed on a hidden, translated element, and the fade was not worth the extra bail-out needed to keep it from doing that.
…e representative of their semantic meaning. Icons have different sizes to correct for a visual perception illusion. Address latent bug that was also found in the card and panel headers too.
Break the writing tips out of the About Testimony page into their own page at /learn/writing-effective-testimony -- a hero over a checklist of what every letter should include, then the five "be timely / original / ..." principles as numbered chapters, and a call to action. The About Testimony page now links out to it in place of the collapsible tips panel, and it joins the Learn dropdown. Relabel the Learn dropdown to match the page headings (How Testimony Works, The MA Legislative Process). Redraw the "Why it Matters" icons and pin them at their drawn size so they stop shrinking as flex items. Rework the Why Use MAPLE persona panels: drop the tinted hero fill for a plain card, name each persona above its tagline, and carry the persona colour in the label, tabs and ticks rather than the headline. Disable the Learn crumb in the breadcrumb for now, without removing the hub page. Also correct the Mass Legal Services guide date (2007 -> 2023) and let the AI Research Tools subhead run the full column width.
Guard the three places where a locale-driven list indexes a fixed array: the About Testimony "why it matters" icons and card badges now skip a missing entry rather than rendering <undefined /> and crashing, and the Writing Effective Testimony principles cycle the stage colours so any number of tips stays coloured. Make the mobile Learn dropdown match the desktop one: pin its menu background to the desktop grey, and drop the custom hover so Bootstrap drives the hover and active states -- fixing a broken look where hovering the selected item left white text on a light background. Also tighten the "what every letter should include" copy.
k-g-k
requested review from
mertbagt,
mvictor55,
nesanders and
sashamaryl
as code owners
July 14, 2026 23:43
k-g-k
requested review from
Mephistic,
alexjball,
kiminkim724 and
timblais
as code owners
July 14, 2026 23:43
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bootstrap's default dropdown hover background inherits from --bs-tertiary-bg, which resolves dark inside the navbar (the desktop dropdown escapes that context via a portal, so it stays light). Relying on it turned the mobile hover dark. Pin the non-active hover to the light highlight the desktop menu uses, and hold the active row's red fill and white text through hover and focus so hovering the selected item no longer falls back to the dark default.
Collaborator
|
The animated legislative process header is really slick! |
… resource links Give each persona tab a closing card matching the Writing Effective Testimony page: individuals and organizations get a sign-up card, and legislators get a Browse Testimony button paired with a secondary sign-up. Each card ends with a contact line linking to info@mapletestimony.org. The copy is drawn into a new `cta` block per persona namespace, leaving the legacy `challenge` keys alone. Give the CTA buttons MAPLE's button height in a navy pill, and apply the same treatment to the Writing Effective Testimony button so the two pages match. Underline the Additional Resources links on the legislative process page, matching the link style on the About Testimony page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Live preview
Deployed at https://maple-learn-redesign.vercel.app/learn/testimony — jump to Writing Effective Testimony, Legislative Process, Why Use MAPLE, AI Research Tools, or the Learn hub.
What & why
Rebuilds the Learn section around a shared page shell (
LearnLayout/LearnHeader/LearnBreadcrumb) and replaces the old tabbed/learn/[slug]and/why-use-maple/[slug]tab systems with dedicated, independently-linkable pages.Pages
/learn) — a card-grid entry point./learn/testimony) — the former four testimony tabs consolidated into one page./learn/writing-effective-testimony) — new page; the writing tips broken out of the testimony page, with a call to action.Routing
testimony-basics,role-of-testimony,communicating-with-legislators)308-redirect to the consolidated page, with anchors where a section replaced a tab, so footer and external links keep working./learn/[slug]and the per-persona/abouttab components are removed.Scope
Focused on the Learn section, with supporting changes to:
next.config.js.Nothing elsewhere in the app changes.
Notes for reviewers
public/locales/en/learn.jsonand the persona namespaces; one added key (personaLabel).components/learn/icons/are self-contained SVGs.mainwas merged into this branch (conflict-free) so it is up to date.Issue
No tracking issue exists for this work, so none is linked. Happy to add one if a maintainer would like it tracked.