feat: replace MUI tabs with custom implementation (#1030)

* feat: custom navbar implementation

* remove mui/material

* add key navigation and improve visuals

also allow for handling of cases that likely won't happen anyway

---------

Co-authored-by: saintnoodle <hi@noodle.moe>
This commit is contained in:
saintnoodle
2024-04-09 23:35:57 +01:00
committed by GitHub
co-authored by saintnoodle
parent 0bb76fb338
commit 1dec6d34c6
19 changed files with 498 additions and 410 deletions
-3
View File
@@ -27,13 +27,10 @@
},
"dependencies": {
"@babel/core": "7.1.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fortawesome/fontawesome-free": "5.13.0",
"@fullcalendar/core": "^6.0.0",
"@fullcalendar/daygrid": "^6.0.0",
"@fullcalendar/react": "^6.0.0",
"@mui/material": "^5.11.1",
"@nivo/bar": "0.79.1",
"@nivo/colors": "0.79.1",
"@nivo/core": "0.79.0",
+4 -7
View File
@@ -1,4 +1,3 @@
import { MaterialThemeProvider } from "components/layout/misc/MaterialThemeProvider";
import { LoadingScreen } from "components/layout/screens/LoadingScreen";
import { AllLUGPTStatsContextProvider } from "context/AllLUGPTStatsContext";
import { BannedContextProvider } from "context/BannedContext";
@@ -29,12 +28,10 @@ export default function App({ basename }: { basename: string }) {
<UserSettingsContextProvider>
<AllLUGPTStatsContextProvider>
<BrowserRouter basename={basename}>
<MaterialThemeProvider>
<Toaster />
<SubheaderContextProvider>
<Routes />
</SubheaderContextProvider>
</MaterialThemeProvider>
<Toaster />
<SubheaderContextProvider>
<Routes />
</SubheaderContextProvider>
</BrowserRouter>
</AllLUGPTStatsContextProvider>
</UserSettingsContextProvider>
@@ -1,26 +1,21 @@
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import React from "react";
import Navbar from "components/nav/Navbar";
export function DashboardHeader() {
const navItems = [
<NavItem key="activity" to="/">
<Navbar.Item key="activity" to="/">
Activity
</NavItem>,
<NavItem key="activity" to="/calendar">
</Navbar.Item>,
<Navbar.Item key="calendar" to="/calendar">
Calendar
</NavItem>,
<NavItem key="profiles" to="/profiles">
</Navbar.Item>,
<Navbar.Item key="profiles" to="/profiles">
Your Profiles
</NavItem>,
<NavItem key="profiles" to="/global-activity">
</Navbar.Item>,
<Navbar.Item key="global-activity" to="/global-activity">
Global Activity
</NavItem>,
</Navbar.Item>,
];
return (
<div className="align-items-center mb-0">
<Navbar>{navItems}</Navbar>
</div>
);
return <Navbar>{navItems}</Navbar>;
}
+13 -18
View File
@@ -1,41 +1,36 @@
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext } from "react";
import { UserSettingsContext } from "context/UserSettingsContext";
import Navbar from "components/nav/Navbar";
export function GPTBottomNav({ baseUrl }: { baseUrl: string }) {
const { settings } = useContext(UserSettingsContext);
const navItems = [
<NavItem key="activity" to={`${baseUrl}/`}>
<Navbar.Item key="activity" to={`${baseUrl}/`}>
Activity
</NavItem>,
<NavItem key="songs" to={`${baseUrl}/songs`}>
</Navbar.Item>,
<Navbar.Item key="songs" to={`${baseUrl}/songs`}>
Songs
</NavItem>,
<NavItem key="leaderboards" to={`${baseUrl}/leaderboards`}>
</Navbar.Item>,
<Navbar.Item key="leaderboards" to={`${baseUrl}/leaderboards`}>
Leaderboards
</NavItem>,
<NavItem
</Navbar.Item>,
<Navbar.Item
key="quests"
to={`${baseUrl}/quests`}
otherMatchingPaths={[`${baseUrl}/questline`]}
>
Quests
</NavItem>,
</Navbar.Item>,
];
if (settings?.preferences.developerMode) {
navItems.push(
<NavItem key="dev-info" to={`${baseUrl}/dev-info`}>
<Navbar.Item key="dev-info" to={`${baseUrl}/dev-info`}>
Developer Info
</NavItem>
</Navbar.Item>
);
}
return (
<div className="align-items-center mb-0">
<Navbar>{navItems}</Navbar>
</div>
);
return <Navbar>{navItems}</Navbar>;
}
@@ -20,7 +20,7 @@ export default function UserHeaderContainer({
<div className="card-body">
<div className="row align-items-center">{children}</div>
</div>
<div className="card-footer pb-0 pt-0">{footer}</div>
<div className="card-footer p-0">{footer}</div>
</div>
</div>
<div className="col-12">
@@ -1,30 +0,0 @@
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { ColourConfig } from "lib/config";
import React from "react";
import { JustChildren } from "types/react";
const theme = createTheme({
typography: {
fontFamily: "Poppins",
},
palette: {
secondary: {
main: ColourConfig.primary,
},
primary: {
main: ColourConfig.primary,
},
},
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
});
export function MaterialThemeProvider({ children }: JustChildren) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
@@ -1,5 +1,5 @@
import { CircularProgress } from "@mui/material";
import React from "react";
import Loading from "components/util/Loading";
import SplashImage from "../misc/SplashImage";
export function SplashScreen({ broke }: { broke: string }) {
@@ -9,7 +9,7 @@ export function SplashScreen({ broke }: { broke: string }) {
className="bg-body position-fixed inset-0 d-flex flex-column justify-content-center align-items-center"
>
<SplashImage />
{!broke && <CircularProgress />}
{!broke && <Loading />}
{broke && <p className="mt-4">{broke}</p>}
</div>
);
-23
View File
@@ -1,23 +0,0 @@
import { Tab } from "@mui/material";
import React from "react";
import { Link } from "react-router-dom";
export default function NavItem({
to,
children,
}: {
to: string;
otherMatchingPaths?: Array<string>;
children: string;
}) {
return (
// @ts-expect-error Faulty types from MUI. The to={to} here is necessary
// and works perfectly fine, but the MUI types disagree that it exists.
<Tab
label={<span className="mx-4">{children}</span>}
LinkComponent={Link}
to={to}
style={{ opacity: 1 }}
/>
);
}
+336 -35
View File
@@ -1,52 +1,353 @@
import Tabs from "@mui/material/Tabs";
import React, { useMemo } from "react";
import { useLocation } from "react-router-dom";
import Icon from "components/util/Icon";
import { debounce } from "lodash";
import React, {
cloneElement,
KeyboardEventHandler,
ReactElement,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useLocation, Link, LinkProps } from "react-router-dom";
export default function Navbar({ children }: { children: JSX.Element[] }) {
const links = children
.filter((e) => e.props.to)
.map((e) => {
const basePath =
e.props.to[e.props.to.length - 1] === "/"
? e.props.to.substring(0, e.props.to.length - 1)
: e.props.to;
export interface NavbarItemProps extends LinkProps<HTMLAnchorElement> {
to: string;
children: string;
/**
* An array of other pathnames that should trigger an active item
*/
otherMatchingPaths?: Array<string>;
}
if (e.props.otherMatchingPaths) {
return [basePath, ...e.props.otherMatchingPaths];
}
return [basePath];
});
export type NavbarItem = ReactElement<NavbarItemProps>;
export interface NavbarProps {
children: ReactElement<NavbarItemProps> | ReactElement<NavbarItemProps>[];
}
export default function Navbar({ children }: NavbarProps) {
const location = useLocation();
const value = useMemo(() => {
const loc = location.pathname.split(/([#?]|\/$)/u)[0];
const navRef = useRef<HTMLDivElement>(null);
const itemsListRef = useRef<HTMLDivElement>(null);
let lidx = 0;
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({
left: 0,
width: 0,
});
const [showScrollLeft, setShowScrollLeft] = useState(false);
const [showScrollRight, setShowScrollRight] = useState(false);
const links = useMemo(
() =>
Array.isArray(children)
? children.map((el) => {
const to = el.props.to;
if (!to) {
return [];
}
const basePath =
to[to.length - 1] === "/" ? to.substring(0, to.length - 1) : to;
if (el.props.otherMatchingPaths) {
return [basePath, ...el.props.otherMatchingPaths];
}
return [basePath];
})
: children.props.to
? [[children.props.to]]
: [[]],
[children]
);
const activeLocIndex = useMemo(() => {
const loc = location.pathname.split(/([#?]|\/$)/u)[0];
let locIndex = 0;
for (let i = 0; i < links.length; i++) {
const matchingPaths = links[i];
if (matchingPaths.some((path) => loc.startsWith(path))) {
lidx = i;
locIndex = i;
// break; THIS IS DELIBERATE TO AVOID / matching everything
}
}
return locIndex;
}, [links, location]);
return lidx;
}, [location]);
const items = React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) {
return null;
}
if (process.env.NODE_ENV === "development") {
if (child.type === React.Fragment) {
console.error("The navbar doesn't accept React fragments; Pass an array instead");
}
}
return cloneElement(child, {
key: child.key || index,
"aria-selected": index === activeLocIndex,
role: "tab",
draggable: false,
className: "tachi-navbar-item",
});
});
const scrollButtons = (
<>
{(showScrollLeft || showScrollRight) && (
<>
<button
aria-label="Scroll Navbar Left"
aria-hidden
hidden={!showScrollLeft}
tabIndex={-1}
className="tachi-navbar-button-left"
onClick={() => handleScrollButton()}
>
<Icon type="chevron-left" />
</button>
<button
aria-label="Scroll Navbar Right"
aria-hidden
hidden={!showScrollRight}
tabIndex={-1}
className="tachi-navbar-button-right"
onClick={() => handleScrollButton(true)}
>
<Icon type="chevron-right" />
</button>
</>
)}
</>
);
const nav = navRef.current;
const itemsList = itemsListRef.current;
const activeElement = itemsList?.children[activeLocIndex];
const updateIndicator = () => {
if (nav && activeElement) {
const navRect = nav.getBoundingClientRect();
const activeElementRect = activeElement.getBoundingClientRect();
setIndicatorStyle({
left: activeElementRect.left - navRect.left + nav.scrollLeft,
width: activeElementRect.width,
});
}
};
const scrollActiveElementIntoView = debounce(
() => {
if (nav && itemsList && activeElement) {
if (itemsList.clientWidth > nav.clientWidth) {
const childRect = activeElement.getBoundingClientRect();
const navRect = nav.getBoundingClientRect();
const left = childRect.left - navRect.left + nav.scrollLeft;
const right = left + childRect.width;
if (left <= nav.scrollLeft || right >= nav.scrollLeft + nav.clientWidth) {
// ScrollIntoView is seemingly broken in Chromium so we will do it ourselves
requestAnimationFrame(() => {
if (left < nav.scrollLeft) {
nav.scrollTo({ left });
} else if (right > nav.scrollLeft + nav.clientWidth) {
nav.scrollTo({ left: right - nav.clientWidth });
}
});
}
}
}
},
125,
{ trailing: true }
);
const handleScrollButton = (right = false) => {
if (nav && itemsList) {
const scrollSize = Array.from(itemsList.children).reduceRight(
(accumulator: number, item, index) => {
if (accumulator + item.clientWidth > nav.clientWidth) {
if (index === 0) {
return nav.clientWidth;
}
}
return accumulator + item.clientWidth;
},
0
);
nav.scrollTo({
left: (nav.scrollLeft += right ? scrollSize : -scrollSize),
});
}
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
type Tab = HTMLAnchorElement | null;
if (!itemsList) {
return;
}
const currentFocus = itemsList.ownerDocument.activeElement;
if (!currentFocus) {
return;
}
const previous = currentFocus.previousElementSibling as Tab;
const next = currentFocus.nextElementSibling as Tab;
const first = itemsList.firstElementChild as Tab;
const last = itemsList.lastElementChild as Tab;
switch (event.key) {
case "ArrowLeft":
event.preventDefault();
if (currentFocus === itemsList.firstElementChild) {
last?.focus();
} else {
previous?.focus();
}
break;
case "ArrowRight":
event.preventDefault();
if (currentFocus === itemsList.lastElementChild) {
first?.focus();
} else {
next?.focus();
}
break;
case "Home":
event.preventDefault();
first?.focus();
break;
case "End":
event.preventDefault();
last?.focus();
break;
default:
break;
}
};
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
updateIndicator();
scrollActiveElementIntoView();
}, [nav, itemsList, activeElement]);
useEffect(() => {
if (!nav || !itemsList) {
return;
}
const debouncedUpdateIndicator = debounce(updateIndicator, 75, { trailing: true });
const debounceIntersectOptions = [125, { leading: true }] as const;
const handleFirstIntersect: IntersectionObserverCallback = debounce(
(entries) => setShowScrollLeft(!entries[0].isIntersecting),
...debounceIntersectOptions
);
const handleLastIntersect: IntersectionObserverCallback = debounce(
(entries) => setShowScrollRight(!entries[0].isIntersecting),
...debounceIntersectOptions
);
const observeChildren = () => {
if (
itemsList.children.length > 1 &&
itemsList.firstElementChild &&
itemsList.lastElementChild
) {
firstObserver.observe(itemsList.firstElementChild);
lastObserver.observe(itemsList.lastElementChild);
}
};
/**
* handles observations whenever mutations may happen
*
* I'm not convinced we need this but the previously used mui navbar uses a mutation observer
* so i thought it would be safe to do the same in case there's a chance a conditional item changes during render
*/
const handleMutation: MutationCallback = (mutations) => {
firstObserver.disconnect();
lastObserver.disconnect();
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
resizeObserver.observe(node as Element);
});
mutation.removedNodes.forEach((node) => {
resizeObserver.unobserve(node as Element);
});
});
updateIndicator();
observeChildren();
};
const mutationObserver = new MutationObserver(handleMutation);
mutationObserver.observe(itemsList, { childList: true });
const resizeObserver = new ResizeObserver(() => {
debouncedUpdateIndicator();
scrollActiveElementIntoView();
});
Array.from(itemsList.children).forEach((item) => resizeObserver.observe(item));
resizeObserver.observe(nav);
const observerOptions = {
root: nav,
threshold: 0.75,
};
const firstObserver = new IntersectionObserver(handleFirstIntersect, observerOptions);
const lastObserver = new IntersectionObserver(handleLastIntersect, observerOptions);
observeChildren();
return () => {
mutationObserver.disconnect();
resizeObserver.disconnect();
firstObserver.disconnect();
lastObserver.disconnect();
};
}, [activeElement, nav, itemsList]);
return (
<Tabs
sx={{
width: "100%",
borderRadius: ".42rem",
}}
variant="scrollable"
scrollButtons="auto"
value={value}
allowScrollButtonsMobile
>
{children}
</Tabs>
<div className="d-flex position-relative rounded overflow-hidden px-1">
<nav className="tachi-navbar" ref={navRef}>
<div
ref={itemsListRef}
onKeyDown={handleKeyDown}
role="tablist"
className="hstack gap-4 flex-grow-1 justify-content-around "
>
{items}
</div>
<div
className="tachi-navbar-indicator"
style={indicatorStyle}
hidden={!mounted || indicatorStyle.width === 0}
/>
</nav>
{scrollButtons}
</div>
);
}
/**
* Exposes a React Router Link component accepting otherMatchingPaths
*/
export const NavbarItem = React.forwardRef<HTMLAnchorElement, NavbarItemProps>(
/* we don't want to pass otherMatchingPaths to the Link component
it is still seen by the navbar and processed accordingly */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ otherMatchingPaths, ...restProps }, ref) => <Link ref={ref} {...restProps} />
);
Navbar.Item = NavbarItem;
+18 -19
View File
@@ -3,7 +3,6 @@ import { APIFetchV1 } from "util/api";
import { HumaniseError } from "util/humanise-error";
import { SendErrorToast, SendSuccessToast } from "util/toaster";
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import MiniTable from "components/tables/components/MiniTable";
import Divider from "components/util/Divider";
import React, { useContext } from "react";
@@ -204,52 +203,52 @@ export function UGPTBottomNav({
playtype: Playtype;
}) {
const navItems = [
<NavItem key="overview" to={`${baseUrl}/`}>
<Navbar.Item key="overview" to={`${baseUrl}/`}>
Overview & Activity
</NavItem>,
<NavItem key="scores" to={`${baseUrl}/scores`}>
</Navbar.Item>,
<Navbar.Item key="scores" to={`${baseUrl}/scores`}>
Scores
</NavItem>,
<NavItem key="folders" to={`${baseUrl}/folders`}>
</Navbar.Item>,
<Navbar.Item key="folders" to={`${baseUrl}/folders`}>
Folders
</NavItem>,
<NavItem key="sessions" to={`${baseUrl}/sessions`}>
</Navbar.Item>,
<Navbar.Item key="sessions" to={`${baseUrl}/sessions`}>
Sessions
</NavItem>,
</Navbar.Item>,
];
const utilsName = GetGPTUtilsName(game, playtype, isRequestedUser);
if (utilsName) {
navItems.push(
<NavItem key="targets" to={`${baseUrl}/utils`}>
<Navbar.Item key="utils" to={`${baseUrl}/utils`}>
{utilsName}
</NavItem>
</Navbar.Item>
);
}
if (isRequestedUser) {
navItems.push(
<NavItem key="rivals" to={`${baseUrl}/rivals`}>
<Navbar.Item key="rivals" to={`${baseUrl}/rivals`}>
Rivals
</NavItem>,
<NavItem key="targets" to={`${baseUrl}/targets`}>
</Navbar.Item>,
<Navbar.Item key="targets" to={`${baseUrl}/targets`}>
Goals & Quests
</NavItem>
</Navbar.Item>
);
}
navItems.push(
<NavItem key="leaderboard" to={`${baseUrl}/leaderboard`}>
<Navbar.Item key="leaderboard" to={`${baseUrl}/leaderboard`}>
Leaderboard
</NavItem>
</Navbar.Item>
);
if (isRequestedUser) {
navItems.push(
<NavItem key="settings" to={`${baseUrl}/settings`}>
<Navbar.Item key="settings" to={`${baseUrl}/settings`}>
Settings
</NavItem>
</Navbar.Item>
);
}
+13 -18
View File
@@ -1,7 +1,6 @@
import { APIFetchV1 } from "util/api";
import { FormatDate } from "util/time";
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import Icon from "components/util/Icon";
@@ -133,24 +132,24 @@ export function UserBottomNav({ baseUrl, reqUser }: { baseUrl: string; reqUser:
const isRequestedUser = !!(user && user.id === reqUser.id);
const navItems = [
<NavItem key="about" to={`${baseUrl}/`}>
<Navbar.Item key="about" to={`${baseUrl}/`}>
Overview
</NavItem>,
<NavItem key="games" to={`${baseUrl}/games`}>
</Navbar.Item>,
<Navbar.Item key="games" to={`${baseUrl}/games`}>
Games
</NavItem>,
</Navbar.Item>,
];
if (isRequestedUser) {
navItems.push(
<NavItem key="integrations" to={`${baseUrl}/integrations`}>
<Navbar.Item key="integrations" to={`${baseUrl}/integrations`}>
Service Integrations
</NavItem>
</Navbar.Item>
);
navItems.push(
<NavItem key="imports" to={`${baseUrl}/imports`}>
<Navbar.Item key="imports" to={`${baseUrl}/imports`}>
Imports
</NavItem>
</Navbar.Item>
);
// If mandates login, assume that we also use invite codes.
@@ -158,24 +157,20 @@ export function UserBottomNav({ baseUrl, reqUser }: { baseUrl: string; reqUser:
// querying the server, but I just don't care.
if (ClientConfig.MANDATE_LOGIN) {
navItems.push(
<NavItem key="invites" to={`${baseUrl}/invites`}>
<Navbar.Item key="invites" to={`${baseUrl}/invites`}>
Invites
</NavItem>
</Navbar.Item>
);
}
navItems.push(
<NavItem key="settings" to={`${baseUrl}/settings`}>
<Navbar.Item key="settings" to={`${baseUrl}/settings`}>
Profile Settings
</NavItem>
</Navbar.Item>
);
}
return (
<div className="mx-n9 align-items-center mb-0">
<Navbar>{navItems}</Navbar>
</div>
);
return <Navbar>{navItems}</Navbar>;
}
function StatusComponent({ reqUser }: { reqUser: UserDocument }) {
+2 -2
View File
@@ -1,10 +1,10 @@
import CircularProgress from "@mui/material/CircularProgress";
import React, { CSSProperties } from "react";
import Spinner from "react-bootstrap/Spinner";
export default function Loading({ style }: { style?: CSSProperties }) {
return (
<div className="d-flex justify-content-center align-items-center w-100" style={style}>
<CircularProgress />
<Spinner variant="primary" />
</div>
);
}
+1
View File
@@ -32,6 +32,7 @@
@import "bootstrap-scss/pagination";
@import "bootstrap-scss/modal";
@import "bootstrap-scss/close";
@import "bootstrap-scss/spinners";
@import "customised-bootstrap";
@import "bootstrap-scss/utilities/api";
@@ -20,8 +20,11 @@ $utilities: map-merge(
50: 50%,
//75: 75%,
100: 100%,
//"half": 50%,
"full": 100%,
//half: 50%,
full: 100%,
min: min-content,
fit: fit-content,
max: max-content,
auto: auto,
),
),
@@ -43,8 +46,11 @@ $utilities: map-merge(
//50: 50%,
//75: 75%,
100: 100%,
//"half": 50%,
"full": 100%,
//half: 50%,
full: 100%,
min: min-content,
fit: fit-content,
max: max-content,
auto: auto,
),
),
@@ -161,7 +167,7 @@ $utilities: map-merge(
"sm": var(--#{$prefix}box-shadow-sm),
"md": var(--#{$prefix}box-shadow),
"lg": var(--#{$prefix}box-shadow-lg),
//"inset": var(--#{$prefix}box-shadow-inset),
"inset": var(--#{$prefix}box-shadow-inset),
"none": none,
),
),
+2 -1
View File
@@ -3,4 +3,5 @@
@import "folder-minimap";
@import "table";
@import "timeline";
@import "tooltip";
@import "tooltip";
@import "navbar";
+70
View File
@@ -0,0 +1,70 @@
.tachi-navbar {
display: inline-flex;
position: relative;
outline: none;
width: 100%;
background-color: var(--bs-secondary-bg);
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
@extend .scrollbar-hide;
&-item {
padding: 1rem 2rem;
outline: none;
text-decoration: none;
text-wrap: nowrap;
user-select: none;
z-index: 20;
&:focus-visible {
text-decoration: underline;
@extend .text-primary;
}
}
&-button {
%navbar-button {
position: absolute;
top: 0;
border: none;
height: 100%;
color: rgba(var(--bs-body-color-rgb), 0.8);
transition: opacity, ease-in-out, $transition-duration;
z-index: 40;
@extend .animate-fade-in;
&:hover,
&:focus {
color: rgba(var(--bs-body-color-rgb), 1);
}
}
$navbar-button-gradient: rgba(var(--bs-secondary-bg-rgb), 0) 0%,
rgba(var(--bs-secondary-bg-rgb), 1) 60%;
&-left {
@extend %navbar-button;
left: 0;
padding: 0 0.85rem 0 0.65rem;
background: linear-gradient(270deg, $navbar-button-gradient);
}
&-right {
@extend %navbar-button;
right: 0;
padding: 0 0.65rem 0 0.85rem;
background: linear-gradient(90deg, $navbar-button-gradient);
}
}
&-indicator {
position: absolute;
bottom: 2px;
height: 2px;
border-radius: var(--bs-border-radius);
background-color: var(--bs-primary);
transition-property: left, width;
transition-duration: $transition-duration-lg;
transition-timing-function: ease-in-out;
z-index: 10;
}
}
+15 -11
View File
@@ -1,23 +1,27 @@
.nav-link {
@extend .rounded;
@extend .rounded;
}
.credits-easter-egg {
transition: text-decoration-color 120ms ease-in-out;
text-decoration: underline;
text-decoration-color: transparent;
&:hover {
text-decoration-color: var(--#{$prefix}body-color);
}
transition: text-decoration-color 120ms ease-in-out;
text-decoration: underline;
text-decoration-color: transparent;
&:hover {
text-decoration-color: var(--#{$prefix}body-color);
}
}
a {
@extend .transition-color;
}
body,
header {
@extend .transition-bg;
@extend .transition-color;
@extend .transition-bg;
@extend .transition-color;
}
code {
padding: #{$kbd-padding-y} #{$kbd-padding-x};
border-radius: var(--#{$prefix}border-radius-sm);
padding: #{$kbd-padding-y} #{$kbd-padding-x};
border-radius: var(--#{$prefix}border-radius-sm);
}
@@ -40,10 +40,3 @@
transition: opacity 0.3s;
opacity: 0;
}
a:where(.MuiTab-root) {
@extend .transition-color;
&:hover {
color: var(--#{$prefix}link-hover-color)
}
}
-213
View File
@@ -170,12 +170,6 @@ importers:
'@babel/core':
specifier: 7.1.0
version: 7.1.0
'@emotion/react':
specifier: ^11.10.5
version: 11.10.5(@babel/core@7.1.0)(@types/react@17.0.13)(react@17.0.2)
'@emotion/styled':
specifier: ^11.10.5
version: 11.10.5(@babel/core@7.1.0)(@emotion/react@11.10.5)(@types/react@17.0.13)(react@17.0.2)
'@fortawesome/fontawesome-free':
specifier: 5.13.0
version: 5.13.0
@@ -188,9 +182,6 @@ importers:
'@fullcalendar/react':
specifier: ^6.0.0
version: 6.0.0(@fullcalendar/core@6.0.0)(react-dom@17.0.2)(react@17.0.2)
'@mui/material':
specifier: ^5.11.1
version: 5.11.1(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(@types/react@17.0.13)(react-dom@17.0.2)(react@17.0.2)
'@nivo/bar':
specifier: 0.79.1
version: 0.79.1(@nivo/core@0.79.0)(prop-types@15.6.0)(react-dom@17.0.2)(react@17.0.2)
@@ -2310,12 +2301,6 @@ packages:
resolution: {integrity: sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==}
dev: false
/@emotion/is-prop-valid@1.2.0:
resolution: {integrity: sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==}
dependencies:
'@emotion/memoize': 0.8.0
dev: false
/@emotion/memoize@0.8.0:
resolution: {integrity: sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==}
dev: false
@@ -2359,31 +2344,6 @@ packages:
resolution: {integrity: sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==}
dev: false
/@emotion/styled@11.10.5(@babel/core@7.1.0)(@emotion/react@11.10.5)(@types/react@17.0.13)(react@17.0.2):
resolution: {integrity: sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==}
peerDependencies:
'@babel/core': ^7.0.0
'@emotion/react': ^11.0.0-rc.0
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@babel/core':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/core': 7.1.0
'@babel/runtime': 7.20.7
'@emotion/babel-plugin': 11.10.5(@babel/core@7.1.0)
'@emotion/is-prop-valid': 1.2.0
'@emotion/react': 11.10.5(@babel/core@7.1.0)(@types/react@17.0.13)(react@17.0.2)
'@emotion/serialize': 1.1.1
'@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@17.0.2)
'@emotion/utils': 1.2.0
'@types/react': 17.0.13
react: 17.0.2
dev: false
/@emotion/unitless@0.8.0:
resolution: {integrity: sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==}
dev: false
@@ -2630,164 +2590,6 @@ packages:
dev: false
optional: true
/@mui/base@5.0.0-alpha.111(@types/react@17.0.13)(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-2wfIPpl97S4dPzD0QOM3UIzQ/EuXCYQvHmXxTpfKxev/cfkzOe7Ik/McoYUBbtM1bSOqH3W276R/L2LF9cyXqQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.7
'@emotion/is-prop-valid': 1.2.0
'@mui/types': 7.2.3(@types/react@17.0.13)
'@mui/utils': 5.11.1(react@17.0.2)
'@popperjs/core': 2.11.6
'@types/react': 17.0.13
clsx: 1.2.1
prop-types: 15.8.1
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
react-is: 18.2.0
dev: false
/@mui/core-downloads-tracker@5.11.1:
resolution: {integrity: sha512-QVqVNlZ2K+LqUDE5kFgYd0r4KekR/dv2cNYbAutQWbfOA8VPVUVrDz0ELrEcoe8TjM/CwnsmGvaDh/YSNl/ALA==}
dev: false
/@mui/material@5.11.1(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(@types/react@17.0.13)(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-yaZiXvcrl2vgUK+VO24780BWRgwdAMmAyuMVZnRTts1Yu0tWd6PjIYq2ZtaOlpj6/LbaSS+Q2kSfxYnDQ20CEQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.7
'@emotion/react': 11.10.5(@babel/core@7.1.0)(@types/react@17.0.13)(react@17.0.2)
'@emotion/styled': 11.10.5(@babel/core@7.1.0)(@emotion/react@11.10.5)(@types/react@17.0.13)(react@17.0.2)
'@mui/base': 5.0.0-alpha.111(@types/react@17.0.13)(react-dom@17.0.2)(react@17.0.2)
'@mui/core-downloads-tracker': 5.11.1
'@mui/system': 5.11.1(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(@types/react@17.0.13)(react@17.0.2)
'@mui/types': 7.2.3(@types/react@17.0.13)
'@mui/utils': 5.11.1(react@17.0.2)
'@types/react': 17.0.13
'@types/react-transition-group': 4.4.5
clsx: 1.2.1
csstype: 3.1.1
prop-types: 15.8.1
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
react-is: 18.2.0
react-transition-group: 4.4.5(react-dom@17.0.2)(react@17.0.2)
dev: false
/@mui/private-theming@5.11.1(@types/react@17.0.13)(react@17.0.2):
resolution: {integrity: sha512-nnHg7kA5RwFRhy0wiDYe59sLCVGORpPypL1JcEdhv0+N0Zbmc2E/y4z2zqMRZ62MAEscpro7cQbvv244ThA84A==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.6
'@mui/utils': 5.11.1(react@17.0.2)
'@types/react': 17.0.13
prop-types: 15.8.1
react: 17.0.2
dev: false
/@mui/styled-engine@5.11.0(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(react@17.0.2):
resolution: {integrity: sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
dependencies:
'@babel/runtime': 7.22.6
'@emotion/cache': 11.10.5
'@emotion/react': 11.10.5(@babel/core@7.1.0)(@types/react@17.0.13)(react@17.0.2)
'@emotion/styled': 11.10.5(@babel/core@7.1.0)(@emotion/react@11.10.5)(@types/react@17.0.13)(react@17.0.2)
csstype: 3.1.1
prop-types: 15.8.1
react: 17.0.2
dev: false
/@mui/system@5.11.1(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(@types/react@17.0.13)(react@17.0.2):
resolution: {integrity: sha512-BEA2S0hay8n8CcZftkeAVsi0nsb5ZjdnZRCahv5lX7QJYwDjO4ucJ6lnvxHe2v/9Te1LLjTO7ojxu/qM6CE5Cg==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.7
'@emotion/react': 11.10.5(@babel/core@7.1.0)(@types/react@17.0.13)(react@17.0.2)
'@emotion/styled': 11.10.5(@babel/core@7.1.0)(@emotion/react@11.10.5)(@types/react@17.0.13)(react@17.0.2)
'@mui/private-theming': 5.11.1(@types/react@17.0.13)(react@17.0.2)
'@mui/styled-engine': 5.11.0(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(react@17.0.2)
'@mui/types': 7.2.3(@types/react@17.0.13)
'@mui/utils': 5.11.1(react@17.0.2)
'@types/react': 17.0.13
clsx: 1.2.1
csstype: 3.1.1
prop-types: 15.8.1
react: 17.0.2
dev: false
/@mui/types@7.2.3(@types/react@17.0.13):
resolution: {integrity: sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==}
peerDependencies:
'@types/react': '*'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 17.0.13
dev: false
/@mui/utils@5.11.1(react@17.0.2):
resolution: {integrity: sha512-lMAPgIJoil8V9ZxsMbEflMsvZmWcHbRVMc4JDY9jPO9V4welpF43h/O267b1RqlcRnC5MEbVQV605GYkTZY29Q==}
engines: {node: '>=12.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.7
'@types/prop-types': 15.7.5
'@types/react-is': 17.0.3
prop-types: 15.8.1
react: 17.0.2
react-is: 18.2.0
dev: false
/@nivo/annotations@0.79.1(@nivo/core@0.79.0)(prop-types@15.6.0)(react-dom@17.0.2)(react@17.0.2):
resolution: {integrity: sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w==}
peerDependencies:
@@ -3620,12 +3422,6 @@ packages:
'@types/react': 17.0.13
dev: false
/@types/react-is@17.0.3:
resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==}
dependencies:
'@types/react': 17.0.13
dev: false
/@types/react-router-bootstrap@0.26.1:
resolution: {integrity: sha512-EdgZn/PmSA8+I0RW6WyDUgBvkVXc8AmbbgOJ8WJfec1R/V06X++OrIOzwzdgYl4g3uXvEB6oEIxzcjx1/8Opqw==}
dependencies:
@@ -4594,11 +4390,6 @@ packages:
engines: {node: '>=0.8'}
dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/cluster-key-slot@1.1.0:
resolution: {integrity: sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==}
engines: {node: '>=0.10.0'}
@@ -9066,10 +8857,6 @@ packages:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: false
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false