(feat): add theming
This commit is contained in:
parent
87e4a158e3
commit
040d60a35f
6 changed files with 429 additions and 315 deletions
|
|
@ -31,10 +31,10 @@ import NotificationsProvider from "@tensamin/notifications/context";
|
||||||
|
|
||||||
import TAuthWrapper from "@tensamin/tauth/context";
|
import TAuthWrapper from "@tensamin/tauth/context";
|
||||||
|
|
||||||
import { ThemeProvider } from "@tensamin/ui";
|
import { ThemeProvider, useTheme } from "@tensamin/ui";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
import Storage from "@tensamin/storage/context";
|
import Storage from "@tensamin/storage/context";
|
||||||
import Session from "@tensamin/storage/session";
|
import Session from "@tensamin/storage/session";
|
||||||
|
|
@ -106,12 +106,111 @@ function LoginWrapper({ children }: { children: ReactNode }) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ThemeStorageBridge() {
|
||||||
|
const { load, save } = useStorage();
|
||||||
|
const {
|
||||||
|
themeColor,
|
||||||
|
setThemeColor,
|
||||||
|
themePalette,
|
||||||
|
setThemePalette,
|
||||||
|
themePrimaryColor,
|
||||||
|
setThemePrimaryColor,
|
||||||
|
themePolarity,
|
||||||
|
setThemePolarity,
|
||||||
|
themeTint,
|
||||||
|
setThemeTint,
|
||||||
|
} = useTheme();
|
||||||
|
const loadedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
load("theme_color"),
|
||||||
|
load("theme_palette"),
|
||||||
|
load("theme_primary_color"),
|
||||||
|
load("theme_polarity"),
|
||||||
|
load("theme_tint"),
|
||||||
|
]).then(([color, palette, primaryColor, polarity, tint]) => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThemeColor(color);
|
||||||
|
setThemePalette(palette);
|
||||||
|
setThemePrimaryColor(primaryColor);
|
||||||
|
setThemePolarity(polarity);
|
||||||
|
setThemeTint(tint);
|
||||||
|
loadedRef.current = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
load,
|
||||||
|
setThemeColor,
|
||||||
|
setThemePalette,
|
||||||
|
setThemePolarity,
|
||||||
|
setThemePrimaryColor,
|
||||||
|
setThemeTint,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
save("theme_color", themeColor);
|
||||||
|
}, [save, themeColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
save("theme_palette", themePalette);
|
||||||
|
}, [save, themePalette]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
save("theme_primary_color", themePrimaryColor);
|
||||||
|
}, [save, themePrimaryColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
save("theme_polarity", themePolarity);
|
||||||
|
}, [save, themePolarity]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
save("theme_tint", themeTint);
|
||||||
|
}, [save, themeTint]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function RootShell() {
|
function RootShell() {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeeplinkContext>
|
<DeeplinkContext>
|
||||||
<ThemeProvider>
|
<ThemeProvider
|
||||||
|
storageKey={null}
|
||||||
|
colorStorageKey={null}
|
||||||
|
paletteStorageKey={null}
|
||||||
|
primaryColorStorageKey={null}
|
||||||
|
tintStorageKey={null}
|
||||||
|
>
|
||||||
<div className="w-screen h-dvh overflow-hidden">
|
<div className="w-screen h-dvh overflow-hidden">
|
||||||
<Toaster
|
<Toaster
|
||||||
position={isMobile ? "top-center" : "bottom-right"}
|
position={isMobile ? "top-center" : "bottom-right"}
|
||||||
|
|
@ -125,6 +224,7 @@ function RootShell() {
|
||||||
/>
|
/>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Storage>
|
<Storage>
|
||||||
|
<ThemeStorageBridge />
|
||||||
<LoginWrapper>
|
<LoginWrapper>
|
||||||
<LegalWrapper>
|
<LegalWrapper>
|
||||||
<Crypto>
|
<Crypto>
|
||||||
|
|
|
||||||
5
apps/web/src/routes/settings/theme.tsx
Normal file
5
apps/web/src/routes/settings/theme.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { StylePicker } from "@tensamin/ui";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <StylePicker />;
|
||||||
|
}
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
"typescript-eslint": "^8.59.1"
|
"typescript-eslint": "^8.59.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz",
|
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.37.tar.gz",
|
||||||
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz"
|
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.24.tar.gz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,26 @@ export type Communities = z.infer<
|
||||||
>;
|
>;
|
||||||
export type Calls = z.infer<typeof ttp.challenge_response.response.shape.calls>;
|
export type Calls = z.infer<typeof ttp.challenge_response.response.shape.calls>;
|
||||||
|
|
||||||
|
type Base16Palette = Record<
|
||||||
|
| "base00"
|
||||||
|
| "base01"
|
||||||
|
| "base02"
|
||||||
|
| "base03"
|
||||||
|
| "base04"
|
||||||
|
| "base05"
|
||||||
|
| "base06"
|
||||||
|
| "base07"
|
||||||
|
| "base08"
|
||||||
|
| "base09"
|
||||||
|
| "base0A"
|
||||||
|
| "base0B"
|
||||||
|
| "base0C"
|
||||||
|
| "base0D"
|
||||||
|
| "base0E"
|
||||||
|
| "base0F",
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
// TTP
|
// TTP
|
||||||
const user = z.object({
|
const user = z.object({
|
||||||
about: z.string().max(255).optional(),
|
about: z.string().max(255).optional(),
|
||||||
|
|
@ -260,6 +280,11 @@ export interface Storage extends SettingsStorageDefaults {
|
||||||
ttp_server_cert: string;
|
ttp_server_cert: string;
|
||||||
call_mute_range_start: number;
|
call_mute_range_start: number;
|
||||||
call_mute_range_end: number;
|
call_mute_range_end: number;
|
||||||
|
theme_color: string;
|
||||||
|
theme_palette: Base16Palette | null;
|
||||||
|
theme_primary_color: string;
|
||||||
|
theme_polarity: "dark" | "light" | "system";
|
||||||
|
theme_tint: "soft" | "hard" | "extreme";
|
||||||
chat_trusted_domains: string[];
|
chat_trusted_domains: string[];
|
||||||
chat_picker_saved_media: string[];
|
chat_picker_saved_media: string[];
|
||||||
chat_picker_last_tab: "gif" | "meme" | "saved";
|
chat_picker_last_tab: "gif" | "meme" | "saved";
|
||||||
|
|
@ -303,6 +328,11 @@ export const storageDefaults: Storage = {
|
||||||
ttp_server_cert: "",
|
ttp_server_cert: "",
|
||||||
call_mute_range_start: -55,
|
call_mute_range_start: -55,
|
||||||
call_mute_range_end: -45,
|
call_mute_range_end: -45,
|
||||||
|
theme_color: "",
|
||||||
|
theme_palette: null,
|
||||||
|
theme_primary_color: "",
|
||||||
|
theme_polarity: "system",
|
||||||
|
theme_tint: "soft",
|
||||||
chat_trusted_domains: [
|
chat_trusted_domains: [
|
||||||
// Other platforms
|
// Other platforms
|
||||||
"cdn.discordapp.com",
|
"cdn.discordapp.com",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ const settings = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
application: {
|
application: {
|
||||||
|
theme: {},
|
||||||
licenses: {},
|
licenses: {},
|
||||||
},
|
},
|
||||||
} as const satisfies SettingsSchema;
|
} as const satisfies SettingsSchema;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue