themes lol
This commit is contained in:
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user