(feat): add theming
All checks were successful
/ build-web (push) Successful in 1m24s
/ build-desktop (linux) (push) Successful in 6m51s
/ build-mobile (push) Successful in 15m48s
/ release (push) Successful in 31s

This commit is contained in:
Alois 2026-06-15 21:13:09 +02:00
commit 040d60a35f
6 changed files with 429 additions and 315 deletions

View file

@ -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>

View file

@ -0,0 +1,5 @@
import { StylePicker } from "@tensamin/ui";
export default function Page() {
return <StylePicker />;
}

600
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -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": {

View file

@ -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",

View file

@ -46,6 +46,7 @@ const settings = {
}, },
}, },
application: { application: {
theme: {},
licenses: {}, licenses: {},
}, },
} as const satisfies SettingsSchema; } as const satisfies SettingsSchema;