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 (
+
+
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