feat(web): add mobile drawer for docs navigation#1985
feat(web): add mobile drawer for docs navigation#1985ComputelessComputer merged 9 commits intomainfrom
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
📝 WalkthroughWalkthroughAdds a DocsDrawer context and provider, a mobile docs drawer (MobileDocsDrawer) with shared DocsNavigation, and a header mobile toggle that opens/closes the drawer on /docs pages; Desktop left sidebar reuses DocsNavigation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Header
participant DocsCtx as DocsDrawerContext
participant MobileDrawer
participant DocsNav as DocsNavigation
participant Router
User->>Header: Tap docs toggle (mobile)
Header->>DocsCtx: setIsOpen(true)
DocsCtx-->>MobileDrawer: isOpen = true (render)
MobileDrawer->>DocsNav: render sections + currentSlug
DocsNav-->>User: display nav links
Note right of User: User taps a doc link
User->>DocsNav: click link
DocsNav->>Router: navigate to doc
DocsNav->>DocsCtx: setIsOpen(false)
DocsCtx-->>MobileDrawer: isOpen = false (hide)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
✅ Deploy Preview for hyprnote-storybook ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for hyprnote ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
apps/web/src/hooks/use-docs-drawer.ts (1)
3-14: Context shape looks good; consider aligning setter type with React’ssetStatefor flexibilityThe context is straightforward and works, but
setIsOpenis narrower than the actualsetIsDrawerOpentype (Dispatch<SetStateAction<boolean>>). If you ever want to use the functional updater form (setIsOpen(prev => !prev)), the current typing will complain even though React supports it.You could make the context future-proof and better aligned with React by typing it as:
import type { Dispatch, SetStateAction } from "react"; interface DocsDrawerContextType { isOpen: boolean; setIsOpen: Dispatch<SetStateAction<boolean>>; }This is purely a type-level improvement; current usage with
setIsOpen(true/false)is fine.apps/web/src/components/header.tsx (1)
192-201: Mobile docs button behavior & visibility look good; minor cleanup is possibleThe mobile-only docs button is well scoped (
sm:hiddencontainer, cleararia-label, guarded byisDocsPage && docsDrawer). Once the context placement is fixed, it should behave as expected.Small optional polish:
md:hiddenon the button is redundant because the parent container is alreadysm:hidden; the button will never be visible atmdand up anyway. You can safely dropmd:hiddento simplify the class list.This is cosmetic and can be deferred.
apps/web/src/routes/_view/docs/route.tsx (3)
50-86: Docs section grouping logic is duplicated betweenLeftSidebarandMobileDrawerBoth
LeftSidebarandMobileDrawerbuilddocsBySectionwith effectively identicaluseMemoblocks: iteratingallDocs, skipping index docs, grouping bysection, sorting byorder, and then mappingdocsStructure.sections.This duplication:
- Increases maintenance cost if the grouping or ordering rules change.
- Risks subtle inconsistencies between desktop and mobile navs over time.
Consider extracting a shared helper (or hook) such as:
function useDocsSections() { return useMemo(() => { const sectionGroups: Record< string, { title: string; docs: (typeof allDocs)[0][] } > = {}; allDocs.forEach((doc) => { if (doc.slug === "index" || doc.isIndex) return; const sectionName = doc.section; if (!sectionGroups[sectionName]) { sectionGroups[sectionName] = { title: sectionName, docs: [] }; } sectionGroups[sectionName].docs.push(doc); }); Object.values(sectionGroups).forEach((section) => { section.docs.sort((a, b) => a.order - b.order); }); const sections = docsStructure.sections .map((sectionId) => { const sectionName = sectionId.charAt(0).toUpperCase() + sectionId.slice(1); return sectionGroups[sectionName]; }) .filter(Boolean); return { sections }; }, []); }Then both components can call
const { sections } = useDocsSections();and passsectionsintoDocsNavigation.This keeps behavior in sync and simplifies future changes.
Also applies to: 153-189
100-137:DocsNavigationis solid; considercnfor conditional classes per project conventionThe navigation component cleanly centralizes docs link rendering and reuses it in both sidebar and drawer; the logic for
currentSlughighlighting is correct.Per the project’s guideline to use
cnfor conditional classNames, you might refactor:className={`block px-3 py-1.5 text-sm rounded-sm transition-colors ${ currentSlug === doc.slug ? "bg-neutral-100 text-stone-600 font-medium" : "text-neutral-600 hover:text-stone-600 hover:bg-neutral-50" }`}to:
import { cn } from "@hypr/utils"; className={cn( "block px-3 py-1.5 text-sm rounded-sm transition-colors", currentSlug === doc.slug ? "bg-neutral-100 text-stone-600 font-medium" : "text-neutral-600 hover:text-stone-600 hover:bg-neutral-50", )}This is stylistic, but it aligns with the shared pattern and scales better if more conditional classes are added.
139-220: Mobile drawer UX is good; add dialog semantics for better accessibilityThe
MobileDrawerimplementation (backdrop, slide-in panel, close button, and reuse ofDocsNavigation) looks solid, and the earlyif (!isOpen) return null;guard is clear.For accessibility, it would be helpful to give the drawer explicit dialog semantics:
- Add
role="dialog"andaria-modal="true"on the drawer container.- Optionally, connect it to the “Documentation” heading with
aria-labelledby.For example:
<div className="fixed top-0 left-0 bottom-0 w-72 bg-white border-r border-neutral-100 shadow-lg z-50 md:hidden animate-in slide-in-from-left duration-300" role="dialog" aria-modal="true" aria-labelledby="docs-drawer-title" > <div className="flex items-center justify-between h-[69px] px-4 border-b border-neutral-100"> <span id="docs-drawer-title" className="font-semibold text-neutral-700" > Documentation </span> {/* close button */} </div> {/* body */} </div>This makes the drawer more usable with assistive technologies.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/src/components/header.tsx(3 hunks)apps/web/src/hooks/use-docs-drawer.ts(1 hunks)apps/web/src/routes/_view/docs/route.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, usecn(import from@hypr/utils). It is similar toclsx. Always pass an array and split by logical grouping.
Usemotion/reactinstead offramer-motion.
Files:
apps/web/src/components/header.tsxapps/web/src/routes/_view/docs/route.tsxapps/web/src/hooks/use-docs-drawer.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces
Files:
apps/web/src/hooks/use-docs-drawer.ts
🧬 Code graph analysis (2)
apps/web/src/components/header.tsx (1)
apps/web/src/hooks/use-docs-drawer.ts (1)
useDocsDrawer(12-14)
apps/web/src/routes/_view/docs/route.tsx (2)
apps/web/src/hooks/use-docs-drawer.ts (1)
DocsDrawerContext(8-10)apps/web/src/routes/_view/docs/structure.ts (1)
docsStructure(1-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Redirect rules - hyprnote
- GitHub Check: Header rules - hyprnote
- GitHub Check: Pages changed - hyprnote
- GitHub Check: Redirect rules - hyprnote-storybook
- GitHub Check: Header rules - hyprnote-storybook
- GitHub Check: Pages changed - hyprnote-storybook
- GitHub Check: ci (macos, macos-14)
- GitHub Check: fmt
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
apps/web/src/routes/_view/route.tsx (1)
10-11: Good fix lifting DocsDrawerContext to the layout; consider memoizing the context valueWiring
DocsDrawerContext.Providerhere correctly scopes the drawer state over both<Header />and<Outlet />, which resolves the previous issue whereHeadercould not see the drawer context from the docs route.To avoid creating a new
valueobject on every layout render (and thus triggering context updates even whenisDocsDrawerOpenhasn’t changed), you can memoize the context value:function Component() { const router = useRouterState(); const isDocsPage = router.location.pathname.startsWith("/docs"); const [onTrigger, setOnTrigger] = useState<(() => void) | null>(null); - const [isDocsDrawerOpen, setIsDocsDrawerOpen] = useState(false); + const [isDocsDrawerOpen, setIsDocsDrawerOpen] = useState(false); + const docsDrawerValue = useMemo( + () => ({ isOpen: isDocsDrawerOpen, setIsOpen: setIsDocsDrawerOpen }), + [isDocsDrawerOpen], + ); return ( <HeroContext.Provider value={{ onTrigger, setOnTrigger: (callback) => setOnTrigger(() => callback), }} > - <DocsDrawerContext.Provider - value={{ isOpen: isDocsDrawerOpen, setIsOpen: setIsDocsDrawerOpen }} - > + <DocsDrawerContext.Provider value={docsDrawerValue}> <div className="min-h-screen flex flex-col"> <Header /> <main className="flex-1"> <Outlet /> </main> {!isDocsPage && <Footer />} </div> </DocsDrawerContext.Provider> </HeroContext.Provider> ); }Also applies to: 31-42
apps/web/src/routes/_view/docs/route.tsx (2)
88-97: Nice extraction of DocsNavigation; consider minor className cleanup only if it growsPulling out
DocsNavigationand reusing it in both the sidebar and drawer reduces duplication and keeps the section/slug logic in one place. The Link wiring (to="/docs/$"with_splat) and currentSlug highlighting look consistent with the existing matching logic.If the conditional classes on the
<Link>grow more complex in the future, it might be worth switching to the sharedcnhelper for readability, but the current string interpolation is still manageable.Also applies to: 100-137
139-219: MobileDrawer behavior is solid; optional accessibility improvementsThe drawer logic (early return when
!isOpen, backdrop click to close, X button witharia-label, and usingDocsNavigationwithonLinkClick={onClose}) matches the product requirements and should give a good mobile UX. The duplicated docsBySection computation viauseMemois acceptable here given the PR note about intentional duplication.If you want to refine accessibility later, consider:
- Adding
role="dialog"andaria-modal="true"to the drawer container.- Providing an
aria-labelledbytying to the “Documentation” heading.- Optionally handling
Escapekey to close the drawer.These are nice-to-haves and not blockers.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/routes/_view/docs/route.tsx(2 hunks)apps/web/src/routes/_view/route.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, usecn(import from@hypr/utils). It is similar toclsx. Always pass an array and split by logical grouping.
Usemotion/reactinstead offramer-motion.
Files:
apps/web/src/routes/_view/docs/route.tsxapps/web/src/routes/_view/route.tsx
🧬 Code graph analysis (1)
apps/web/src/routes/_view/route.tsx (2)
apps/web/src/hooks/use-docs-drawer.ts (1)
DocsDrawerContext(8-10)apps/web/src/components/header.tsx (1)
Header(36-370)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Redirect rules - hyprnote
- GitHub Check: Header rules - hyprnote
- GitHub Check: Pages changed - hyprnote
- GitHub Check: fmt
- GitHub Check: ci (macos, macos-14)
🔇 Additional comments (1)
apps/web/src/routes/_view/docs/route.tsx (1)
19-37: Docs drawer context usage in docs Component looks correctUsing
const docsDrawer = useDocsDrawer();here and conditionally renderingMobileDrawerwired todocsDrawer.isOpen/docsDrawer.setIsOpen(false)lines up with the lifted provider in_view/route.tsx, so the drawer state is now shared between Header and the docs route as intended.You could simplify slightly by assuming
useDocsDraweris non-null (if the hook already throws outside a provider) and dropping thedocsDrawer &&guard, but the current defensive check is fine if you prefer the looser contract.
- Add DocsDrawerContext to manage drawer state - Add MobileDrawer component that shows docs navigation on mobile - Add drawer icon (PanelLeft) to header that only appears on docs pages in mobile view - Extract DocsNavigation component for reuse between sidebar and drawer Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
- Move the context provider to the parent layout so Header can access it - Update docs/route.tsx to consume context instead of providing it - This fixes the drawer icon not appearing in the header on docs pages Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
Place the logo (and optional docs drawer button) in the header prefix for small screens so the icon appears on the left side instead of the suffix. This rearranges the mobile-only markup: the docs drawer button and homepage Link are wrapped together in the prefix sm:hidden container, and the duplicate docs-button in the suffix is removed to avoid rendering the icon on the right.
Increase spacing between the logo and the drawer icon on small screens to improve visual balance and touch target separation. This adjusts the gap utility from 1 to 3 in the header component so the drawer button isn't crowded against the logo when viewing documentation pages.
Make the docs navigation toggle button available as soon as the documentation sidebar is not visible so users on smaller viewports can open the docs drawer without waiting. This change removes the sm-only wrapper around the left header block, adds a docs-drawer button (hidden on md+), and moves several nav links to be hidden on small screens (sm:block) to keep header layout consistent. This ensures the drawer toggle appears immediately when the left sidebar disappears, improving navigation for documentation pages on narrow screens.
Place the mobile documentation drawer inside the main _view route so it shares the same layout/hierarchy as the header and shows below it (header on top, drawer then body). This removes the separate drawer implementation from the docs route and reuses a MobileDocsDrawer in the top-level _view route. The change moves drawer rendering into the main layout, wires up docs data and routing for navigation links, and adjusts styles/animations to display the drawer beneath the header on mobile. Summary of changes: - Remove MobileDrawer component and related hook usage from apps/web/src/routes/_view/docs/route.tsx. - Add MobileDocsDrawer, DocsNavigation, and required imports (allDocs, X icon, docsStructure, useMatchRoute, useMemo) to apps/web/src/routes/_view/route.tsx. - Render MobileDocsDrawer conditionally under Header in the top-level _view route and manage isOpen state there. - Build docs sections and links for the drawer using content-collections and docsStructure, preserving currentSlug highlighting and onLink close behavior.
Refine the documentation drawer to behave like a collapsible drawer with a dynamic icon and toggle behavior. Clicking the drawer icon now toggles open/closed state (and updates aria-label accordingly), and the drawer markup/CSS was adjusted for smoother transitions, larger max height, and simplified structure to remove the separate close button and header. Changes: - Toggle docsDrawer.isOpen when icon button is clicked and swap PanelLeft/PanelLeftClose icons. - Update aria-label to reflect open/close state. - Adjust drawer container classes (max-height, borders, shadow, transition) and simplify internal structure; remove explicit close button and header, increase scrollable area.
Make the mobile docs drawer slide in from the left instead of dropping down from the top, set its height to calc(100vh - 69px) so it fits beneath the header, and ensure its z-index matches/overlaps the header by using z-50 with an overlay at z-40. This improves the mobile docs UX and keeps the footer/main layout stable by moving Outlet and Footer positions. Changes: - Move Outlet and Footer placement inside main layout to ensure proper structure. - Replace previous top-down collapsible block with a left sliding fixed drawer and backdrop overlay. - Add positioning, height, width, borders, shadow, and transition classes for smooth slide animation. - Keep DocsNavigation inside a scrollable container within the drawer.
Simplify mobile drawer UI by removing the fullscreen backdrop and embedding the drawer directly. Replaced the overlay with a drawer container that always renders and uses translate-x for show/hide, added larger shadow (shadow-2xl with neutral tint) and minor structure adjustments so navigation stays scrollable and closes on link click. This reduces DOM elements and implements the requested right-side shadow instead of a backdrop.
bd66780 to
f4df127
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
apps/web/src/components/header.tsx (1)
2-8: Docs drawer toggle wiring looks correct; consider small a11y/UX refinements
- Using
isDocsPageplus thedocsDrawernull-check is a good guard so the button only appears on docs routes when the context is available.- The toggle correctly calls
docsDrawer.setIsOpen(!docsDrawer.isOpen)and swaps betweenPanelLeft/PanelLeftClosewith a cleararia-label.Two optional improvements:
- Add
aria-expanded={docsDrawer.isOpen}(and optionallyaria-pressed) on the button so assistive tech can track the open/closed state of the drawer.- Consider resetting the docs drawer when the main mobile hamburger menu opens (e.g., calling
docsDrawer?.setIsOpen(false)where you toggleisMenuOpen) to avoid both menus being open at once on small screens.Also applies to: 11-11, 49-51, 59-76
apps/web/src/routes/_view/route.tsx (2)
66-117: Mobile docs drawer logic looks solid; consider tightening memo deps and slug handling
useMatchRoute({ to: "/docs/$", fuzzy: true })+_splatextraction is a reasonable way to computecurrentSlugfor highlighting.docsBySectioncorrectly filters out index docs, groups bydoc.section, sorts byorder, and orders sections according todocsStructure.sections, with a safe.filter(Boolean).A couple of small refinements you might consider:
- Instead of an empty dependency array on
useMemo, tie it to the static inputs for clarity/future‑proofing, e.g.useMemo(() => { ... }, [allDocs, docsStructure.sections]), in case those ever become dynamic.- If there’s ever a docs route without
_splat(e.g.,/docsindex),currentSlugwill beundefinedand nothing is highlighted; that’s acceptable, but you could optionally map that case to a default slug fromdocsStructure.defaultPagesif you want consistent highlighting.Also applies to: 118-132
135-172: DocsNavigation is clean; optionalcnandaria-currenttweaks
- The section/title rendering and
currentSlug === doc.slugcheck for active styling are straightforward, andonLinkClickis a nice hook for closing the drawer on navigation.Optional polish:
- Per the coding guidelines, if this
classNamegrows, consider switching the conditional string tocn([...])to keep class logic easier to extend.- For accessibility, you could add
aria-current="page"on the active<Link>whencurrentSlug === doc.slugto give screen readers an explicit cue about the current doc.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/src/components/header.tsx(5 hunks)apps/web/src/hooks/use-docs-drawer.ts(1 hunks)apps/web/src/routes/_view/docs/route.tsx(1 hunks)apps/web/src/routes/_view/route.tsx(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/src/routes/_view/docs/route.tsx
- apps/web/src/hooks/use-docs-drawer.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, usecn(import from@hypr/utils). It is similar toclsx. Always pass an array and split by logical grouping.
Usemotion/reactinstead offramer-motion.
Files:
apps/web/src/components/header.tsxapps/web/src/routes/_view/route.tsx
🧬 Code graph analysis (2)
apps/web/src/components/header.tsx (1)
apps/web/src/hooks/use-docs-drawer.ts (1)
useDocsDrawer(12-14)
apps/web/src/routes/_view/route.tsx (3)
apps/web/src/hooks/use-docs-drawer.ts (1)
DocsDrawerContext(8-10)apps/web/src/components/header.tsx (1)
Header(42-373)apps/web/src/routes/_view/docs/structure.ts (1)
docsStructure(1-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Redirect rules - hyprnote
- GitHub Check: Header rules - hyprnote
- GitHub Check: Pages changed - hyprnote
- GitHub Check: fmt
- GitHub Check: ci (macos, macos-14)
🔇 Additional comments (2)
apps/web/src/components/header.tsx (1)
88-88: Header nav breakpoint changes look fine; just sanity‑check layout atsmSwitching the Product dropdown container to
hidden sm:blockand making Blog/Pricing visible fromsmupwards keeps the smallest viewport cleaner while showing more nav on small tablets and up. This looks consistent with the new docs toggle, but it’s worth a quick visual pass on 320–640px widths to ensure items don’t crowd the header.Also applies to: 159-166
apps/web/src/routes/_view/route.tsx (1)
3-9: LiftingDocsDrawerContext.Providerinto the_viewlayout correctly fixes Header scope
isDocsDrawerOpenstate inComponentandDocsDrawerContext.Provideraround<Header />,<Outlet />, and<Footer />ensureuseDocsDrawer()is now usable from Header and any docs child routes.- Conditional
Footer/MobileDocsDrawerrendering keyed offisDocsPagekeeps the drawer docs‑only while preserving the rest of the layout.Two minor considerations:
- You’re deriving
isDocsPagefromrouter.location.pathname.startsWith("/docs")here and usinguseMatchRouteinMobileDocsDrawer; in the future you might unify both onuseMatchRoutefor consistency.- Since the provider now always wraps
_view,useDocsDraweris safe to use anywhere under this layout; it may be worth documenting this in the hook or context file.Also applies to: 13-15, 32-37, 45-61
feat(web): add mobile drawer for docs navigation
Summary
Adds an expandable drawer for mobile view on documentation pages. The drawer icon (PanelLeft) only appears in the header when viewing docs pages on mobile devices.
Changes:
DocsDrawerContextto share drawer state between header and docs routeMobileDrawercomponent that slides in from the left with docs navigationDocsNavigationcomponent for reuse between sidebar and drawerUpdates since last revision:
DocsDrawerContext.Providerfromdocs/route.tsxto_view/route.tsx(parent layout) so the Header component can access the drawer state. This was necessary because React context only flows from ancestors to descendants.Demo
View original video (rec-3273c1676b6c4178a71035b34b11b052-edited.mp4)
Review & Testing Checklist for Human
/docson a mobile device or use browser dev tools to simulate mobile. Verify the drawer icon (small sidebar icon to the left of "Get reminder") appears in the header/blog,/pricing) on mobile and confirm the drawer icon does NOT appearRecommended test plan: Open the web app in Chrome DevTools with mobile viewport (e.g., iPhone SE 375x667), navigate to
/docs, and test the full drawer flow.Notes
MobileDrawercomponent duplicates thedocsBySectionlogic fromLeftSidebar- this is intentional to keep the components independent, but could be refactored to share the logic if preferred