Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
4075d14
Add prototype sidebar to Ada Overview
jacbn Nov 14, 2025
01b08b5
Add customisable `site` field to sidebar
jacbn Nov 14, 2025
cf667c1
Set `site` for all existing sidebars
jacbn Nov 14, 2025
61e1a22
Allow changing direction of tag picker highlight (wip)
jacbn Nov 14, 2025
c9762c5
Propose new page structure for mixing sidebars / containers
jacbn Nov 14, 2025
43b9806
Create `.icon`s for Overview pages
jacbn Nov 17, 2025
6d0aa1d
Improve styled tab picker customisation options
jacbn Nov 17, 2025
6602d93
Add sidebar to Groups as proof-of-concept
jacbn Nov 17, 2025
89a9395
Improve link styling on Ada sidebar
jacbn Nov 18, 2025
2954a2f
Improve centering for left/down chevron icons
jacbn Nov 18, 2025
cfd61a0
Add basic collapse functionality to Ada sidebar
jacbn Nov 18, 2025
d77f4eb
Migrate `tab-picker` def into site-specific files
jacbn Nov 18, 2025
1620f52
Adjust Ada breakpoints to account for sidebar
jacbn Nov 18, 2025
35d3c53
Add Ada sidebar to immediately accessible pages
jacbn Nov 18, 2025
e35c2d6
Add student view to My Ada sidebar
jacbn Nov 18, 2025
d67aeba
Add sidebar to /progress
jacbn Nov 18, 2025
fca1f47
Switch Ada active tab picker element bg to gray
jacbn Nov 18, 2025
b234c88
Make sidebar contents sticky
jacbn Nov 25, 2025
70bae5f
Make page spacing consistent across My Ada pages
jacbn Nov 25, 2025
fc17bd1
Implement My Ada sidebar on mobile; rework Ada containers
jacbn Nov 26, 2025
c4edaeb
Improve spacing across sidebar and Overview
jacbn Nov 27, 2025
bca6aeb
Better centre chevron icon
jacbn Nov 28, 2025
715e17d
Maintain sidebar collapsed state between pages
jacbn Nov 28, 2025
0b0a78b
Make padding around Ada news consistent
jacbn Nov 28, 2025
33c7c36
Fix misaligned padding on Ada sidebar
jacbn Nov 28, 2025
bccaf7c
Animate sidebar open/close
jacbn Nov 28, 2025
390f186
Merge branch 'main' into feature/my-ada-sidebar
jacbn Dec 4, 2025
35f8549
Improve sidebar breakpoints
jacbn Dec 4, 2025
efbbd5d
Fix issues from icon overhaul
jacbn Dec 4, 2025
524832e
Add fixed margin to all My Ada sidebar pages
jacbn Dec 4, 2025
402bcbb
Merge branch 'main' into feature/my-ada-sidebar
jacbn Dec 8, 2025
247f918
Improve Ada sidebar layout; restore bottom spacing
jacbn Dec 8, 2025
a2cc01b
Restore /overview background image
jacbn Dec 8, 2025
fceeb44
Update VRT baselines
actions-user Dec 8, 2025
02a1fcc
Merge pull request #1851 from isaacphysics/vrt/feature/my-ada-sidebar
jacbn Dec 8, 2025
8328ed1
Fix unused imports
jacbn Dec 9, 2025
cf80b84
Remove unused imports
jacbn Dec 9, 2025
9401a36
Merge branch 'main' into feature/my-ada-sidebar
jacbn Dec 9, 2025
a3e8818
Remove `overflow` on sticky parent preventing stick
jacbn Dec 9, 2025
8a241c1
Add breadcrumb to Overview page for consistency
jacbn Dec 11, 2025
752395c
Fix inconsistency between mobile and desktop menu open-ness
jacbn Dec 11, 2025
bac4abb
Implement alternative design for Ada sidebar toggle
jacbn Dec 11, 2025
05c1b67
Remove unused SCSS
jacbn Dec 11, 2025
d3806a5
Merge branch 'main' into feature/my-ada-sidebar
axlewin Dec 12, 2025
fd0ab9e
Update VRT baselines
actions-user Dec 12, 2025
7263bab
Merge pull request #1876 from isaacphysics/vrt/feature/my-ada-sidebar
axlewin Dec 12, 2025
0254758
Update groups tests
axlewin Dec 12, 2025
eb8eb62
Update Set Assignments tests for new Ada breakpoints
axlewin Dec 12, 2025
b3b2b83
Fix user manager tests on Ada
axlewin Dec 15, 2025
cc68250
Auto-close sidebar on page switch on mobile
jacbn Dec 19, 2025
d4a7ff4
Improve icon spacing
jacbn Dec 19, 2025
6ffbe9c
Restore Ada nav scale
jacbn Dec 19, 2025
966e8c8
Merge branch 'main' into feature/my-ada-sidebar
jacbn Dec 19, 2025
4c21634
Update VRT baselines
actions-user Dec 19, 2025
29fc934
Merge pull request #1887 from isaacphysics/vrt/feature/my-ada-sidebar
jacbn Dec 19, 2025
ad630f8
Merge branch 'main' into feature/my-ada-sidebar
jacbn Dec 24, 2025
136d11c
Rework "is collapsed" query for groups manager test
jacbn Dec 24, 2025
19ebbf3
Remove unused import
jacbn Dec 24, 2025
d1e45a9
Apply page renames to sidebar
jacbn Jan 6, 2026
7cd44e3
Merge branch 'main' into feature/my-ada-sidebar
jacbn Jan 22, 2026
b60b663
FIx ESLint rule violation failing build
jsharkey13 Jan 22, 2026
4171c0d
Merge branch 'main' into feature/my-ada-sidebar
jacbn Feb 5, 2026
167c080
Additional merge changes
jacbn Feb 5, 2026
d844863
Remove `useHistory` from Ada sidebar
jacbn Feb 5, 2026
b78868e
Update VRT baselines
actions-user Feb 5, 2026
41c3784
Merge pull request #1939 from isaacphysics/vrt/feature/my-ada-sidebar
jacbn Feb 5, 2026
7c0ea13
Make sidebar open by default on desktop only
jacbn Feb 6, 2026
1c2fc02
Fix initial sidebar state; remove "expect inital null" reducer test
jacbn Feb 17, 2026
631f066
Change name of My Tests on Ada
jacbn Feb 17, 2026
465be87
Merge branch 'improvement/sidebar-container-refactor' into feature/my…
jacbn Feb 19, 2026
a5db863
Restore missing Ada sidebars from merge; fix tab-picker
jacbn Feb 19, 2026
5cc0386
Wrap Ada sidebars in feature flag
jacbn Feb 19, 2026
cfcc56b
Fix missing imports from merge
jacbn Feb 19, 2026
a2be446
Fix layout bugs
jacbn Feb 19, 2026
609e511
Revert to dropdown navigation on mobile
jacbn Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/assets/cs/icons/home.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/app/components/elements/AdaNewsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { Row, Button, Container } from "reactstrap";
import { Row, Button } from "reactstrap";
import { useDeviceSize } from "../../services";
import { IconCard } from "./cards/IconCard";
import { NewsCard } from "./cards/NewsCard";
Expand All @@ -14,7 +14,7 @@ export const AdaNewsSection = ({isHomepage}: {isHomepage?: boolean}) => {
const showNewsletterPrompts = !userPreferences?.EMAIL_PREFERENCE?.NEWS_AND_UPDATES;
const {setLinkedSetting} = useLinkableSetting();

return ((news && news.length > 0) || showNewsletterPrompts) && <Container className={isHomepage ? "homepage-padding mw-1600" : "overview-padding mw-1600"}>
return ((news && news.length > 0) || showNewsletterPrompts) && <>
<h2 className={classNames({"font-size-1-75 mb-4": isHomepage})}>Tips, tools & support</h2>
{news && news.length > 0 &&
<>
Expand All @@ -40,6 +40,6 @@ export const AdaNewsSection = ({isHomepage}: {isHomepage?: boolean}) => {
/>
</Row>
}
</Container>;
</>;
};

48 changes: 31 additions & 17 deletions src/app/components/elements/SidebarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AffixButton } from './AffixButton';
import { ButtonProps } from 'reactstrap';
import { sidebarSlice, useAppDispatch } from '../../state';
import classNames from 'classnames';
import { siteSpecific } from '../../services';
import { Spacer } from './Spacer';

interface SidebarButtonProps extends ButtonProps {
buttonTitle?: string;
Expand All @@ -28,23 +30,35 @@ export const SidebarButton = ({ buttonTitle, absolute, ...rest }: SidebarButtonP
};
}, [isSticky]);

const button = <AffixButton
{...rest}
data-testid="sidebar-toggle"
innerRef={absolute ? undefined : elementRef as React.RefObject<HTMLButtonElement>}
className={classNames("sidebar-toggle no-print", {"sidebar-toggle-top": !absolute, "stuck": sticky}, rest.className)}
color="keyline"
onClick={toggleMenu}
affix={{
affix: "icon-sidebar",
position: "prefix",
type: "icon",
affixClassName: "icon-inline me-2"
}}
style={{maxWidth: textRef?.current ? `${(!sticky ? textRef.current.clientWidth + 8 : 0) + 61}px` : "max-content"}}
>
<span ref={textRef}>{buttonTitle ?? "Search and filter"}</span>
</AffixButton>;
const button = siteSpecific(
<AffixButton
{...rest}
data-testid="sidebar-toggle"
innerRef={absolute ? undefined : elementRef as React.RefObject<HTMLButtonElement>}
className={classNames("sidebar-toggle no-print", {"sidebar-toggle-top": !absolute, "stuck": sticky}, rest.className)}
color="keyline"
onClick={toggleMenu}
affix={{
affix: "icon-sidebar",
position: "prefix",
type: "icon",
affixClassName: "icon-inline me-2"
}}
style={{maxWidth: textRef?.current ? `${(!sticky ? textRef.current.clientWidth + 8 : 0) + 61}px` : "max-content"}}
>
<span ref={textRef}>{buttonTitle ?? "Search and filter"}</span>
</AffixButton>,
<button
{...rest}
data-testid="sidebar-toggle"
className={classNames("sidebar-toggle w-100 d-flex no-print", rest.className)}
onClick={toggleMenu}
>
<span ref={textRef}>{buttonTitle}</span>
<Spacer />
<i className="icon icon-chevron-right icon-inline-sm" />
</button>
);

return absolute
? <div className="sidebar-toggle-top" ref={elementRef as React.RefObject<HTMLDivElement>}>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/elements/TitleAndBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type TitleAndBreadcrumbProps = BreadcrumbTrailProps & PageTitleProps & TitleMeta
};

export const TitleAndBreadcrumb = ({children, breadcrumbTitleOverride, currentPageTitle, displayTitleOverride, subTitle, disallowLaTeX, className, audienceViews, help, collectionType, intermediateCrumbs, preview, icon}: TitleAndBreadcrumbProps) => {
return <div id="page-title" className={classNames(className, {"title-breadcrumb-container": isPhy, "pt-4 pt-md-7": isAda})}>
return <div id="page-title" className={classNames(className, "title-breadcrumb-container")}>
{isPhy && <div className="title-graphics"/>}
<BreadcrumbTrail
currentPageTitle={breadcrumbTitleOverride ?? currentPageTitle}
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/elements/inputs/HorizontalScroller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const HorizontalScroller = ({ children, enabled, className }: HorizontalS
}, [handleResize]);

return (
<div className={className}>
<div className={classNames(className, "d-grid")}>
<div className={classNames("top-scrollbar-container", {"closed": !displayScroll || !enabled})}
onScroll={() => syncScroll("top")} ref={topScrollbarRef} style={{ height: scrollbarSize }}
>
Expand Down
46 changes: 36 additions & 10 deletions src/app/components/elements/inputs/StyledTabPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Label, Input } from "reactstrap";
import { isDefined } from "../../../services";
import { Spacer } from "../Spacer";
import classNames from "classnames";
import { Link } from "react-router-dom";

/**
* @typedef {Object} StyledTabPickerProps
Expand All @@ -13,20 +14,40 @@ import classNames from "classnames";
* @property {React.ChangeEventHandler<HTMLInputElement>} [onInputChange] - The function to call when the tab is clicked.
* @property {ReactNode} checkboxTitle - The title of the tab.
* @property {number} [count] - The number to display on the tab.
* @property {"left" | "right"} [indicatorPosition] - The position of the indicator.
* @property {Object} [suffix] - An optional suffix to display on the tab.
* @property {"checkbox" | "radio" | "link"} type - The type of the tab picker.
* @property {string} [to] - The URL to navigate to when the tab is clicked (only for type "link").
*/

interface StyledTabPickerProps extends React.HTMLAttributes<HTMLLabelElement> {
type StyledTabPickerProps = React.HTMLAttributes<HTMLElement> & {
checked?: boolean;
disabled?: boolean;
onInputChange?: React.ChangeEventHandler<HTMLInputElement> | undefined;
checkboxTitle: ReactNode;
count?: number;
indicatorPosition?: "left" | "right";
suffix?: {
icon: string;
action?: (e: React.MouseEvent<HTMLButtonElement>) => void;
info: string;
}
}
} & ({
type?: "checkbox" | "radio";
to?: never;
} | {
type: "link";
to: string;
})

const PickerContents = ({checkboxTitle, count, suffix, disabled}: Pick<StyledTabPickerProps, "checkboxTitle" | "count" | "suffix" | "disabled">) => <>
<span className="ms-3">{checkboxTitle}</span>
{isDefined(count) && <span className="badge rounded-pill ms-2">{count}</span>}
<Spacer/>
{suffix && <button type="button" className="px-2 py-1 bg-transparent" onClick={suffix.action} aria-label={suffix.info} title={suffix.info} disabled={disabled}>
<i className={`${suffix.icon} d-block`}/>
</button>}
</>;

/**
* A StyledTabPicker component, used to render a list of selectable tabs, each with a title and optional counter (as to indicate how many options selecting that would provide).
Expand All @@ -38,13 +59,18 @@ interface StyledTabPickerProps extends React.HTMLAttributes<HTMLLabelElement> {
export const StyledTabPicker = (props: StyledTabPickerProps): JSX.Element => {
const { checked, disabled, onInputChange, checkboxTitle, count, suffix, ...rest } = props;
const id = checkboxTitle?.toString().replace(" ", "-");
return <Label {...rest} id={id} tabIndex={-1} className={classNames("d-flex align-items-center tab-picker py-2 my-1 w-100", rest.className, {"checked": checked})}>
<Input type="checkbox" checked={checked ?? false} onChange={onInputChange} readOnly={onInputChange === undefined} disabled={disabled} aria-labelledby={id} />
<span className="ms-3">{checkboxTitle}</span>
{isDefined(count) && <span className="badge rounded-pill ms-2">{count}</span>}
<Spacer/>
{suffix && <button type="button" className="px-2 py-1 bg-transparent" onClick={suffix.action} aria-label={suffix.info} title={suffix.info} disabled={disabled}>
<i className={`${suffix.icon} d-block`}/>
</button>}
const type = props.type ?? "checkbox";

if (type === "link") {
return <Link {...rest} id={props.id ?? id} className={classNames("d-flex align-items-center py-2 w-100 tab-picker", rest.className, {"checked": checked})}
to={rest.to as string}
>
<PickerContents checkboxTitle={checkboxTitle} count={count} suffix={suffix} disabled={disabled} />
</Link>;
}

return <Label {...rest} id={props.id ?? id} tabIndex={-1} className={classNames("d-flex align-items-center py-2 my-1 w-100 tab-picker", rest.className, {"checked": checked})}>
<Input type={type} checked={checked ?? false} onChange={onInputChange} readOnly={onInputChange === undefined} disabled={disabled} aria-labelledby={props.id ?? id} />
<PickerContents checkboxTitle={checkboxTitle} count={count} suffix={suffix} disabled={disabled} />
</Label>;
};
8 changes: 6 additions & 2 deletions src/app/components/elements/layout/PageContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from "react";
import { Container, ContainerProps } from "reactstrap";
import { siteSpecific } from "../../../services";
import { isAda, siteSpecific } from "../../../services";
import { MainContent, SidebarLayout } from "./SidebarLayout";
import classNames from "classnames";
import { FeatureFlag, useFeatureFlag } from "../../../services/featureFlag";

interface PageContainerProps extends Omit<ContainerProps, "pageTitle"> {
pageTitle?: React.ReactNode;
Expand All @@ -19,7 +20,10 @@ interface PageContainerProps extends Omit<ContainerProps, "pageTitle"> {
*/
export const PageContainer = (props: PageContainerProps) => {
const { children, sidebar, pageTitle, id, ...rest } = props;
if (!sidebar) {

const useAdaSidebars = useFeatureFlag(FeatureFlag.ENABLE_ADA_SIDEBARS);

if (!sidebar || (isAda && !useAdaSidebars)) {
return <Container {...rest} id={id} className={classNames("mb-7", rest.className)}>
{pageTitle}
{children}
Expand Down
34 changes: 25 additions & 9 deletions src/app/components/elements/layout/SidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useContext, useEffect } from "react";
import { Col, ColProps, RowProps, Offcanvas, OffcanvasBody, OffcanvasHeader } from "reactstrap";
import { Col, ColProps, RowProps, Offcanvas, OffcanvasBody, OffcanvasHeader, Container, Accordion, AccordionItem, AccordionHeader, AccordionBody } from "reactstrap";
import classNames from "classnames";
import { above, siteSpecific, useDeviceSize } from "../../../services";
import { mainContentIdSlice, selectors, sidebarSlice, useAppDispatch, useAppSelector } from "../../../state";
Expand Down Expand Up @@ -64,15 +64,20 @@ export const ContentSidebar = (props: ContentSidebarProps) => {
const sidebarContext = useContext(SidebarContext);
if (!sidebarContext?.sidebarPresent) return <></>;

const breakpoint = siteSpecific("lg", "md");

const { className, buttonTitle, hideButton, optionBar, ...rest } = props;
return <>
{above['lg'](deviceSize)
? <Col tag="aside" data-testid="sidebar" aria-label="Sidebar" lg={4} xl={3} {...rest} className={classNames("d-none d-lg-flex flex-column sidebar no-print p-4 order-0", className)} />
: <>
return above[breakpoint](deviceSize)
? siteSpecific(
<Col tag="aside" data-testid="sidebar" aria-label="Sidebar" lg={4} xl={3} {...rest} className={classNames("d-none d-lg-flex flex-column sidebar no-print p-4 order-0", className)} />,
<Col tag="aside" data-testid="sidebar" aria-label="Sidebar" {...rest} className={classNames("flex-column sidebar no-print order-0", className)} />
)
: siteSpecific(
<>
{optionBar && <div className="d-flex align-items-center no-print flex-wrap py-3 gap-3">
<div className="flex-grow-1 d-inline-grid align-items-end">{optionBar}</div>
</div>}
{!hideButton && <SidebarButton buttonTitle={buttonTitle} className="my-3"/>}
{!hideButton && <SidebarButton buttonTitle={buttonTitle} className="my-3" />}
<Offcanvas id="content-sidebar-offcanvas" direction="start" isOpen={sidebarOpen} toggle={toggleMenu} container="#root" data-bs-theme={pageTheme ?? "neutral"}>
<OffcanvasHeader toggle={toggleMenu} close={
<div className="d-flex w-100 justify-content-end align-items-center flex-wrap p-3">
Expand All @@ -91,7 +96,18 @@ export const ContentSidebar = (props: ContentSidebarProps) => {
</ContentSidebarContext.Provider>
</OffcanvasBody>
</Offcanvas>
</>
}
</>;
</>,
!hideButton && <Container fluid className="w-100">
<Accordion open={sidebarOpen ? ["myAda"] : []} toggle={toggleMenu} className="position-relative mx-lg-3 my-3" tag="aside" data-testid="sidebar" aria-label="Sidebar">
<AccordionItem className="border">
<AccordionHeader targetId="myAda">
<span className="fw-bold">{buttonTitle}</span>
</AccordionHeader>
<AccordionBody accordionId="myAda" className="accordion-flush-body">
<Col {...rest} className={classNames("flex-column", className)} />
</AccordionBody>
</AccordionItem>
</Accordion>
</Container>
);
};
2 changes: 1 addition & 1 deletion src/app/components/elements/panels/GetStartedWithAda.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const GetStartedWithAda = () => {
<AccordionItem>
<AccordionHeader targetId="1">
<span className="fw-bold">Get started with Ada CS</span>
{getStartedTasks && Object.values(getStartedTasks).every(Boolean) && <i className="icon icon-tick icon-md mx-3" />}
{getStartedTasks && Object.values(getStartedTasks).every(Boolean) && <i className="icon icon-tick icon-sm mx-3" />}
</AccordionHeader>
<AccordionBody accordionId="1">
<ShowLoading
Expand Down
5 changes: 3 additions & 2 deletions src/app/components/elements/sidebar/MyAccountSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export const MyAccountSidebar = (props: MyAccountSidebarProps) => {
<ContentSidebarContext.Consumer>
{(context) =>
<StyledTabPicker id={title} tabIndex={0} checkboxTitle={title} checked={activeTab === tab}
onClick={() => { setActiveTab(tab); context?.close(); }} onKeyDown={ifKeyIsEnter(() => { setActiveTab(tab); context?.close(); })}/>
onClick={() => { setActiveTab(tab); context?.close(); }} onKeyDown={ifKeyIsEnter(() => { setActiveTab(tab); context?.close(); })}
/>
}
</ContentSidebarContext.Consumer>
</li>
)}
</ul>
</ContentSidebar>;
};
};
Loading