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
+48
View File
@@ -0,0 +1,48 @@
import { createTheme } from "@mantine/core";
const commonInputStyles = {
label: {
padding: 5,
},
root: {
margin: "0",
},
};
// Shared by MantineProvider and the theme-picker preview iframes.
export const baseTheme = createTheme({
defaultRadius: "sm",
respectReducedMotion: true,
fontFamily: '"Inter", sans-serif',
headings: { fontFamily: '"League Spartan", sans-serif' },
activeClassName: "flxn-press",
components: {
TextInput: {
styles: commonInputStyles,
},
DateTimePicker: {
styles: commonInputStyles,
},
Input: {
styles: commonInputStyles,
},
Select: {
styles: commonInputStyles,
},
Autocomplete: {
styles: commonInputStyles,
},
Card: {
defaultProps: {
shadow: "xs",
},
},
Title: {
styles: {
root: {
letterSpacing: "-0.01em",
},
},
},
},
});
+29 -7
View File
@@ -1,18 +1,40 @@
import { useAuth } from "@/contexts/auth-context";
import { useMantineColorScheme } from "@mantine/core";
import { useEffect } from "react";
import { CHROME_COLORS, setThemeColorMeta } from "./theme-colors";
import { useEffect, useRef } from "react";
import { getTheme } from "@/themes/registry";
import { resolveChromeColors } from "@/themes/chrome";
import { setBaseThemeColor } from "./theme-colors";
const ColorSchemeProvider = ({ children }: { children: React.ReactNode }) => {
const {
metadata: { colorScheme },
metadata: { colorScheme, theme },
} = useAuth();
const { setColorScheme } = useMantineColorScheme();
const { colorScheme: currentScheme, setColorScheme } =
useMantineColorScheme();
// Mantine recreates setColorScheme every render; calling it redundantly
// triggers its global transition-suppression flash, so ref it and only
// call on an actual value change.
const setColorSchemeRef = useRef(setColorScheme);
setColorSchemeRef.current = setColorScheme;
const manifest = getTheme(theme);
const forcedScheme =
manifest.colorSchemes === "both" ? undefined : manifest.colorSchemes;
useEffect(() => {
const chrome = resolveChromeColors(manifest);
if (forcedScheme) {
setBaseThemeColor(chrome[forcedScheme].base);
return;
}
if (!colorScheme) return;
setColorScheme(colorScheme);
if (colorScheme !== currentScheme) {
setColorSchemeRef.current(colorScheme);
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
const applyThemeColor = () => {
@@ -22,7 +44,7 @@ const ColorSchemeProvider = ({ children }: { children: React.ReactNode }) => {
? "dark"
: "light"
: colorScheme;
setThemeColorMeta(CHROME_COLORS[resolved].base);
setBaseThemeColor(chrome[resolved].base);
};
applyThemeColor();
@@ -31,7 +53,7 @@ const ColorSchemeProvider = ({ children }: { children: React.ReactNode }) => {
media.addEventListener("change", applyThemeColor);
return () => media.removeEventListener("change", applyThemeColor);
}
}, [colorScheme, setColorScheme]);
}, [colorScheme, currentScheme, manifest, forcedScheme]);
return children;
};
+18 -52
View File
@@ -1,12 +1,16 @@
import { useAuth } from "@/contexts/auth-context";
import {
createTheme,
mergeThemeOverrides,
MantineProvider as MantineProviderCore,
} from "@mantine/core";
import { baseTheme } from "./base-theme";
import { DatesProvider } from "@mantine/dates";
import { useLingui } from "@lingui/react";
import type { AppLocale } from "@/lib/i18n";
import ColorSchemeProvider from "./color-scheme-provider";
import ThemeProvider from "@/themes/theme-provider";
import { DEFAULT_THEME_ID, getTheme } from "@/themes/registry";
import "@/themes/styles";
import { useState, useEffect, useMemo } from "react";
import "dayjs/locale/es-mx";
import "dayjs/locale/de";
@@ -21,15 +25,6 @@ const DAYJS_LOCALE: Record<AppLocale, string> = {
ja: "ja",
};
const commonInputStyles = {
label: {
padding: 5,
},
root: {
margin: "0",
},
};
const pressFeedbackCss = `
.flxn-press {
transition: transform 120ms cubic-bezier(0.32, 0.72, 0, 1);
@@ -46,43 +41,6 @@ const pressFeedbackCss = `
}
`;
const theme = createTheme({
defaultRadius: "sm",
respectReducedMotion: true,
fontFamily: '"Inter", sans-serif',
headings: { fontFamily: '"League Spartan", sans-serif' },
activeClassName: "flxn-press",
components: {
TextInput: {
styles: commonInputStyles,
},
DateTimePicker: {
styles: commonInputStyles,
},
Input: {
styles: commonInputStyles,
},
Select: {
styles: commonInputStyles,
},
Autocomplete: {
styles: commonInputStyles,
},
Card: {
defaultProps: {
shadow: "xs",
},
},
Title: {
styles: {
root: {
letterSpacing: "-0.01em",
},
},
},
},
});
const MantineProvider = ({ children }: { children: React.ReactNode }) => {
const { metadata } = useAuth();
const { i18n } = useLingui();
@@ -93,17 +51,23 @@ const MantineProvider = ({ children }: { children: React.ReactNode }) => {
}, []);
const colorScheme = isHydrated ? metadata.colorScheme || "auto" : "auto";
const manifest = getTheme(isHydrated ? metadata.theme : DEFAULT_THEME_ID);
const primaryColor = isHydrated ? metadata.accentColor || "blue" : "blue";
const themeWithPrimaryColor = useMemo(
() => ({ ...theme, primaryColor }),
[primaryColor]
const theme = useMemo(
() =>
mergeThemeOverrides(baseTheme, manifest.mantine ?? {}, { primaryColor }),
[manifest, primaryColor]
);
const forceColorScheme =
manifest.colorSchemes === "both" ? undefined : manifest.colorSchemes;
return (
<MantineProviderCore
defaultColorScheme={colorScheme}
theme={themeWithPrimaryColor}
forceColorScheme={forceColorScheme}
theme={theme}
>
<style href="flxn-press-feedback" precedence="medium">
{pressFeedbackCss}
@@ -113,7 +77,9 @@ const MantineProvider = ({ children }: { children: React.ReactNode }) => {
locale: DAYJS_LOCALE[i18n.locale as AppLocale] ?? "en",
}}
>
<ColorSchemeProvider>{children}</ColorSchemeProvider>
<ThemeProvider manifest={manifest} ready={isHydrated}>
<ColorSchemeProvider>{children}</ColorSchemeProvider>
</ThemeProvider>
</DatesProvider>
</MantineProviderCore>
);
+18
View File
@@ -8,6 +8,24 @@ export const CHROME_COLORS: Record<
dark: { base: "#242424", dimmed: "#161616" },
};
// theme-color meta ownership: while a drawer holds a dimmed color, base
// writes (from scheme/theme changes) must not overwrite it.
let dimmedDepth = 0;
export function setBaseThemeColor(color: string): void {
if (dimmedDepth === 0) setThemeColorMeta(color);
}
export function pushDimmedThemeColor(color: string): void {
dimmedDepth++;
setThemeColorMeta(color);
}
export function popDimmedThemeColor(base: string): void {
dimmedDepth = Math.max(0, dimmedDepth - 1);
if (dimmedDepth === 0) setThemeColorMeta(base);
}
export function setThemeColorMeta(color: string): void {
if (typeof document === "undefined") return;
+2 -2
View File
@@ -21,7 +21,7 @@ function success(toast: Omit<ToastProps, "id"> | string) {
const config = typeof toast === "string" ? { description: toast } : toast;
return makeToast({
...config,
icon: <ShieldCheckIcon color="lightgreen" size={48} weight="fill" />,
icon: <ShieldCheckIcon color="var(--flxn-toast-success)" size={48} weight="fill" />,
});
}
@@ -29,7 +29,7 @@ function error(toast: Omit<ToastProps, "id"> | string) {
const config = typeof toast === "string" ? { description: toast } : toast;
return makeToast({
...config,
icon: <WarningCircleIcon color="lightcoral" size={48} weight="fill" />,
icon: <WarningCircleIcon color="var(--flxn-toast-error)" size={48} weight="fill" />,
});
}