Merge branch 'style' into development

This commit is contained in:
2026-07-12 21:30:02 -07:00
72 changed files with 1547 additions and 472 deletions
+5 -4
View File
@@ -1,4 +1,4 @@
import { Box } from "@mantine/core"
import { UnstyledButton } from "@mantine/core"
import { ArrowLeftIcon } from "@phosphor-icons/react"
import { useRouter } from "@tanstack/react-router"
@@ -6,15 +6,16 @@ const BackButton = ({ top=20, left=20 }: { top?: number, left?: number }) => {
const router = useRouter()
return (
<Box
style={{ cursor: 'pointer', zIndex: 1000 }}
<UnstyledButton
aria-label='Go back'
style={{ cursor: 'pointer', zIndex: 1000, display: 'flex' }}
onClick={() => router.history.back()}
pos='absolute'
left={left}
top={top}
>
<ArrowLeftIcon weight='bold' size={20} />
</Box>
</UnstyledButton>
);
}
+15 -5
View File
@@ -1,19 +1,29 @@
import { Title, AppShell, Flex, Box, Paper } from "@mantine/core";
import { Title, AppShell, Flex } from "@mantine/core";
import { HeaderConfig } from "../types/header-config";
import BackButton from "./back-button";
interface HeaderProps extends HeaderConfig {}
interface HeaderProps extends HeaderConfig {
elevated?: boolean;
}
const Header = ({ collapsed, title, withBackButton }: HeaderProps) => {
const Header = ({ collapsed, title, withBackButton, elevated }: HeaderProps) => {
return (
<AppShell.Header
id='app-header'
display={collapsed ? 'none' : 'flex'}
style={{ alignItems: 'center', justifyContent: 'center' }}
withBorder={false}
style={{
alignItems: 'center',
justifyContent: 'center',
borderBottom: `1px solid ${elevated ? 'var(--app-shell-border-color, var(--mantine-color-default-border))' : 'transparent'}`,
transition: 'border-color 200ms ease-out',
}}
>
{ withBackButton && <BackButton /> }
<Flex justify='center' px='md' mt={8}>
<Title order={1}>{title?.toLocaleUpperCase()}</Title>
<Title order={1} lts='0.08em' style={{ userSelect: 'none' }}>
{title?.toLocaleUpperCase()}
</Title>
</Flex>
</AppShell.Header>
);
+4 -3
View File
@@ -6,6 +6,7 @@ import Pullable from './pullable';
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
import useRouterConfig from '../hooks/use-router-config';
import Page from '@/components/page';
import './route-transition.css';
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
const { header, withPadding, fullWidth } = useRouterConfig();
@@ -35,15 +36,15 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
// top: viewport.top
}}
>
<Header {...header} />
<Header {...header} elevated={scrollPosition.y > 2} />
<AppShell.Main
pos='relative'
h='100%'
mah='100%'
pb={{ base: 65, sm: 0 }}
pb={{ base: 'calc(65px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
maw='100dvw'
style={{ transition: 'none', overflow: 'hidden' }}
style={{ overflow: 'hidden' }}
>
<Pullable>
<Page noPadding={!withPadding} fullWidth={fullWidth}>
@@ -34,6 +34,8 @@ export const NavLink = ({
to={href}
className={styles.navLinkBox}
p={{ base: 0, sm: 8 }}
aria-label={label}
aria-current={isActive ? "page" : undefined}
>
<Flex
direction={{ base: "column", md: "row" }}
@@ -2,5 +2,26 @@
text-decoration: none;
border-radius: var(--mantine-radius-md);
color: unset;
min-width: 2.75rem;
min-height: 2.75rem;
width: fit-content;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.navLinkBox svg,
.navLinkBox p {
transition: color 150ms ease-out;
}
@media (prefers-reduced-motion: no-preference) {
.navLinkBox {
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
}
.navLinkBox:active {
transform: scale(0.92);
}
}
+5 -7
View File
@@ -1,4 +1,4 @@
import { AppShell, ScrollArea, Stack, Group, Paper, useMantineColorScheme } from "@mantine/core";
import { AppShell, ScrollArea, Stack, Group, Paper } from "@mantine/core";
import { NavLink } from "./nav-link";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { useAuth } from "@/contexts/auth-context";
@@ -9,18 +9,16 @@ import { useIsPWA } from "@/hooks/use-is-pwa";
const Navbar = () => {
const { user, roles } = useAuth()
const isMobile = useIsMobile();
const { colorScheme } = useMantineColorScheme();
const isPWA = useIsPWA();
const links = useLinks(user?.id, roles);
const isDark = colorScheme === 'dark';
const borderColor = isDark ? 'var(--mantine-color-dimmed)' : 'black';
const boxShadowColor = isDark ? 'var(--mantine-color-dimmed)' : 'black';
// boxShadow: `5px 5px ${boxShadowColor}`, borderColor
const bottomOffset = isPWA
? 'max(env(safe-area-inset-bottom, 0px), 0.5rem)'
: 'env(safe-area-inset-bottom, 0px)';
if (isMobile) return (
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={isPWA ? '1rem' : '0'} style={{ zIndex: 10 }}>
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={bottomOffset} style={{ zIndex: 10 }}>
<Group gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
{links.map((link) => (
<NavLink key={link.href} {...link} />
+18 -19
View File
@@ -1,17 +1,19 @@
import { ActionIcon, Box, Button, Flex, ScrollArea } from "@mantine/core";
import { Box, Flex, ScrollArea } from "@mantine/core";
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react";
import useAppShellHeight from "@/hooks/use-appshell-height";
import { ArrowClockwiseIcon, SpinnerIcon } from "@phosphor-icons/react";
import { SpinnerIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import useRouterConfig from "../hooks/use-router-config";
import { useLocation } from "@tanstack/react-router";
const THRESHOLD = 80;
/**
* Pullable is a component that allows the user to pull down to refresh the page
*/
const Pullable: React.FC<PropsWithChildren> = ({ children }) => {
interface PullableProps extends PropsWithChildren {
scrollPosition: { x: number, y: number };
onScrollPositionChange: (position: { x: number, y: number }) => void;
}
const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollPositionChange }) => {
const height = useAppShellHeight();
const [isRefreshing, setIsRefreshing] = useState(false);
const [scrolling, setScrolling] = useState(false);
@@ -31,16 +33,18 @@ const Pullable: React.FC<PropsWithChildren> = ({ children }) => {
const onTrigger = useCallback(async () => {
setIsRefreshing(true);
const minSpinnerDisplay = new Promise((resolve) => setTimeout(resolve, 400));
if (refresh.length > 0) {
// TODO: Remove this after testing - or does the delay help ux?
await new Promise(resolve => setTimeout(resolve, 1000));
refresh.forEach(async (queryKey) => {
const keyArray = Array.isArray(queryKey) ? queryKey : [queryKey];
await queryClient.refetchQueries({ queryKey: keyArray, exact: true });
});
await Promise.all(
refresh.map((queryKey) => {
const keyArray = Array.isArray(queryKey) ? queryKey : [queryKey];
return queryClient.refetchQueries({ queryKey: keyArray, exact: true });
})
);
}
await minSpinnerDisplay;
setIsRefreshing(false);
}, [refresh]);
}, [refresh, queryClient]);
useEffect(() => {
if (!isRefreshing && scrollY > THRESHOLD) {
@@ -87,7 +91,6 @@ const Pullable: React.FC<PropsWithChildren> = ({ children }) => {
return () => void ac.abort();
}, []);
// Fix wheel scrolling over child elements
useEffect(() => {
const scrollWrapper = document.getElementById('scroll-wrapper');
if (!scrollWrapper) return;
@@ -98,33 +101,29 @@ const Pullable: React.FC<PropsWithChildren> = ({ children }) => {
const handleWheel = (e: WheelEvent) => {
const target = e.target as HTMLElement;
// Check if the target is inside a nested scrollable container
let element = target;
while (element && element !== viewport) {
const overflow = window.getComputedStyle(element).overflow;
const overflowY = window.getComputedStyle(element).overflowY;
const overflowX = window.getComputedStyle(element).overflowX;
// If we found a scrollable ancestor (not the main viewport), don't interfere
if (
(overflow === 'auto' || overflow === 'scroll' ||
overflowY === 'auto' || overflowY === 'scroll' ||
overflowX === 'auto' || overflowX === 'scroll') &&
element !== viewport
) {
// Check if this element can actually scroll in the wheel direction
const canScrollY = element.scrollHeight > element.clientHeight;
const canScrollX = element.scrollWidth > element.clientWidth;
if ((e.deltaY !== 0 && canScrollY) || (e.deltaX !== 0 && canScrollX)) {
return; // Let the nested scroller handle it
return;
}
}
element = element.parentElement as HTMLElement;
}
// No nested scroller found, scroll the main viewport
viewport.scrollTop += e.deltaY;
viewport.scrollLeft += e.deltaX;
};
@@ -0,0 +1,29 @@
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root) {
animation: flxn-route-fade-out 120ms ease-out both;
}
::view-transition-new(root) {
animation: flxn-route-fade-in 200ms ease-out both;
}
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
@keyframes flxn-route-fade-out {
to {
opacity: 0;
}
}
@keyframes flxn-route-fade-in {
from {
opacity: 0;
}
}
@@ -1,4 +1,4 @@
import { Box } from "@mantine/core"
import { UnstyledButton } from "@mantine/core"
import { GearIcon } from "@phosphor-icons/react"
import { useNavigate } from "@tanstack/react-router"
import { memo } from "react";
@@ -13,16 +13,17 @@ const SettingsButton = ({ to }: SettingButtonProps) => {
const navigate = useNavigate();
return (
<Box
style={{ cursor: 'pointer', zIndex: 1000 }}
<UnstyledButton
aria-label='Settings'
style={{ cursor: 'pointer', zIndex: 1000, display: 'flex' }}
onClick={() => navigate({ to })}
pos='absolute'
right={20}
top={6}
>
<GearIcon weight='bold' size={20} />
</Box>
</UnstyledButton>
);
}
export default memo(SettingsButton, (prev, next) => prev.to !== next.to);
export default memo(SettingsButton, (prev, next) => prev.to === next.to);