Skip to content

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
codeforboston:mainfrom
k-g-k:learn-hub-redesign
Open

Redesign the Learn section: hub, testimony, process, why-use-maple, and a new writing-tips page#2196
k-g-k wants to merge 29 commits into
codeforboston:mainfrom
k-g-k:learn-hub-redesign

Conversation

@k-g-k

@k-g-k k-g-k commented Jul 14, 2026

Copy link
Copy Markdown

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 hub (/learn) — a card-grid entry point.
  • How Testimony Works (/learn/testimony) — the former four testimony tabs consolidated into one page.
  • Writing Effective Testimony (/learn/writing-effective-testimony) — new page; the writing tips broken out of the testimony page, with a call to action.
  • The MA Legislative Process — a scroll-tracked stage rail.
  • Why Use MAPLE — persona tabs (individuals / organizations / legislators), each with its own URL and server-rendered with the right persona selected.
  • AI Research Tools — restyled onto the shared shell.

Routing

  • The three retired testimony slugs (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.
  • The old /learn/[slug] and the per-persona /about tab components are removed.

Scope

Focused on the Learn section, with supporting changes to:

  • the navbar — new Learn dropdown links and relabeled items, plus a fix so the mobile Learn dropdown's menu background, hover, and active-row states match the desktop dropdown;
  • the footer — Learn links updated to the new pages;
  • the Learn testimony redirects in next.config.js.

Nothing elsewhere in the app changes.

Notes for reviewers

  • Large but self-contained. New copy lives in public/locales/en/learn.json and the persona namespaces; one added key (personaLabel).
  • Icons in components/learn/icons/ are self-contained SVGs.
  • main was 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.

k-g-k added 26 commits July 10, 2026 01:13
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.
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
maple-dev Ready Ready Preview, Comment Jul 17, 2026 2:52pm

Request Review

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.
@nesanders

Copy link
Copy Markdown
Collaborator

The animated legislative process header is really slick!

k-g-k added 2 commits July 17, 2026 10:03
… 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.
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