themes lol

This commit is contained in:
2026-08-23 17:17:04 -07:00
parent 0a7b5fa00f
commit 6f575d6a23
79 changed files with 13108 additions and 278 deletions
+28 -8
View File
@@ -1,7 +1,7 @@
import {
Avatar as MantineAvatar,
AvatarProps as MantineAvatarProps,
Paper,
Box,
Modal,
Image,
Group,
@@ -22,10 +22,17 @@ interface AvatarProps
contain?: boolean;
}
// Mantine radius tokens ("sm", "xl") are valid as props but not as raw CSS
// values — resolve them before using in style/borderRadius.
const resolveRadius = (radius: string | number) =>
typeof radius === "string" && /^[a-z]+$/.test(radius)
? `var(--mantine-radius-${radius})`
: radius;
const Avatar = ({
name,
size = 35,
radius = "100%",
radius = "var(--flxn-avatar-radius, 50%)",
withBorder = true,
disableFullscreen = false,
contain = false,
@@ -34,6 +41,7 @@ const Avatar = ({
const { t } = useLingui();
const [isFullscreenOpen, setIsFullscreenOpen] = useState(false);
const hasImage = Boolean(props.src);
const frameRadius = resolveRadius(radius);
const handleAvatarClick = () => {
if (hasImage && !disableFullscreen) {
@@ -43,11 +51,20 @@ const Avatar = ({
return (
<>
<Paper
p={size / 20}
radius={radius}
withBorder={withBorder}
<Box
className="flxn-avatar-frame"
p={withBorder ? size / 20 : 0}
style={{
// Plain Box, not Paper: the frame must own the avatar's single
// border (via --flxn-avatar-border) and stay immune to generic
// theme Paper restyles; shape comes from --flxn-avatar-radius.
borderRadius: frameRadius,
border: withBorder
? 'var(--flxn-avatar-border, 1px solid var(--mantine-color-default-border))'
: undefined,
backgroundColor: withBorder
? 'var(--mantine-color-body)'
: undefined,
cursor: hasImage && !disableFullscreen ? 'pointer' : 'default',
transition: 'transform 0.15s ease',
}}
@@ -69,14 +86,17 @@ const Avatar = ({
size={size}
radius={radius}
w={size}
{...props}
styles={{
root: {
borderRadius: frameRadius,
},
image: {
objectFit: contain ? 'contain' : 'cover',
},
}}
{...props}
/>
</Paper>
</Box>
<Modal
opened={isFullscreenOpen}
+13 -5
View File
@@ -1,7 +1,13 @@
import { Box, Container, Title, useComputedColorScheme } from "@mantine/core";
import { PropsWithChildren, useEffect, useRef } from "react";
import { Drawer as VaulDrawer } from "vaul";
import { CHROME_COLORS, setThemeColorMeta } from "@/lib/mantine/theme-colors";
import {
pushDimmedThemeColor,
popDimmedThemeColor,
} from "@/lib/mantine/theme-colors";
import { getTheme } from "@/themes/registry";
import { resolveChromeColors } from "@/themes/chrome";
import { useAuth } from "@/contexts/auth-context";
import styles from "./styles.module.css";
interface DrawerProps extends PropsWithChildren {
@@ -20,22 +26,24 @@ const Drawer: React.FC<DrawerProps> = ({
onExited,
}) => {
const colorScheme = useComputedColorScheme("light");
const { metadata } = useAuth();
const contentRef = useRef<HTMLDivElement>(null);
const themeId = metadata.theme;
useEffect(() => {
if (!opened) return;
const appElement = document.querySelector<HTMLElement>(".app");
const colors = CHROME_COLORS[colorScheme];
const colors = resolveChromeColors(getTheme(themeId))[colorScheme];
appElement?.classList.add("drawer-scaling");
setThemeColorMeta(colors.dimmed);
pushDimmedThemeColor(colors.dimmed);
return () => {
appElement?.classList.remove("drawer-scaling");
setThemeColorMeta(colors.base);
popDimmedThemeColor(colors.base);
};
}, [opened, colorScheme]);
}, [opened, colorScheme, themeId]);
useEffect(() => {
if (!opened || !contentRef.current) return;
+1 -1
View File
@@ -1,7 +1,7 @@
.drawerOverlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.4);
background-color: var(--flxn-overlay);
z-index: 101;
}
+25 -23
View File
@@ -48,6 +48,8 @@ function SwipeableTabs({
}, [search?.tab, tabs, defaultTab]);
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
const [stuck, setStuck] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const controlsRefs = useRef<Record<number, HTMLSpanElement | null>>({});
const slideRefs = useRef<Record<number, HTMLDivElement | null>>({});
@@ -132,6 +134,20 @@ function SwipeableTabs({
};
}, [activeTab, updateHeight]);
// A 1px sentinel above the sticky strip: once it scrolls out of view the
// strip is pinned — flip data-stuck so themes can swap flat → docked chrome.
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
([entry]) => setStuck(!entry.isIntersecting),
{ threshold: 0 }
);
observer.observe(sentinel);
return () => observer.disconnect();
}, []);
const setControlRef = useCallback(
(index: number) => (node: HTMLSpanElement | null) => {
controlsRefs.current[index] = node;
@@ -148,8 +164,11 @@ function SwipeableTabs({
return (
<Box style={{ touchAction: "pan-y" }}>
<div ref={sentinelRef} aria-hidden style={{ height: 1, marginTop: -1 }} />
<Box
ref={setRootRef}
className="flxn-tabs"
data-stuck={stuck || undefined}
pos="sticky"
top={0}
style={{
@@ -157,45 +176,28 @@ function SwipeableTabs({
paddingInline: "var(--mantine-spacing-md)",
marginBottom: mb !== undefined ? mb : "var(--mantine-spacing-md)",
zIndex: 100,
backgroundColor: "var(--mantine-color-body)",
}}
>
<FloatingIndicator
target={controlsRefs.current[activeTab]}
parent={rootRef}
styles={{
root: {
borderBottom: "2px solid var(--mantine-primary-color-filled)",
paddingInline: "0.5rem",
},
}}
className="flxn-tabs-indicator"
/>
{tabs.map((tab, index) => (
<UnstyledButton
key={`${tab.label}-${index}`}
onClick={() => changeTab(index)}
style={{
flex: 1,
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-xs)",
textAlign: "center",
color:
activeTab === index
? "var(--mantine-primary-color-filled)"
: "var(--mantine-color-text)",
fontWeight: activeTab === index ? 600 : 400,
transition: "color 200ms ease, font-weight 200ms ease",
backgroundColor: "transparent",
border: "none",
borderRadius: 0,
}}
className="flxn-tab"
data-active={activeTab === index || undefined}
>
<Text
size="sm"
component="span"
style={{
// the FloatingIndicator sizes to this span — keep padding symmetric
display: "inline-block",
paddingInline: "0.5rem",
paddingBottom: "0.25rem",
paddingInline: "0.875rem",
paddingBlock: "0.375rem",
}}
ref={setControlRef(index)}
>
+3 -3
View File
@@ -62,7 +62,7 @@ const TeamAvatar = ({
top: -crownSize * 1.1,
left: "45%",
transform: "translateX(-50%)",
color: "gold",
color: "var(--flxn-gold)",
rotate: "-5deg",
}}
>
@@ -83,7 +83,7 @@ const TeamAvatar = ({
top: -crownSize * 0.95,
left: "65%",
transform: "translateX(-50%)",
color: "gold",
color: "var(--flxn-gold)",
rotate: "10deg",
}}
>
@@ -116,7 +116,7 @@ const TeamAvatar = ({
top: -crownSize * 0.6,
left: -crownSize * 0.25,
transform: "rotate(-25deg)",
color: "gold",
color: "var(--flxn-gold)",
}}
>
<CrownIcon size={crownSize} weight="fill" />