mirror of
https://github.com/zoriya/yoshiki.git
synced 2025-12-06 07:06:13 +00:00
Rework the registry
This commit is contained in:
@@ -43,27 +43,15 @@ const AppName = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const RootRegistry = ({ children }: { children: ReactNode }) => {
|
||||
const registry = useMemo(() => createStyleRegistry(), []);
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
return registry.flushToComponent();
|
||||
});
|
||||
|
||||
return <StyleRegistryProvider registry={registry}>{children}</StyleRegistryProvider>;
|
||||
};
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
useMobileHover();
|
||||
const auto = useAutomaticTheme("theme", theme);
|
||||
|
||||
return (
|
||||
<RootRegistry>
|
||||
<ThemeProvider theme={{ ...theme, ...auto }}>
|
||||
<Component {...pageProps} />
|
||||
<AppName />
|
||||
</ThemeProvider>
|
||||
</RootRegistry>
|
||||
<ThemeProvider theme={{ ...theme, ...auto }}>
|
||||
<Component {...pageProps} />
|
||||
<AppName />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
36
examples/next-example/src/pages/_document.tsx
Normal file
36
examples/next-example/src/pages/_document.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright (c) Zoe Roux and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
//
|
||||
|
||||
import { StyleRegistryProvider, createStyleRegistry } from "yoshiki";
|
||||
import Document, { DocumentContext } from "next/document";
|
||||
|
||||
Document.getInitialProps = async (ctx: DocumentContext) => {
|
||||
const renderPage = ctx.renderPage;
|
||||
const registry = createStyleRegistry();
|
||||
|
||||
ctx.renderPage = () =>
|
||||
renderPage({
|
||||
enhanceApp: (App) => (props) => {
|
||||
return (
|
||||
<StyleRegistryProvider registry={registry}>
|
||||
<App {...props} />
|
||||
</StyleRegistryProvider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const props = await ctx.defaultGetInitialProps(ctx);
|
||||
return {
|
||||
...props,
|
||||
styles: (
|
||||
<>
|
||||
{props.styles}
|
||||
{registry.flushToComponent()}
|
||||
</>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export default Document;
|
||||
@@ -19,7 +19,7 @@ const rnwPreprocess = (block: Record<string, unknown>) => {
|
||||
export const useYoshiki = () => {
|
||||
const registry = useStyleRegistry();
|
||||
const theme = useTheme();
|
||||
const childPrefix = useClassId();
|
||||
const [parentPrefix, childPrefix] = useClassId();
|
||||
|
||||
useInsertionEffect(() => {
|
||||
registry.flushToBrowser();
|
||||
@@ -37,6 +37,7 @@ export const useYoshiki = () => {
|
||||
css,
|
||||
[...parentKeys.map((x) => `${childPrefix}${x}`), ...(overrides?.split(" ") ?? [])],
|
||||
{
|
||||
parentPrefix,
|
||||
registry,
|
||||
theme,
|
||||
preprocessBlock: rnwPreprocess,
|
||||
|
||||
@@ -54,6 +54,6 @@ body { ${cssVariables.map((x) => `${x.name}: ${x.light}`).join(";")} }
|
||||
body { ${cssVariables.map((x) => `${x.name}: ${x.dark}`).join(";")} }
|
||||
}
|
||||
`;
|
||||
registry.addRule(`automatic-theme-${key}`, rule);
|
||||
registry.addRule({ type: "user", key, state: "normal", breakpoint: "default" }, rule);
|
||||
return auto;
|
||||
};
|
||||
|
||||
@@ -85,15 +85,19 @@ const generateAtomicCss = (
|
||||
{
|
||||
theme,
|
||||
preprocessBlock,
|
||||
registry,
|
||||
}: {
|
||||
theme: Theme;
|
||||
preprocessBlock?: PreprocessBlockFunction;
|
||||
registry: StyleRegistry;
|
||||
},
|
||||
): [string, string][] => {
|
||||
): string[] => {
|
||||
if (key in shorthandsFn) {
|
||||
const expanded = shorthandsFn[key as keyof typeof shorthandsFn](value as any);
|
||||
return Object.entries(expanded)
|
||||
.map(([eKey, eValue]) => generateAtomicCss(eKey, eValue, state, { theme, preprocessBlock }))
|
||||
.map(([eKey, eValue]) =>
|
||||
generateAtomicCss(eKey, eValue, state, { theme, preprocessBlock, registry }),
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
@@ -109,34 +113,43 @@ const generateAtomicCss = (
|
||||
preprocessBlock,
|
||||
);
|
||||
if (!block) return [];
|
||||
return [
|
||||
[
|
||||
className,
|
||||
addBreakpointBlock(bp as BreakpointKey, `${stateMapper[state](className)} ${block}`),
|
||||
],
|
||||
];
|
||||
registry.addRule(
|
||||
{ type: "atomic", key: `${key}:${bpValue}`, state, breakpoint: bp as BreakpointKey },
|
||||
`${stateMapper[state](className)} ${block}`,
|
||||
);
|
||||
return className;
|
||||
});
|
||||
}
|
||||
|
||||
const className = generateAtomicName(statePrefix, key, value);
|
||||
const block = generateClassBlock({ [key]: value }, preprocessBlock);
|
||||
if (!block) return [];
|
||||
return [[className, `${stateMapper[state](className)} ${block}`]];
|
||||
registry.addRule(
|
||||
{ type: "atomic", key: `${key}:${value}`, state, breakpoint: "default" },
|
||||
`${stateMapper[state](className)} ${block}`,
|
||||
);
|
||||
return [className];
|
||||
};
|
||||
|
||||
const dedupProperties = (...classList: (string[] | undefined)[]) => {
|
||||
const propMap = new Map<string, string>();
|
||||
const atomicMap = new Map<string, string>();
|
||||
const rest: string[] = [];
|
||||
for (const classes of classList) {
|
||||
if (!classes) continue;
|
||||
|
||||
for (const name of classes) {
|
||||
if (!name) continue;
|
||||
if (!name.startsWith("ys-")) {
|
||||
rest.push(name);
|
||||
continue;
|
||||
}
|
||||
// example ys-background-blue or ys-sm_background-red
|
||||
const key = name.substring(3, name.lastIndexOf("-"));
|
||||
propMap.set(key, name);
|
||||
atomicMap.set(key, name);
|
||||
}
|
||||
}
|
||||
return Array.from(propMap.values()).join(" ");
|
||||
rest.push(...atomicMap.values())
|
||||
return rest.join(" ");
|
||||
};
|
||||
|
||||
export const yoshikiCssToClassNames = (
|
||||
@@ -165,21 +178,13 @@ export const yoshikiCssToClassNames = (
|
||||
if (!inlineStyle) return [];
|
||||
if (preprocess) inlineStyle = preprocess(inlineStyle);
|
||||
|
||||
// I'm sad that traverse is not a thing in JS.
|
||||
const [localClassNames, localStyle] = Object.entries(inlineStyle).reduce<[string[], string[]]>(
|
||||
(acc, [key, value]) => {
|
||||
const n = generateAtomicCss(key, value, state ?? "normal", {
|
||||
theme,
|
||||
preprocessBlock,
|
||||
});
|
||||
acc[0].push(...n.map((x) => x[0]));
|
||||
acc[1].push(...n.map((x) => x[1]));
|
||||
return acc;
|
||||
},
|
||||
[[], []],
|
||||
return Object.entries(inlineStyle).flatMap(([key, value]) =>
|
||||
generateAtomicCss(key, value, state ?? "normal", {
|
||||
theme,
|
||||
preprocessBlock,
|
||||
registry,
|
||||
}),
|
||||
);
|
||||
registry.addRules(localClassNames, localStyle);
|
||||
return localClassNames;
|
||||
};
|
||||
|
||||
return dedupProperties(
|
||||
@@ -293,7 +298,7 @@ export const generateChildCss = (
|
||||
if (!block) continue;
|
||||
const cssClass = `${stateMapper[state](parentName)} .${className} ${block}`;
|
||||
registry.addRule(
|
||||
`${className}-${state}-${breakpoint}`,
|
||||
{ type: "atomic", key: className, breakpoint: breakpoint as BreakpointKey, state },
|
||||
addBreakpointBlock(breakpoint as BreakpointKey, cssClass),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,24 +5,30 @@
|
||||
|
||||
import { createContext, createElement, ReactNode, useContext } from "react";
|
||||
import { breakpoints } from "../theme";
|
||||
import { WithState } from "../type";
|
||||
|
||||
function findLastIndex<T>(
|
||||
array: readonly T[],
|
||||
predicate: (element: T, index: number) => boolean,
|
||||
startIndex?: number,
|
||||
): number {
|
||||
for (let i = startIndex ?? array.length - 1; i >= 0; i--) {
|
||||
if (predicate(array[i], i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
type StyleKey = {
|
||||
type: "atomic" | "general" | "user";
|
||||
key: string;
|
||||
breakpoint: keyof typeof breakpoints | "default";
|
||||
state: keyof WithState<unknown> | "normal";
|
||||
};
|
||||
const keyToStr = ({ type, key, breakpoint, state }: StyleKey) => {
|
||||
return `${type[0]}-${key}-${breakpoint}-${state}`;
|
||||
};
|
||||
type StyleRule = { key: StyleKey; strKey: string; css: string };
|
||||
|
||||
export class StyleRegistry {
|
||||
private completed: string[] = [];
|
||||
private rules: [string, string][] = [];
|
||||
private rules: [StyleKey, string][] = [];
|
||||
private styleElement: HTMLStyleElement | null = null;
|
||||
private cssOutput: Record<StyleKey["state"], Record<StyleKey["breakpoint"], string[]>> =
|
||||
Object.fromEntries(
|
||||
["normal", "hover", "focus", "press"].map((x) => [
|
||||
x,
|
||||
Object.fromEntries(Object.keys({ default: 0, ...breakpoints }).map((bp) => [bp, []])),
|
||||
]),
|
||||
) as any;
|
||||
|
||||
constructor(isDefault?: true) {
|
||||
if (isDefault) {
|
||||
@@ -30,46 +36,36 @@ export class StyleRegistry {
|
||||
"Warning: Yoshiki was used without a top level StyleRegistry. SSR won't be supported.",
|
||||
);
|
||||
}
|
||||
if (typeof window !== "undefined") this.hydrate();
|
||||
}
|
||||
|
||||
addRule(key: string, rule: string) {
|
||||
if (this.rules.find(([eKey]) => key === eKey)) return;
|
||||
addRule(key: StyleKey, rule: string) {
|
||||
if (this.rules.find(([eKey]) => Object.is(key, eKey))) return;
|
||||
this.rules.push([key, rule]);
|
||||
}
|
||||
|
||||
addRules(keys: string[], rules: string[]) {
|
||||
addRules(keys: StyleKey[], rules: string[]) {
|
||||
// I'm sad that sequence is not a thing...
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.addRule(keys[i], rules[i]);
|
||||
}
|
||||
}
|
||||
|
||||
flush(): string[] {
|
||||
const ret = this.rules.filter(([key]) => !this.completed.includes(key));
|
||||
flush(): StyleRule[] {
|
||||
const toFlush = this.rules
|
||||
.map(([key, css]) => ({ key, strKey: keyToStr(key), css: css }))
|
||||
.filter(({ strKey }) => !this.completed.includes(strKey));
|
||||
this.rules = [];
|
||||
this.completed.push(...ret.map(([key]) => key));
|
||||
return ret.map(([, value]) => value);
|
||||
this.completed.push(...toFlush.map(({ strKey }) => strKey));
|
||||
return toFlush;
|
||||
}
|
||||
|
||||
flushToBrowser() {
|
||||
const toMerge: string[] = [];
|
||||
|
||||
if (!this.styleElement) {
|
||||
const styles = document.querySelectorAll<HTMLStyleElement>("style[data-yoshiki]");
|
||||
for (const style of styles) {
|
||||
this.completed.push(...(style.dataset.yoshiki ?? " ").split(" "));
|
||||
if (!this.styleElement) {
|
||||
this.styleElement = style;
|
||||
style.dataset.yoshiki = "";
|
||||
} else {
|
||||
if (style.textContent) toMerge.push(...style.textContent.split("\n"));
|
||||
style.remove();
|
||||
}
|
||||
}
|
||||
this.hydrate();
|
||||
}
|
||||
|
||||
// If we have something to merge, do it before a flush.
|
||||
const toFlush = toMerge.length ? toMerge : this.flush();
|
||||
const toFlush = this.flush();
|
||||
if (!toFlush.length) return;
|
||||
|
||||
if (!this.styleElement) {
|
||||
@@ -78,11 +74,8 @@ export class StyleRegistry {
|
||||
`<style data-yoshiki="">${this.toStyleString(toFlush)}</style>`,
|
||||
);
|
||||
} else {
|
||||
this.styleElement.textContent = this.toStyleString(toFlush, this.styleElement.textContent);
|
||||
this.styleElement.textContent = this.toStyleString(toFlush);
|
||||
}
|
||||
|
||||
// Since we did not flush earlier to merge, we do it now.
|
||||
if (toMerge.length) this.flushToBrowser();
|
||||
}
|
||||
|
||||
flushToComponent() {
|
||||
@@ -95,63 +88,48 @@ export class StyleRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
toStyleString(classes: string[], existingStyle?: string | null) {
|
||||
const newChunks = this.splitInChunks(classes);
|
||||
if (!existingStyle) {
|
||||
return newChunks
|
||||
.map((x, i) => (x.length ? x.join("\n") + `\n/*${i}*/` : null))
|
||||
.filter((x) => x)
|
||||
.join("\n");
|
||||
toStyleString(rules: StyleRule[]): string {
|
||||
for (const { key, css } of rules) {
|
||||
this.cssOutput[key.state][key.breakpoint].push(css);
|
||||
}
|
||||
|
||||
const lines = existingStyle.split("\n");
|
||||
const comReg = new RegExp("/\\*(\\d+)\\*/");
|
||||
|
||||
for (const [i, chunk] of newChunks.entries()) {
|
||||
if (!chunk.length) continue;
|
||||
const pos = findLastIndex(lines, (x) => {
|
||||
const match = comReg.exec(x);
|
||||
if (!match) return false;
|
||||
return parseInt(match[1]) <= i;
|
||||
});
|
||||
|
||||
if (pos === -1) {
|
||||
// No section with a same or lower priority exists, create one.
|
||||
lines.splice(0, 0, ...chunk, `/*${i}*/`);
|
||||
} else if (!lines[pos].includes(i.toString())) {
|
||||
// Our session does not exist, create one at the right place.
|
||||
lines.splice(pos + 1, 0, ...chunk, `/*${i}*/`);
|
||||
} else {
|
||||
// Append in our section.
|
||||
lines.splice(pos, 0, ...chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
return Object.entries(this.cssOutput)
|
||||
.map(([state, bp]) =>
|
||||
Object.entries(bp)
|
||||
.map(([breakpoint, css]) => `/* ${state}-${breakpoint} */\n${css.join("\n")}\n`)
|
||||
.join("\n"),
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
splitInChunks(classes: string[]): string[][] {
|
||||
const chunks: string[][][] = [...Array(4 /* Normal, Hover, Focus and Active*/)].map(() =>
|
||||
[...Array(1 + Object.keys(breakpoints).length)].map(() => []),
|
||||
);
|
||||
hydrate() {
|
||||
const styles = document.querySelectorAll<HTMLStyleElement>("style[data-yoshiki]");
|
||||
for (const style of styles) {
|
||||
this.completed.push(...(style.dataset.yoshiki ?? "").split(" "));
|
||||
if (style.textContent) this.hydrateStyle(style.textContent);
|
||||
if (!this.styleElement) {
|
||||
this.styleElement = style;
|
||||
style.dataset.yoshiki = "";
|
||||
} else {
|
||||
style.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const cl of classes) {
|
||||
const start = cl.indexOf(".ys-");
|
||||
const cn = cl.substring(start, cl.indexOf("-", start + 4));
|
||||
const modifier = cn.includes("_") ? cn.substring(4, cn.lastIndexOf("_")) : null;
|
||||
hydrateStyle(css: string) {
|
||||
const comReg = new RegExp("/\\* (\\w+):(\\w) \\*/");
|
||||
let state: StyleKey["state"] = "normal";
|
||||
let bp: StyleKey["breakpoint"] = "default";
|
||||
|
||||
if (!modifier) {
|
||||
chunks[0][0].push(cl);
|
||||
for (const line of css.split("\n")) {
|
||||
const match = line.match(comReg);
|
||||
if (!match) {
|
||||
this.cssOutput[state][bp].push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = ["hover", "focus", "press"].findIndex((x) => modifier.includes(x)) + 1;
|
||||
const bp = Object.keys(breakpoints).findIndex((x) => modifier.includes(x)) + 1;
|
||||
|
||||
chunks[type][bp].push(cl);
|
||||
// Not really safe but will break only if the user modifies the css manually.
|
||||
state = match[1] as StyleKey["state"];
|
||||
bp = match[2] as StyleKey["breakpoint"];
|
||||
}
|
||||
|
||||
return chunks.flat();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user