mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Added SsrScript to work around suspended script (#99)
* simplified browser script * improved code structure * added ssr script
This commit is contained in:
parent
02568ba36d
commit
fd40498116
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Container } from "@stackframe/stack-ui";
|
||||
import React, { useEffect, useId } from "react";
|
||||
import React, { useId } from "react";
|
||||
import { SsrScript } from "./ssr-layout-effect";
|
||||
|
||||
export function MaybeFullPage({
|
||||
children,
|
||||
@ -23,13 +24,6 @@ export function MaybeFullPage({
|
||||
el.style.minHeight = \`calc(100vh - \${offset}px)\`;
|
||||
})(${JSON.stringify([id])})`;
|
||||
|
||||
useEffect(() => {
|
||||
// TODO fix workaround: React has a bug where it doesn't run the script on the first CSR render if SSR has been skipped due to suspense
|
||||
// As a workaround, we run the script in the <script> tag again after the first render
|
||||
// Note that we do an indirect eval as described here: https://esbuild.github.io/content-types/#direct-eval
|
||||
(0, eval)(scriptString);
|
||||
}, []);
|
||||
|
||||
if (fullPage) {
|
||||
return (
|
||||
<>
|
||||
@ -49,9 +43,7 @@ export function MaybeFullPage({
|
||||
{children}
|
||||
</Container>
|
||||
</div>
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: scriptString,
|
||||
}} />
|
||||
<SsrScript script={scriptString} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
|
||||
13
packages/stack/src/components/elements/ssr-layout-effect.tsx
Normal file
13
packages/stack/src/components/elements/ssr-layout-effect.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export function SsrScript(props: { script: string }) {
|
||||
useLayoutEffect(() => {
|
||||
// TODO fix workaround: React has a bug where it doesn't run the script on the first CSR render if SSR has been skipped due to suspense
|
||||
// As a workaround, we run the script in the <script> tag again after the first render
|
||||
// Note that we do an indirect eval as described here: https://esbuild.github.io/content-types/#direct-eval
|
||||
(0, eval)(props.script);
|
||||
}, []);
|
||||
|
||||
return <script dangerouslySetInnerHTML={{ __html: props.script }} />;
|
||||
}
|
||||
@ -5,6 +5,8 @@ import StyledComponentsRegistry from "./styled-components-registry";
|
||||
import { globalCSS } from "../generated/global-css";
|
||||
import { BrowserScript } from "../utils/browser-script";
|
||||
import { DEFAULT_THEME } from "../utils/constants";
|
||||
import Color from "color";
|
||||
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
|
||||
type Colors = {
|
||||
background: string,
|
||||
@ -38,6 +40,40 @@ type ThemeConfig = {
|
||||
light?: Partial<Colors>,
|
||||
dark?: Partial<Colors>,
|
||||
} & Partial<Omit<Theme, 'light' | 'dark'>>;
|
||||
|
||||
function convertColorToCSSVars(obj: Record<string, string>) {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
|
||||
const color = Color(value).hsl().array();
|
||||
return [
|
||||
// Convert camelCase key to dash-case
|
||||
key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`),
|
||||
// Convert color to CSS HSL string
|
||||
`${color[0]} ${color[1]}% ${color[2]}%`
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
function convertColorsToCSS(theme: Theme) {
|
||||
const { dark, light, ...rest } = theme;
|
||||
const colors = {
|
||||
light: { ...convertColorToCSSVars(light), ...rest },
|
||||
dark: convertColorToCSSVars(dark),
|
||||
};
|
||||
|
||||
function colorsToCSSVars(colors: Record<string, string>) {
|
||||
return Object.entries(colors).map((params) => {
|
||||
return `--${params[0]}: ${params[1]};\n`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
return deindent`
|
||||
.stack-scope {
|
||||
${colorsToCSSVars(colors.light)}
|
||||
}
|
||||
[data-stack-theme="dark"] .stack-scope {
|
||||
${colorsToCSSVars(colors.dark)}
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
export function StackTheme({
|
||||
@ -56,8 +92,8 @@ export function StackTheme({
|
||||
|
||||
return (
|
||||
<StyledComponentsRegistry>
|
||||
<BrowserScript theme={themeValue} />
|
||||
<style dangerouslySetInnerHTML={{ __html: globalCSS }} />
|
||||
<BrowserScript />
|
||||
<style dangerouslySetInnerHTML={{ __html: globalCSS + '\n' + convertColorsToCSS(themeValue) }} />
|
||||
{children}
|
||||
</StyledComponentsRegistry>
|
||||
);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import Color from "color";
|
||||
import { Theme } from "../providers/theme-provider";
|
||||
|
||||
// Note that this script can not import anything from outside as it will be converted to a string and executed in the browser.
|
||||
|
||||
import { SsrScript } from "../components/elements/ssr-layout-effect";
|
||||
|
||||
// Also please note that there might be hydration issues with this script, always check the browser console for errors after changing this script.
|
||||
const script = (colors: { light: Record<string, string>, dark: Record<string, string> }) => {
|
||||
const script = () => {
|
||||
const attributes = ['data-joy-color-scheme', 'data-mui-color-scheme', 'data-theme', 'data-color-scheme', 'class'];
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
@ -29,41 +29,8 @@ const script = (colors: { light: Record<string, string>, dark: Record<string, st
|
||||
attributes: true,
|
||||
attributeFilter: attributes,
|
||||
});
|
||||
|
||||
function colorsToCSSVars(colors: Record<string, string>) {
|
||||
return Object.entries(colors).map((params) => {
|
||||
return "--" + params[0] + ": " + params[1] + ";\n";
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const cssVars = '.stack-scope {\n' + colorsToCSSVars(colors.light) + '}\n[data-stack-theme="dark"] .stack-scope {\n' + colorsToCSSVars(colors.dark) + '}';
|
||||
const style = document.createElement('style');
|
||||
style.textContent = cssVars;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
function convertKeysToDashCase(obj: Record<string, string>) {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`), value]));
|
||||
}
|
||||
|
||||
function convertColorToHSL(obj: Record<string, string>) {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
|
||||
const color = Color(value).hsl().array();
|
||||
return [key, `${color[0]} ${color[1]}% ${color[2]}%`];
|
||||
}));
|
||||
}
|
||||
|
||||
function processColors(colors: Record<string, string>) {
|
||||
return convertColorToHSL(convertKeysToDashCase(colors));
|
||||
}
|
||||
|
||||
export function BrowserScript(props: { theme: Theme }) {
|
||||
const { dark, light, ...rest } = props.theme;
|
||||
const convertedColors = {
|
||||
light: { ...processColors(light), ...rest },
|
||||
dark: processColors(dark),
|
||||
};
|
||||
return (
|
||||
<script dangerouslySetInnerHTML={{ __html: `(${script.toString()})(${JSON.stringify(convertedColors)})` }}/>
|
||||
);
|
||||
export function BrowserScript() {
|
||||
return <SsrScript script={`(${script.toString()})()`}/>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user