diff --git a/README.md b/README.md index 7166946..f10dab3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - Atomic CSS generation - Automatic vendor prefixing - SSR support +- Automatic theme (light/dark) ## Installation @@ -258,6 +259,33 @@ const AppName = () => { }; ``` +### Automatic theme + +If you have a light and dark theme, you may want to automatically switching between the two based on user preferences. +Yoshiki support this directly with the css property, you can use the `useAutomaticTheme` to get the automatic version +of a light/dark theme. + +This approach works with SSR. + +```tsx +import { useYoshiki, useAutomaticTheme } from "yohsiki/web"; + +const App = () => { + const theme = { + light: { background: "white", text: "black" }, + dark: { background: "black", text: "white" }, + }; + const auto = useAutomaticTheme(theme); + const { css } = useYoshiki(); + + return ( +
+

Automatic theme

+
+ ); +}; +``` + ## API ### useYoshiki diff --git a/examples/next-example/src/pages/_app.tsx b/examples/next-example/src/pages/_app.tsx index 9a9a8da..65d8b1f 100644 --- a/examples/next-example/src/pages/_app.tsx +++ b/examples/next-example/src/pages/_app.tsx @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. // -import { Theme, ThemeProvider } from "yoshiki"; +import { Theme, ThemeProvider, useAutomaticTheme } from "yoshiki"; import { useYoshiki, useMobileHover } from "yoshiki/web"; import { AppProps } from "next/app"; @@ -11,25 +11,31 @@ declare module "yoshiki" { export interface Theme { spacing: string; name: string; + background: string; + light: { background: string; nested: { black: string } }; + dark: { background: string; nested: { black: string } }; } } export const theme: Theme = { spacing: "24px", name: "yoshiki", + background: "white", + light: { background: "white", nested: { black: "black" } }, + dark: { background: "black", nested: { black: "red" } }, }; const AppName = () => { const { css, theme } = useYoshiki(); - return

theme.spacing })}>{theme.name}

; }; const App = ({ Component, pageProps }: AppProps) => { useMobileHover(); + const auto = useAutomaticTheme(theme); return ( - + diff --git a/examples/next-example/src/pages/index.tsx b/examples/next-example/src/pages/index.tsx index 4722d1b..ffbf34e 100644 --- a/examples/next-example/src/pages/index.tsx +++ b/examples/next-example/src/pages/index.tsx @@ -5,7 +5,7 @@ import Head from "next/head"; import Image from "next/image"; -import { useYoshiki, Stylable, px, md } from "yoshiki/web"; +import { useYoshiki, Stylable, md } from "yoshiki/web"; import { ReactNode } from "react"; const Box = ({ children, ...props }: { children?: ReactNode } & Stylable) => { @@ -27,6 +27,7 @@ export default function Home(props: object) { display: "flex", paddingLeft: "2rem", paddingRight: "2rem", + bg: (theme) => theme.background, }, md({ flexGrow: 1, @@ -48,7 +49,7 @@ export default function Home(props: object) { - +

Get started by editing pages/index.tsx diff --git a/packages/yoshiki/package.json b/packages/yoshiki/package.json index 1feefe1..d218562 100644 --- a/packages/yoshiki/package.json +++ b/packages/yoshiki/package.json @@ -1,6 +1,6 @@ { "name": "yoshiki", - "version": "0.3.2", + "version": "0.3.3", "author": "Zoe Roux (https://github.com/AnonymusRaccoon)", "license": "MIT", "keywords": [ @@ -26,7 +26,8 @@ "@types/node": "18.x.x", "@types/react": "18.x.x", "@types/react-native": ">= 0.70.0", - "inline-style-prefixer": "^7.0.0" + "inline-style-prefixer": "^7.0.0", + "object-hash": "^3.0.0" }, "peerDependencies": { "react": "*", @@ -43,6 +44,7 @@ }, "devDependencies": { "@types/inline-style-prefixer": "^5.0.0", + "@types/object-hash": "^3.0.2", "react": "18.x.x", "react-dom": "^18.2.0", "react-native-web": "^0.18.10", diff --git a/packages/yoshiki/src/web/automatic-theme.ts b/packages/yoshiki/src/web/automatic-theme.ts new file mode 100644 index 0000000..f604290 --- /dev/null +++ b/packages/yoshiki/src/web/automatic-theme.ts @@ -0,0 +1,57 @@ +// +// Copyright (c) Zoe Roux and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// + +import { sha1 } from "object-hash"; +import { useStyleRegistry } from "./registry"; + +type Child = string | number | { [key: string]: Child }; +type ToChild = { [key in keyof T]: T[key] extends object ? ToChild : string }; + +const traverseEntries = , Ret>( + first: T, + second: T, + mapper: (name: string, first: Child, second: Child) => Ret, +): Ret[] => { + return Object.entries(first).map(([name, f]) => mapper(name, f, second[name])); +}; + +export const useAutomaticTheme = >(theme: { + light: T; + dark: T; +}): ToChild => { + const registry = useStyleRegistry(); + const cssVariables: { name: string; light: string | number; dark: string | number }[] = []; + + const toAuto = ( + name: string, + light: Child, + dark: Child, + parent?: string, + ): [string, string | Record] => { + if (typeof light === "object" && typeof dark === "object") { + return [ + name, + Object.fromEntries( + traverseEntries(light, dark, (n, lv, dv) => + toAuto(n, lv, dv, [parent, name].filter((x) => x).join("-")), + ), + ), + ]; + } + const cssVar = ["-", parent, name].filter((x) => x).join("-"); + cssVariables.push({ name: cssVar, light: light.toString(), dark: dark.toString() }); + return [name, `var(${cssVar})`]; + }; + + 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.map((x) => `${x.name}: ${x.dark}`).join(";")} } +} + `; + registry.addRule(`automatic-theme-${sha1(theme)}`, rule); + return auto; +}; diff --git a/packages/yoshiki/src/web/index.ts b/packages/yoshiki/src/web/index.ts index 8fbbbd2..ad3363b 100644 --- a/packages/yoshiki/src/web/index.ts +++ b/packages/yoshiki/src/web/index.ts @@ -21,6 +21,7 @@ export { StyleRegistryProvider, useStyleRegistry, createStyleRegistry } from "./ export { useMobileHover } from "./hover"; export type { Theme }; export { breakpoints, useTheme } from "../theme"; +export { useAutomaticTheme } from "./automatic-theme"; export const ThemeProvider = ({ theme, children }: { theme: Theme; children?: ReactNode }) => createElement(ThemeContext.Provider, { value: theme }, [children]); diff --git a/yarn.lock b/yarn.lock index 0ff37b0..77f25df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2692,6 +2692,13 @@ __metadata: languageName: node linkType: hard +"@types/object-hash@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/object-hash@npm:3.0.2" + checksum: 0332e59074e7df2e74c093a7419c05c1e1c5ae1e12d3779f3240b3031835ff045b4ac591362be0b411ace24d3b5e760386b434761c33af25904f7a3645cb3785 + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.5 resolution: "@types/prop-types@npm:15.7.5" @@ -11278,6 +11285,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:^3.0.0": + version: 3.0.0 + resolution: "object-hash@npm:3.0.0" + checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c + languageName: node + linkType: hard + "object-inspect@npm:^1.12.2, object-inspect@npm:^1.9.0": version: 1.12.2 resolution: "object-inspect@npm:1.12.2" @@ -16056,9 +16070,11 @@ __metadata: dependencies: "@types/inline-style-prefixer": ^5.0.0 "@types/node": 18.x.x + "@types/object-hash": ^3.0.2 "@types/react": 18.x.x "@types/react-native": ">= 0.70.0" inline-style-prefixer: ^7.0.0 + object-hash: ^3.0.0 react: 18.x.x react-dom: ^18.2.0 react-native-web: ^0.18.10