From 29f52e0da06bb09d206dd9ba7470ad4f838504c4 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 10 Jan 2023 16:49:09 +0900 Subject: [PATCH] Allow named css values to be overriden --- examples/next-example/src/pages/_app.tsx | 3 +- packages/yoshiki/src/web/automatic-theme.ts | 14 ++- packages/yoshiki/src/web/generator.ts | 2 +- packages/yoshiki/src/web/registry.ts | 120 +++++++++++--------- 4 files changed, 77 insertions(+), 62 deletions(-) diff --git a/examples/next-example/src/pages/_app.tsx b/examples/next-example/src/pages/_app.tsx index a573aef..6cb9209 100644 --- a/examples/next-example/src/pages/_app.tsx +++ b/examples/next-example/src/pages/_app.tsx @@ -13,6 +13,7 @@ import { Theme, ThemeProvider, useAutomaticTheme, + useStyleRegistry, } from "yoshiki"; declare module "yoshiki" { @@ -43,7 +44,7 @@ const AppName = () => { }; const BrowserOnlyRegistry = ({ children }: { children: JSX.Element }) => { - const registry = useMemo(() => createStyleRegistry(), []); + const registry = useStyleRegistry(); if (typeof window === "undefined") return children; return {children}; }; diff --git a/packages/yoshiki/src/web/automatic-theme.ts b/packages/yoshiki/src/web/automatic-theme.ts index 122de34..803ee21 100644 --- a/packages/yoshiki/src/web/automatic-theme.ts +++ b/packages/yoshiki/src/web/automatic-theme.ts @@ -48,11 +48,17 @@ export const useAutomaticTheme = >( }; const auto = Object.fromEntries(traverseEntries(theme.light, theme.dark, toAuto)) as ToChild; - const rule = ` -body { ${cssVariables.map((x) => `${x.name}: ${x.light}`).join(";")} } -@media (prefers-color-scheme: dark) { body { ${cssVariables + const ruleLight = `body { ${cssVariables.map((x) => `${x.name}: ${x.light}`).join(";")} }`; + const ruleDark = `@media (prefers-color-scheme: dark) { body { ${cssVariables .map((x) => `${x.name}: ${x.dark}`) .join(";")} } }`; - registry.addRule({ type: "user", key, state: "normal", breakpoint: "default" }, rule); + registry.addRule( + { type: "user", key: key + "-light", state: "normal", breakpoint: "default" }, + ruleLight, + ); + registry.addRule( + { type: "user", key: key + "-dark", state: "normal", breakpoint: "default" }, + ruleDark, + ); return auto; }; diff --git a/packages/yoshiki/src/web/generator.ts b/packages/yoshiki/src/web/generator.ts index 943a656..0f3cb7b 100644 --- a/packages/yoshiki/src/web/generator.ts +++ b/packages/yoshiki/src/web/generator.ts @@ -298,7 +298,7 @@ export const generateChildCss = ( if (!block) continue; const cssClass = `${stateMapper[state](parentName)} .${className} ${block}`; registry.addRule( - { type: "atomic", key: className, breakpoint: breakpoint as BreakpointKey, state }, + { type: "general", key: className, breakpoint: breakpoint as BreakpointKey, state }, addBreakpointBlock(breakpoint as BreakpointKey, cssClass), ); } diff --git a/packages/yoshiki/src/web/registry.ts b/packages/yoshiki/src/web/registry.ts index 19c9a89..5816697 100644 --- a/packages/yoshiki/src/web/registry.ts +++ b/packages/yoshiki/src/web/registry.ts @@ -7,28 +7,36 @@ import { createContext, createElement, ReactNode, useContext } from "react"; import { breakpoints } from "../theme"; import { WithState } from "../type"; +const typeMapper: Record<"a" | "g" | "u", StyleKey["type"]> = { + a: "atomic", + g: "general", + u: "user", +}; type StyleKey = { type: "atomic" | "general" | "user"; key: string; breakpoint: keyof typeof breakpoints | "default"; state: keyof WithState | "normal"; }; -const keyToStr = ({ type, key, breakpoint, state }: StyleKey) => { - return `${type[0]}-${key}-${breakpoint}-${state}`; -}; -type StyleRule = { key: StyleKey; strKey: string; css: string }; +type StyleRule = { key: StyleKey; css: string }; export class StyleRegistry { - private completed: string[] = []; - private rules: [StyleKey, string][] = []; + private rules: StyleRule[] = []; private styleElement: HTMLStyleElement | null = null; - private cssOutput: Record> = - Object.fromEntries( - ["normal", "hover", "focus", "press"].map((x) => [ - x, - Object.fromEntries(Object.keys({ default: 0, ...breakpoints }).map((bp) => [bp, []])), - ]), - ) as any; + private cssOutput: Record< + StyleKey["state"], + Record>> + > = Object.fromEntries( + ["normal", "hover", "focus", "press"].map((x) => [ + x, + Object.fromEntries( + Object.keys({ default: 0, ...breakpoints }).map((bp) => [ + bp, + Object.fromEntries(["atomic", "general", "user"].map((x) => [x, {}])), + ]), + ), + ]), + ) as any; constructor(isDefault?: true) { if (isDefault) { @@ -39,24 +47,8 @@ export class StyleRegistry { } addRule(key: StyleKey, rule: string) { - if (this.rules.find(([eKey]) => Object.is(key, eKey))) return; - this.rules.push([key, rule]); - } - - 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(): 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(...toFlush.map(({ strKey }) => strKey)); - return toFlush; + if (this.rules.find(({ key: eKey }) => Object.is(key, eKey))) return; + this.rules.push({ key, css: rule }); } flushToBrowser() { @@ -64,47 +56,53 @@ export class StyleRegistry { this.hydrate(); } - const toFlush = this.flush(); - if (!toFlush.length) return; + const [css] = this.flushToStyleString(); if (!this.styleElement) { - document.head.insertAdjacentHTML( - "beforeend", - ``, - ); + document.head.insertAdjacentHTML("beforeend", ``); } else { - this.styleElement.textContent = this.toStyleString(toFlush); + this.styleElement.textContent = css; } } flushToComponent() { - const toFlush = this.flush(); - if (!toFlush.length) return null; + const [css, keys] = this.flushToStyleString(); // JSX can't be used since the compiler is set to react-native mode. return createElement("style", { - "data-yoshiki": this.completed.join(" "), - children: this.toStyleString(toFlush), + "data-yoshiki": keys, + children: css, }); } - toStyleString(rules: StyleRule[]): string { - for (const { key, css } of rules) { - this.cssOutput[key.state][key.breakpoint].push(css); + flushToStyleString(): [string, string] { + for (const { key, css } of this.rules) { + this.cssOutput[key.state][key.breakpoint][key.type][key.key] = css; } - return Object.entries(this.cssOutput) + this.rules = []; + + const keys: string[] = []; + const css = Object.entries(this.cssOutput) .flatMap(([state, bp]) => - Object.entries(bp).flatMap(([breakpoint, css]) => - css.length ? ["", `/* ${state}-${breakpoint} */`, ...css] : [], + Object.entries(bp).flatMap(([breakpoint, tp]) => + Object.entries(tp).flatMap(([type, css]) => { + const cssEntries = Object.entries(css); + keys.push(...cssEntries.map((x) => x[0])); + return cssEntries.length + ? ["", `/* ${type[0]}-${state}-${breakpoint} */`, ...cssEntries.map((x) => x[1])] + : // + []; + }), ), ) .join("\n"); + return [css, keys.join(" ")]; } hydrate() { const styles = document.querySelectorAll("style[data-yoshiki]"); for (const style of styles) { - this.completed.push(...(style.dataset.yoshiki ?? "").split(" ")); - if (style.textContent) this.hydrateStyle(style.textContent); + if (style.textContent && style.dataset.yoshiki) + this.hydrateStyle(style.textContent, style.dataset.yoshiki); if (!this.styleElement) { this.styleElement = style; style.dataset.yoshiki = ""; @@ -114,22 +112,32 @@ export class StyleRegistry { } } - hydrateStyle(css: string) { - const comReg = new RegExp("/\\* (\\w+)-(\\w+) \\*/"); + hydrateStyle(css: string, keysString: string) { + const comReg = new RegExp("/\\* (\\w)-(\\w+)-(\\w+) \\*/"); + const keys = keysString.split(" "); + + let type: StyleKey["type"] = "atomic"; let state: StyleKey["state"] = "normal"; let bp: StyleKey["breakpoint"] = "default"; + let index = 0; for (const line of css.split("\n")) { const match = line.match(comReg); if (match) { // 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"]; + type = typeMapper[match[1] as "a" | "g" | "u"]; + state = match[2] as StyleKey["state"]; + bp = match[3] as StyleKey["breakpoint"]; continue; } - if (line.length) - this.cssOutput[state][bp].push(line); + if (!line.length) continue; + if (keys.length <= index) { + console.error("Yoshiki: Hydratation mistake. There are more css rules than css keys."); + return; + } + this.cssOutput[state][bp][type][keys[index]] = line; + index++; } } }