Skip to content

Commit 9ce1c12

Browse files
authored
Merge pull request #503 from acelaya-forks/feature/tailwind-navbar
Create tailwind-based NavBar component
2 parents b01845f + 7b96316 commit 9ce1c12

File tree

15 files changed

+258
-16
lines changed

15 files changed

+258
-16
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
66

7+
## [0.9.12] - 2025-06-16
8+
### Added
9+
* Add tailwind-based `NavBar` component.
10+
11+
### Changed
12+
* *Nothing*
13+
14+
### Deprecated
15+
* *Nothing*
16+
17+
### Removed
18+
* *Nothing*
19+
20+
### Fixed
21+
* *Nothing*
22+
23+
724
## [0.9.11] - 2025-06-14
825
### Added
926
* Add tailwind-based `TagsAutocomplete` component.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/playwright:v1.52.0-noble
1+
FROM mcr.microsoft.com/playwright:v1.53.0-noble
22

33
ENV NODE_VERSION 22.14
44
ENV TINI_VERSION v0.19.0

dev/Menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const Menu: FC = () => {
1818
<li><Link to="/tailwind/content/tables">Tables</Link></li>
1919
<li><Link to="/tailwind/content/details">Details</Link></li>
2020
<li><Link to="/tailwind/navigation/paginator">Paginator</Link></li>
21+
<li><Link to="/tailwind/navigation/nav-bar">NavBar</Link></li>
2122
<li><Link to="/tailwind/navigation/nav-pills">NavPills</Link></li>
2223
<li><Link to="/tailwind/navigation/menu">Menu</Link></li>
2324
<li><Link to="/tailwind/navigation/dropdown">Dropdown</Link></li>

dev/tailwind/TailwindComponents.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SearchComboboxPage } from './form/SearchComboboxPage';
1212
import { SearchInputPage } from './form/SearchInputPage';
1313
import { DropdownPage } from './navigation/DropdownPage';
1414
import { MenuPage } from './navigation/MenuPage';
15+
import { NavBarPage } from './navigation/NavBarPage';
1516
import { NavPillsPage } from './navigation/NavPillsPage';
1617
import { PaginatorPage } from './navigation/PaginatorPage';
1718
import { CardsPage } from './surfaces/CardsPage';
@@ -29,6 +30,7 @@ export const TailwindComponents: FC = () => {
2930
<Route path="/content/tables" element={<TablePage />} />
3031
<Route path="/content/details" element={<DetailsPage />} />
3132
<Route path="/navigation/paginator" element={<PaginatorPage />} />
33+
<Route path="/navigation/nav-bar" element={<NavBarPage />} />
3234
<Route path="/navigation/nav-pills">
3335
<Route path="" element={<NavPillsPage />} />
3436
<Route path="*" element={<NavPillsPage />} />

dev/tailwind/navigation/DropdownPage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export const DropdownPage: FC = () => {
9696
<Dropdown.Item>Bar</Dropdown.Item>
9797
<Dropdown.Item>Baz</Dropdown.Item>
9898
</Dropdown>
99+
<Dropdown buttonContent="Text" buttonVariant="text">
100+
<Dropdown.Item>Foo</Dropdown.Item>
101+
<Dropdown.Item>Bar</Dropdown.Item>
102+
<Dropdown.Item>Baz</Dropdown.Item>
103+
</Dropdown>
99104
<Dropdown buttonContent="Disabled" buttonDisabled>
100105
<Dropdown.Item>Foo</Dropdown.Item>
101106
<Dropdown.Item>Bar</Dropdown.Item>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { FC } from 'react';
2+
import { Dropdown, NavBar } from '../../../src/tailwind';
3+
4+
export const NavBarPage: FC = () => {
5+
return (
6+
<div className="tw:flex tw:flex-col tw:gap-y-4">
7+
<div className="tw:flex tw:flex-col tw:gap-y-2">
8+
<h2>NavBar</h2>
9+
<NavBar brand={<>Shlink</>}>
10+
<NavBar.MenuItem to="">Foo</NavBar.MenuItem>
11+
<NavBar.MenuItem to="" active>Bar</NavBar.MenuItem>
12+
<NavBar.MenuItem to="">Baz</NavBar.MenuItem>
13+
<NavBar.Dropdown buttonContent="Options">
14+
<Dropdown.Item>First option</Dropdown.Item>
15+
<Dropdown.Item selected>Second option</Dropdown.Item>
16+
<Dropdown.Item>Third option</Dropdown.Item>
17+
</NavBar.Dropdown>
18+
</NavBar>
19+
<p>
20+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc cursus urna et luctus sagittis. Vivamus nibh
21+
justo, fringilla ut luctus et, facilisis nec magna. In facilisis lacus sit amet sem mattis consequat. Aenean
22+
elementum erat et diam blandit, in efficitur mi pellentesque. Aenean purus quam, venenatis eget orci sit
23+
amet,
24+
lacinia blandit magna. Curabitur ut eros quis ipsum faucibus bibendum. Sed nibh sem, malesuada nec massa
25+
vel,
26+
posuere hendrerit justo. Fusce non egestas mauris. Nulla id sapien dapibus, faucibus nunc sed, condimentum
27+
leo.
28+
</p>
29+
<p>
30+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc cursus urna et luctus sagittis. Vivamus nibh
31+
justo, fringilla ut luctus et, facilisis nec magna. In facilisis lacus sit amet sem mattis consequat. Aenean
32+
elementum erat et diam blandit, in efficitur mi pellentesque. Aenean purus quam, venenatis eget orci sit
33+
amet,
34+
lacinia blandit magna. Curabitur ut eros quis ipsum faucibus bibendum. Sed nibh sem, malesuada nec massa
35+
vel,
36+
posuere hendrerit justo. Fusce non egestas mauris. Nulla id sapien dapibus, faucibus nunc sed, condimentum
37+
leo.
38+
</p>
39+
<p>
40+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc cursus urna et luctus sagittis. Vivamus nibh
41+
justo, fringilla ut luctus et, facilisis nec magna. In facilisis lacus sit amet sem mattis consequat. Aenean
42+
elementum erat et diam blandit, in efficitur mi pellentesque. Aenean purus quam, venenatis eget orci sit
43+
amet,
44+
lacinia blandit magna. Curabitur ut eros quis ipsum faucibus bibendum. Sed nibh sem, malesuada nec massa
45+
vel,
46+
posuere hendrerit justo. Fusce non egestas mauris. Nulla id sapien dapibus, faucibus nunc sed, condimentum
47+
leo.
48+
</p>
49+
</div>
50+
</div>
51+
);
52+
};

src/tailwind/feedback/Tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const Tooltip: FC<TooltipProps> = (
8484
role="tooltip"
8585
aria-live="polite"
8686
className={clsx(
87-
'tw:z-1000 tw:max-w-64',
87+
'tw:z-500 tw:max-w-64',
8888
// Add space between anchor and tooltip via padding, so that if the tooltip is inside the anchor, you can hover it
8989
// and it's never closed
9090
{

src/tailwind/navigation/Dropdown.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ export type DropdownProps = PropsWithChildren<{
1111
buttonContent: RequiredReactNode;
1212
buttonSize?: Size;
1313
buttonClassName?: string;
14-
buttonVariant?: 'button' | 'link';
14+
buttonVariant?: 'button' | 'link' | 'text';
1515
buttonDisabled?: boolean;
1616

1717
/** Set as the button's `aria-label` attribute */
1818
buttonLabel?: string;
19-
2019
/** Classes to be set on the containing wrapper element */
2120
containerClassName?: string;
2221
/** Classes to be set on the menu element */
@@ -28,6 +27,8 @@ export type DropdownProps = PropsWithChildren<{
2827
*/
2928
menuAlignment?: 'left' | 'right';
3029

30+
/** Distance between toggle button and menu when open, in pixels. Defaults to 3 */
31+
menuOffset?: number;
3132
/** Whether to hide the caret or not. Defaults to false */
3233
caretless?: boolean;
3334
}>;
@@ -44,14 +45,15 @@ const BaseDropdown: FC<DropdownProps> = ({
4445
menuClassName,
4546
caretless,
4647
buttonLabel,
48+
menuOffset = 3,
4749
}) => {
4850
const [isOpen, setIsOpen] = useState(false);
4951
const buttonRef = useRef<HTMLButtonElement>(null);
5052
const { refs, floatingStyles, context } = useFloating({
5153
open: isOpen,
5254
onOpenChange: setIsOpen,
5355
placement: menuAlignment === 'right' ? 'bottom-end' : 'bottom-start',
54-
middleware: [flip(), offset(3)],
56+
middleware: [flip(), offset(menuOffset)],
5557
// eslint-disable-next-line react-compiler/react-compiler
5658
elements: { reference: buttonRef.current },
5759
});
@@ -126,9 +128,11 @@ const BaseDropdown: FC<DropdownProps> = ({
126128
'tw:highlight:text-lm-brand-dark tw:dark:highlight:text-dm-brand-dark tw:highlight:underline': buttonVariant === 'link',
127129

128130
// Button sizes
129-
'tw:px-1.5 tw:py-1 tw:text-sm tw:gap-x-1.5': buttonSize === 'sm',
130-
'tw:px-3 tw:py-1.5 tw:gap-x-2': buttonSize === 'md',
131-
'tw:px-4 tw:py-2 tw:text-lg tw:gap-x-2': buttonSize === 'lg',
131+
'tw:px-1.5 tw:py-1 tw:text-sm': buttonVariant !== 'text' && buttonSize === 'sm',
132+
'tw:px-3 tw:py-1.5': buttonVariant !== 'text' && buttonSize === 'md',
133+
'tw:px-4 tw:py-2 tw:text-lg': buttonVariant !== 'text' && buttonSize === 'lg',
134+
'tw:gap-x-1.5': buttonSize === 'sm',
135+
'tw:gap-x-2': buttonSize !== 'sm',
132136
},
133137
buttonClassName,
134138
)}
@@ -147,7 +151,7 @@ const BaseDropdown: FC<DropdownProps> = ({
147151
<div
148152
ref={refs.setFloating}
149153
style={floatingStyles}
150-
className="tw:min-w-full tw:z-1000"
154+
className="tw:min-w-full tw:z-500"
151155
{...getFloatingProps()}
152156
>
153157
<Menu

src/tailwind/navigation/NavBar.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { clsx } from 'clsx';
4+
import type { FC, HTMLProps } from 'react';
5+
import { useEffect } from 'react';
6+
import type { LinkProps } from 'react-router';
7+
import { Link,useLocation } from 'react-router';
8+
import { useToggle } from '../../hooks';
9+
import { Button } from '../form';
10+
import type { RequiredReactNode } from '../types';
11+
import type { DropdownProps } from './Dropdown';
12+
import { Dropdown as BaseDropdown } from './Dropdown';
13+
14+
type ItemProps = {
15+
active?: boolean;
16+
};
17+
18+
const MenuItem: FC<LinkProps & ItemProps> = ({ className, active, ...props }) => (
19+
<li role="menuitem" className="tw:w-full tw:flex">
20+
<Link
21+
className={clsx(
22+
'tw:px-2 tw:py-3',
23+
'tw:max-md:w-full tw:max-md:px-3 tw:max-md:py-2',
24+
'tw:text-white tw:no-underline tw:highlight:opacity-100 tw:transition-opacity',
25+
{
26+
'tw:opacity-60': !active,
27+
'tw:opacity-100': active,
28+
},
29+
className,
30+
)}
31+
{...props}
32+
/>
33+
</li>
34+
);
35+
36+
const Dropdown: FC<Omit<DropdownProps, 'menuAlignment' | 'buttonVariant' | 'menuOffset'> & ItemProps> = (
37+
{ containerClassName, buttonClassName, menuClassName, active, ...props },
38+
) => {
39+
return (
40+
<li role="menuitem" className="tw:w-full tw:flex">
41+
<BaseDropdown
42+
containerClassName={clsx('tw:max-md:w-full', containerClassName)}
43+
buttonVariant="text"
44+
buttonClassName={clsx(
45+
'tw:px-2 tw:py-3',
46+
'tw:max-md:w-full tw:max-md:px-3 tw:max-md:py-2',
47+
'tw:text-white tw:highlight:opacity-100 tw:transition-opacity',
48+
{
49+
'tw:opacity-60': !active,
50+
'tw:opacity-100': active,
51+
},
52+
buttonClassName,
53+
)}
54+
menuAlignment="right"
55+
menuOffset={-3}
56+
menuClassName={clsx('tw:mx-2', menuClassName)}
57+
{...props}
58+
/>
59+
</li>
60+
);
61+
};
62+
63+
export type NavBarProps = HTMLProps<HTMLElement> & {
64+
brand: RequiredReactNode;
65+
};
66+
67+
export const BaseNavBar: FC<NavBarProps> = ({ className, brand, children }) => {
68+
const { flag: menuOpen, toggle: toggleMenu, setToFalse: closeMenu } = useToggle(false, true);
69+
const { pathname } = useLocation();
70+
71+
// In mobile devices, collapse the navbar when the pathname changes
72+
useEffect(() => closeMenu(), [pathname, closeMenu]);
73+
74+
return (
75+
<nav
76+
className={clsx(
77+
'tw:w-full tw:relative',
78+
'tw:bg-lm-main tw:dark:bg-dm-main',
79+
'tw:flex tw:max-md:flex-col tw:items-center tw:justify-between',
80+
className,
81+
)}
82+
>
83+
<div className="tw:w-full tw:relative">
84+
<h4
85+
className={clsx(
86+
'tw:text-white tw:px-4 tw:py-3',
87+
'tw:max-md:w-full tw:max-md:flex tw:max-md:flex-col tw:items-center',
88+
)}
89+
>
90+
{brand}
91+
</h4>
92+
<Button
93+
variant="secondary"
94+
className={clsx(
95+
'tw:absolute tw:right-0 tw:top-[50%] tw:translate-y-[-50%]',
96+
'tw:md:hidden tw:mx-2 tw:[&]:px-2',
97+
'tw:opacity-60 tw:highlight:opacity-100 tw:transition-opacity',
98+
'tw:[&]:text-inherit tw:[&]:border-white tw:[&]:highlight:bg-transparent',
99+
)}
100+
onClick={toggleMenu}
101+
aria-label={`${menuOpen ? 'Hide' : 'Show'} menu`}
102+
>
103+
<FontAwesomeIcon icon={menuOpen ? faChevronUp : faChevronDown} />
104+
</Button>
105+
</div>
106+
<ul
107+
role="menu"
108+
className={clsx(
109+
'tw:m-0 tw:p-0',
110+
'tw:max-md:w-full tw:md:mr-2 tw:max-md:absolute tw:max-md:top-full tw:z-2000',
111+
'tw:flex tw:max-md:flex-col tw:items-center',
112+
'tw:bg-lm-main tw:dark:bg-dm-main',
113+
{ 'tw:max-md:hidden': !menuOpen },
114+
)}
115+
>
116+
{children}
117+
</ul>
118+
</nav>
119+
);
120+
};
121+
122+
export const NavBar = Object.assign(BaseNavBar, { MenuItem, Dropdown });

src/tailwind/navigation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './Dropdown';
22
export * from './LinkButton';
33
export * from './Menu';
4+
export * from './NavBar';
45
export * from './NavPills';
56
export * from './Paginator';
67
export * from './RowDropdown';

0 commit comments

Comments
 (0)