Skip to content

Commit aca2358

Browse files
Migrate NavBar components from styled-components to plain CSS
- Create NavBar.css with CSS variables for theme values - Create NavBarButton.css for button styles - Create NavBarMenuButtons.css for menu/popover styles - Update NavBar.tsx to use classNames and CSS variables - Update NavBarButton.tsx to use plain CSS styling - Update NavBarMenuButtons.tsx components to use plain CSS - Update NavBar.stories.tsx to remove styled-components - Add NavBar.stories.css for story-specific styling All components maintain backward compatibility at the React API level. Theme values are bound via CSS custom properties with fallback defaults. Implements: CORE-2004 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 3db6f5a commit aca2358

8 files changed

Lines changed: 261 additions & 163 deletions

File tree

src/components/NavBar.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* NavBar wrapper - portals to 'nav' slot */
2+
.navbar-wrapper {
3+
overflow: visible;
4+
z-index: var(--navbar-z-index, 10);
5+
background: #ffffff;
6+
position: relative;
7+
padding: 0 var(--navbar-padding-mobile, 1.6rem);
8+
box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1);
9+
min-width: 0;
10+
}
11+
12+
@media screen and (min-width: 75.0625em) {
13+
.navbar-wrapper {
14+
padding: 0 var(--navbar-padding-desktop, 3.2rem);
15+
}
16+
}
17+
18+
/* NavBar inner container */
19+
.navbar-bar {
20+
overflow: visible;
21+
display: flex;
22+
justify-content: var(--navbar-justify-content, space-between);
23+
align-items: center;
24+
height: var(--navbar-height-mobile, 4rem);
25+
max-width: var(--navbar-max-width);
26+
margin: 0 auto;
27+
}
28+
29+
@media screen and (min-width: 75.0625em) {
30+
.navbar-bar {
31+
height: var(--navbar-height-desktop, 4rem);
32+
}
33+
}
34+
35+
@media print {
36+
.navbar-bar {
37+
display: none;
38+
}
39+
}

src/components/NavBar.stories.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Styling for NavBar stories in Ladle */
2+
.story-navbar {
3+
position: fixed;
4+
left: 2rem;
5+
top: 2rem;
6+
width: calc(100% - 36rem);
7+
}
8+
9+
.story-wrapper {
10+
display: flex;
11+
height: 100%;
12+
}
13+
14+
.story-styled-menu-item {
15+
color: #d4450c; /* colors.palette.orange */
16+
}
17+
18+
.info-menu-button:hover svg path {
19+
fill: var(--info-icon-fill, currentColor);
20+
}

src/components/NavBar.stories.tsx

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
1+
import React from "react";
12
import { colors } from "../theme";
2-
import styled from "styled-components";
33
import { NavBar } from "./NavBar";
44
import { NavBarButton } from "./NavBarButton";
55
import { PopoverContainer, NavBarPopoverButton, NavBarMenuButton, NavBarMenuItem } from "./NavBarMenuButtons";
66
import { Info } from "./svgs/Info";
77
import { Tab, Tabs, TabList, TabPanel } from "./Tabs";
8+
import "./NavBar.stories.css";
89

910
const dotsBase64 = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNTYiIHZpZXdCb3g9IjAgMCAxMCA1NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSIyOCIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSI1MSIgcj0iNSIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4K";
1011

11-
const InfoMenuButton = styled(NavBarPopoverButton)`
12-
&:hover {
13-
svg path {
14-
fill: ${colors.palette.lightBlue};
15-
}
16-
}
17-
`;
12+
const InfoMenuButton = ({ children, ...props }: React.ComponentProps<typeof NavBarPopoverButton>) => {
13+
const [isHovered, setIsHovered] = React.useState(false);
1814

19-
const DotsMenuButton = styled(NavBarMenuButton)`
20-
padding: 1rem;
21-
`;
15+
return (
16+
<div
17+
onMouseEnter={() => setIsHovered(true)}
18+
onMouseLeave={() => setIsHovered(false)}
19+
style={{ height: '100%' }}
20+
>
21+
<NavBarPopoverButton
22+
{...props}
23+
style={{
24+
'--info-icon-fill': isHovered ? colors.palette.lightBlue : undefined,
25+
...props.style
26+
} as React.CSSProperties}
27+
className="info-menu-button"
28+
>
29+
{children}
30+
</NavBarPopoverButton>
31+
</div>
32+
);
33+
};
2234

23-
const StyledNavBarMenuItem = styled(NavBarMenuItem)`
24-
color: ${colors.palette.orange};
25-
`;
35+
const DotsMenuButton = (props: React.ComponentProps<typeof NavBarMenuButton>) => (
36+
<NavBarMenuButton {...props} style={{ padding: '1rem', ...props.style }} />
37+
);
2638

27-
const StyledWrapper = styled.div`
28-
display: flex;
29-
height: 100%
30-
`;
31-
32-
const StyledNavBar = styled(NavBar)`
33-
position: fixed;
34-
left: 2rem;
35-
top: 2rem;
36-
width: calc(100% - 36rem);
37-
`;
38-
39-
export const Plain = () => <StyledNavBar>NavBar</StyledNavBar>;
40-
export const LogoAndChildren = () => <StyledNavBar logo>Menu</StyledNavBar>;
41-
export const AltTextLinkedLogo = () => <StyledNavBar logo={{alt: 'custom alt', href: '/'}} />;
42-
export const AltTextNoLinkedLogo = () => <StyledNavBar logo={{alt: 'custom alt unlinked'}} />;
43-
export const OverrideJustifyContent = () => <StyledNavBar justifyContent='center'>
39+
export const Plain = () => <NavBar className="story-navbar">NavBar</NavBar>;
40+
export const LogoAndChildren = () => <NavBar logo className="story-navbar">Menu</NavBar>;
41+
export const AltTextLinkedLogo = () => <NavBar logo={{alt: 'custom alt', href: '/'}} className="story-navbar" />;
42+
export const AltTextNoLinkedLogo = () => <NavBar logo={{alt: 'custom alt unlinked'}} className="story-navbar" />;
43+
export const OverrideJustifyContent = () => <NavBar justifyContent='center' className="story-navbar">
4444
<strong>Centered Menu</strong>
45-
</StyledNavBar>;
45+
</NavBar>;
4646

4747
export const Controls_NavBarButton = () =>
48-
<StyledNavBar>
48+
<NavBar className="story-navbar">
4949
<NavBarButton label="Help" />
5050
<NavBarButton label="Info" icon={<Info />} />
51-
<NavBarButton style={{ padding: '1rem' }} icon="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNTYiIHZpZXdCb3g9IjAgMCAxMCA1NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSIyOCIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSI1MSIgcj0iNSIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4K" aria-label="Menu" />
52-
</StyledNavBar>
51+
<NavBarButton style={{ padding: '1rem' }} icon={dotsBase64} aria-label="Menu" />
52+
</NavBar>
5353

5454
export const PopoverAndMenu = () =>
55-
<StyledNavBar>
55+
<NavBar className="story-navbar">
5656
<InfoMenuButton label="Menu">
5757
<PopoverContainer>
5858
<button>Example button</button>
@@ -71,15 +71,15 @@ export const PopoverAndMenu = () =>
7171
<TabPanel id="three">Third</TabPanel>
7272
</Tabs>
7373
</InfoMenuButton>
74-
<StyledWrapper>
74+
<div className="story-wrapper">
7575
<NavBarMenuButton label="Help">
7676
<NavBarMenuItem>Open Guide</NavBarMenuItem>
7777
<NavBarMenuItem>Contact Support</NavBarMenuItem>
7878
</NavBarMenuButton>
7979
<DotsMenuButton aria-label="Test menu" icon={dotsBase64}>
8080
<NavBarMenuItem>Cool menu item</NavBarMenuItem>
8181
<NavBarMenuItem>Really long menu item with a lot of text</NavBarMenuItem>
82-
<StyledNavBarMenuItem>Styled menu item</StyledNavBarMenuItem>
82+
<NavBarMenuItem className="story-styled-menu-item">Styled menu item</NavBarMenuItem>
8383
</DotsMenuButton>
84-
</StyledWrapper>
85-
</StyledNavBar>;
84+
</div>
85+
</NavBar>;

src/components/NavBar.tsx

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,10 @@
1-
import styled from 'styled-components';
1+
import React from 'react';
2+
import classNames from 'classnames';
23
import * as Constants from '../constants';
34
import theme from '../theme';
45
import { BodyPortal } from './BodyPortal';
56
import { NavBarLogo as OpenstaxLogo } from './NavBarLogo';
6-
7-
const BarWrapper = styled(BodyPortal)`
8-
overflow: visible;
9-
z-index: ${theme.zIndex.navbar};
10-
background: ${theme.colors.palette.white};
11-
position: relative;
12-
padding: 0 ${theme.padding.navbar.mobile}rem;
13-
box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1);
14-
@media screen and (min-width: ${theme.breakpoints.desktopBreak}em) {
15-
padding: 0 ${theme.padding.navbar.desktop}rem;
16-
}
17-
min-width: 0;
18-
`;
19-
20-
const StyledNavBar = styled.div<{
21-
maxWidth?: number;
22-
navDesktopHeight: number;
23-
navMobileHeight: number;
24-
justifyContent?: string;
25-
}>`
26-
overflow: visible;
27-
display: flex;
28-
justify-content: ${props => props.justifyContent || 'space-between'};
29-
align-items: center;
30-
height: ${props => props.navMobileHeight}rem;
31-
${props => props.maxWidth ? `max-width: ${props.maxWidth}rem;` : null}
32-
margin: 0 auto;
33-
@media screen and (min-width: ${theme.breakpoints.desktopBreak}em) {
34-
height: ${props => props.navDesktopHeight}rem;
35-
}
36-
@media print { display: none; }
37-
`;
7+
import './NavBar.css';
388

399
type Logo = React.HTMLProps<HTMLAnchorElement> & { alt?: string };
4010

@@ -45,23 +15,53 @@ type NavBarProps = React.PropsWithChildren<{
4515
logo?: boolean | Logo;
4616
justifyContent?: string;
4717
ariaLabel?: string;
18+
className?: string;
19+
style?: React.CSSProperties;
4820
}>
4921

50-
export const NavBar = ({ logo = false, maxWidth, navDesktopHeight, navMobileHeight, justifyContent, ariaLabel, ...props }: NavBarProps) => {
22+
export const NavBar = ({
23+
logo = false,
24+
maxWidth,
25+
navDesktopHeight,
26+
navMobileHeight,
27+
justifyContent,
28+
ariaLabel,
29+
className,
30+
style,
31+
...props
32+
}: NavBarProps) => {
5133
const logoIsObject = typeof logo === 'object';
5234
const renderAnchor = logoIsObject && 'href' in logo;
5335
const {alt = 'OpenStax Logo', ...anchorProps} = logoIsObject ? logo : {};
5436
const logoComponent = logo ? <OpenstaxLogo alt={alt} /> : null;
5537

56-
return <BarWrapper tagName='nav' ariaLabel={ariaLabel} slot='nav' {...props}>
57-
<StyledNavBar
58-
maxWidth={maxWidth}
59-
navDesktopHeight={navDesktopHeight || Constants.navDesktopHeight}
60-
navMobileHeight={navMobileHeight || Constants.navMobileHeight}
61-
justifyContent={justifyContent}
38+
const wrapperStyle = {
39+
'--navbar-z-index': theme.zIndex.navbar,
40+
'--navbar-padding-mobile': `${theme.padding.navbar.mobile}rem`,
41+
'--navbar-padding-desktop': `${theme.padding.navbar.desktop}rem`,
42+
...style
43+
} as React.CSSProperties;
44+
45+
const barStyle = {
46+
'--navbar-max-width': maxWidth ? `${maxWidth}rem` : undefined,
47+
'--navbar-justify-content': justifyContent,
48+
'--navbar-height-mobile': `${navMobileHeight || Constants.navMobileHeight}rem`,
49+
'--navbar-height-desktop': `${navDesktopHeight || Constants.navDesktopHeight}rem`,
50+
} as React.CSSProperties;
51+
52+
return (
53+
<BodyPortal
54+
tagName='nav'
55+
ariaLabel={ariaLabel}
56+
slot='nav'
57+
className={classNames('navbar-wrapper', className)}
58+
style={wrapperStyle}
59+
{...props}
6260
>
63-
{renderAnchor ? <a {...anchorProps}>{logoComponent}</a> : logoComponent}
64-
{props.children}
65-
</StyledNavBar>
66-
</BarWrapper>
61+
<div className="navbar-bar" style={barStyle}>
62+
{renderAnchor ? <a {...anchorProps}>{logoComponent}</a> : logoComponent}
63+
{props.children}
64+
</div>
65+
</BodyPortal>
66+
);
6767
};

src/components/NavBarButton.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* NavBarButton base styles */
2+
.navbar-button {
3+
border: none;
4+
background: none;
5+
padding: 0;
6+
min-height: 4rem;
7+
min-width: 4rem;
8+
height: 100%;
9+
display: inline-flex;
10+
place-content: center;
11+
align-items: center;
12+
cursor: pointer;
13+
font-weight: 500;
14+
}
15+
16+
.navbar-button img {
17+
max-height: 100%;
18+
}
19+
20+
.navbar-button img + *,
21+
.navbar-button svg + * {
22+
margin-left: 0.8rem;
23+
}

src/components/NavBarButton.tsx

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import React from "react";
12
import { Button, ButtonProps } from "react-aria-components";
2-
import styled from "styled-components";
3+
import classNames from "classnames";
4+
import "./NavBarButton.css";
35

46
export type NavBarButtonProps = Omit<ButtonProps, "aria-label"> & {
57
label?: string;
@@ -8,43 +10,20 @@ export type NavBarButtonProps = Omit<ButtonProps, "aria-label"> & {
810
"aria-label"?: string;
911
} & ({ label: string } | { "aria-label": string });
1012

11-
export const NavBarButton = styled(
12-
({
13-
label,
14-
icon,
15-
className,
16-
"aria-label": ariaLabel,
17-
...props
18-
}: NavBarButtonProps) => (
19-
<Button className={className} aria-label={ariaLabel} {...props}>
20-
{icon &&
21-
(typeof icon === "string" ? (
22-
<img aria-hidden="true" src={icon} alt="" />
23-
) : (
24-
icon
25-
))}
26-
{label ? <span>{label}</span> : null}
27-
</Button>
28-
),
29-
)`
30-
border: none;
31-
background: none;
32-
padding: 0;
33-
min-height: 4rem;
34-
min-width: 4rem;
35-
height: 100%;
36-
display: inline-flex;
37-
place-content: center;
38-
align-items: center;
39-
cursor: pointer;
40-
font-weight: 500;
41-
42-
img {
43-
max-height: 100%;
44-
}
45-
46-
img + *,
47-
svg + * {
48-
margin-left: 0.8rem;
49-
}
50-
`;
13+
export const NavBarButton = ({
14+
label,
15+
icon,
16+
className,
17+
"aria-label": ariaLabel,
18+
...props
19+
}: NavBarButtonProps) => (
20+
<Button className={classNames("navbar-button", className)} aria-label={ariaLabel} {...props}>
21+
{icon &&
22+
(typeof icon === "string" ? (
23+
<img aria-hidden="true" src={icon} alt="" />
24+
) : (
25+
icon
26+
))}
27+
{label ? <span>{label}</span> : null}
28+
</Button>
29+
);

0 commit comments

Comments
 (0)