mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Remove next-themes from dashboard
This commit is contained in:
parent
53c1c9e985
commit
9c0d4e058f
@ -83,7 +83,6 @@
|
||||
"jose": "^6.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "16.1.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"posthog-js": "^1.336.1",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "^9.6.7",
|
||||
|
||||
@ -17,7 +17,7 @@ import { PageLayout } from "../page-layout";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
import { getSvixResult } from "./utils";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { AppPortal } from "svix-react";
|
||||
import "svix-react/style.css";
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import { DevErrorNotifier } from '@/components/dev-error-notifier';
|
||||
import { RouterProvider } from '@/components/router';
|
||||
import { SiteLoadingIndicatorDisplay } from '@/components/site-loading-indicator';
|
||||
import { StyleLink } from '@/components/style-link';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import { Toaster, cn } from '@/components/ui';
|
||||
import { getPublicEnvVar } from '@/lib/env';
|
||||
import { stackServerApp } from '@/stack';
|
||||
@ -88,6 +87,9 @@ export default function RootLayout({
|
||||
...attributes,
|
||||
});
|
||||
})}
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
||||
<script dangerouslySetInnerHTML={{ __html: "(function(){try{var t=localStorage.getItem('theme');var d=document.documentElement;var r=t==='dark'||t==='light'?t:window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';d.classList.add(r);d.style.colorScheme=r}catch(e){}})()" }} />
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
@ -98,20 +100,18 @@ export default function RootLayout({
|
||||
>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
<ThemeProvider>
|
||||
<StackProvider app={stackServerApp} lang={translationLocale as any}>
|
||||
<StackTheme>
|
||||
<ClientPolyfill />
|
||||
<RouterProvider>
|
||||
<UserIdentity />
|
||||
<VersionAlerter />
|
||||
<BackgroundShine />
|
||||
{children}
|
||||
<DevelopmentPortDisplay />
|
||||
</RouterProvider>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
</ThemeProvider>
|
||||
<StackProvider app={stackServerApp} lang={translationLocale as any}>
|
||||
<StackTheme>
|
||||
<ClientPolyfill />
|
||||
<RouterProvider>
|
||||
<UserIdentity />
|
||||
<VersionAlerter />
|
||||
<BackgroundShine />
|
||||
{children}
|
||||
<DevelopmentPortDisplay />
|
||||
</RouterProvider>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
<DevErrorNotifier />
|
||||
<Toaster />
|
||||
<SiteLoadingIndicatorDisplay />
|
||||
|
||||
@ -8,7 +8,7 @@ import { loadConnectAndInitialize } from "@stripe/connect-js";
|
||||
import {
|
||||
ConnectComponentsProvider,
|
||||
} from "@stripe/react-connect-js";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { useEffect } from "react";
|
||||
import { appearanceVariablesForTheme } from "./stripe-theme-variables";
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { getPublicEnvVar } from "@/lib/env";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { useMemo } from "react";
|
||||
import { appearanceVariablesForTheme } from "./stripe-theme-variables";
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider as NextThemeProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemeProvider attribute="class">
|
||||
{props.children}
|
||||
</NextThemeProvider>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { Button } from "@/components/ui";
|
||||
import { MoonIcon, SunIcon } from "@phosphor-icons/react";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
|
||||
type ViewTransitionWithReady = {
|
||||
ready: Promise<void>,
|
||||
@ -14,21 +14,11 @@ type DocumentWithViewTransition = globalThis.Document & {
|
||||
const TRANSITION_DURATION_MS = 600;
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const isReady = resolvedTheme === "dark" || resolvedTheme === "light";
|
||||
const { resolvedTheme, setTheme, mounted } = useTheme();
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
|
||||
if (typeof document === "undefined") {
|
||||
setTheme(nextTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const documentWithTransition: DocumentWithViewTransition = document;
|
||||
|
||||
@ -90,7 +80,7 @@ export default function ThemeToggle() {
|
||||
size="icon"
|
||||
className="w-8 h-8 hover:bg-muted/50"
|
||||
onClick={handleToggle}
|
||||
disabled={!isReady}
|
||||
disabled={!mounted}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<SunIcon className="hidden dark:block w-4 h-4" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Editor, { Monaco } from '@monaco-editor/react';
|
||||
import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises';
|
||||
import { deindent } from '@stackframe/stack-shared/dist/utils/strings';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { dtsBundles } from './dts';
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
|
||||
@ -1,45 +1,136 @@
|
||||
import { useEffect, useState } from "react";
|
||||
'use client';
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
type ResolvedTheme = 'light' | 'dark';
|
||||
|
||||
const STORAGE_KEY = 'theme';
|
||||
|
||||
// --- Theme preference store (module-level singleton) ---
|
||||
|
||||
let currentTheme: Theme = 'system';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'dark' || stored === 'light' || stored === 'system') {
|
||||
currentTheme = stored;
|
||||
}
|
||||
} catch { /* localStorage unavailable (eg. private browsing) */ }
|
||||
}
|
||||
|
||||
const themeListeners = new Set<() => void>();
|
||||
|
||||
function notifyThemeListeners() {
|
||||
for (const listener of themeListeners) listener();
|
||||
}
|
||||
|
||||
function subscribeTheme(listener: () => void): () => void {
|
||||
themeListeners.add(listener);
|
||||
return () => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getThemeSnapshot(): Theme {
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
function getThemeServerSnapshot(): Theme {
|
||||
return 'system';
|
||||
}
|
||||
|
||||
// --- System theme preference (media query) ---
|
||||
|
||||
const darkMq = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)')
|
||||
: null;
|
||||
|
||||
const systemListeners = new Set<() => void>();
|
||||
|
||||
if (darkMq) {
|
||||
darkMq.addEventListener('change', () => {
|
||||
if (currentTheme === 'system') {
|
||||
applyThemeToDOM(darkMq.matches ? 'dark' : 'light');
|
||||
}
|
||||
for (const listener of systemListeners) listener();
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeSystem(listener: () => void): () => void {
|
||||
systemListeners.add(listener);
|
||||
return () => {
|
||||
systemListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function getSystemSnapshot(): ResolvedTheme {
|
||||
return darkMq?.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getSystemServerSnapshot(): ResolvedTheme {
|
||||
return 'light';
|
||||
}
|
||||
|
||||
// --- Mounted (server=false, client=true; handled via useSyncExternalStore hydration) ---
|
||||
|
||||
function subscribeMounted(): () => void {
|
||||
return () => {};
|
||||
}
|
||||
function getMountedClient(): boolean {
|
||||
return true;
|
||||
}
|
||||
function getMountedServer(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- DOM helpers ---
|
||||
|
||||
function applyThemeToDOM(resolved: ResolvedTheme) {
|
||||
if (typeof document === 'undefined') return;
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(resolved);
|
||||
root.style.colorScheme = resolved;
|
||||
}
|
||||
|
||||
function resolve(theme: Theme, system: ResolvedTheme): ResolvedTheme {
|
||||
return theme === 'system' ? system : theme;
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
export function setTheme(theme: Theme) {
|
||||
currentTheme = theme;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
} catch { /* localStorage unavailable */ }
|
||||
applyThemeToDOM(resolve(theme, getSystemSnapshot()));
|
||||
notifyThemeListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current theme of the page.
|
||||
* Hook that provides the current theme, resolved theme, and a setter.
|
||||
*
|
||||
* @returns the current theme and a boolean indicating if the theme has been mounted
|
||||
*
|
||||
* Note that this doesn't work in the server component. Please use the css variables for server side rendering.
|
||||
* Uses `useSyncExternalStore` to subscribe to both the user's preference
|
||||
* (persisted in localStorage) and the system `prefers-color-scheme` query.
|
||||
* No `useEffect` required.
|
||||
*/
|
||||
export function useTheme() {
|
||||
const theme = useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeServerSnapshot);
|
||||
const systemTheme = useSyncExternalStore(subscribeSystem, getSystemSnapshot, getSystemServerSnapshot);
|
||||
const resolvedTheme = resolve(theme, systemTheme);
|
||||
const mounted = useSyncExternalStore(subscribeMounted, getMountedClient, getMountedServer);
|
||||
|
||||
return { theme, resolvedTheme, systemTheme, setTheme, mounted };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience hook that returns just the resolved theme and mounted state.
|
||||
* Useful for components that only need to read the current theme.
|
||||
*/
|
||||
export function useThemeWatcher() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
const updateTheme = () => {
|
||||
const themeEl = document.getElementById('--stack-theme-mode');
|
||||
setTheme(themeEl?.getAttribute('data-stack-theme') === 'dark' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-stack-theme') {
|
||||
updateTheme();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const themeEl = document.getElementById('--stack-theme-mode');
|
||||
if (themeEl) {
|
||||
updateTheme();
|
||||
observer.observe(themeEl, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-stack-theme']
|
||||
});
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return { mounted, theme };
|
||||
const { resolvedTheme, mounted } = useTheme();
|
||||
return { theme: resolvedTheme, mounted };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user