Add automatic theme

This commit is contained in:
Zoe Roux
2023-01-07 01:53:07 +09:00
parent 1e7acacbe5
commit a7e17b35e0
7 changed files with 118 additions and 7 deletions

View File

@@ -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 (
<div {...css({ bg: auto.background })}>
<p {...css({ textColor: auto.text })}>Automatic theme</p>
</div>
);
};
```
## API
### useYoshiki

View File

@@ -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 <p {...css({ padding: (theme) => theme.spacing })}>{theme.name}</p>;
};
const App = ({ Component, pageProps }: AppProps) => {
useMobileHover();
const auto = useAutomaticTheme(theme);
return (
<ThemeProvider theme={theme}>
<ThemeProvider theme={{ ...theme, ...auto }}>
<Component {...pageProps} />
<AppName />
</ThemeProvider>

View File

@@ -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) {
</h1>
<Box />
<Box {...css({ bg: "blue", p: px(12) })} />
<Box {...css({ bg: "blue", padding: "12px" })} />
<p>
Get started by editing <code>pages/index.tsx</code>

View File

@@ -1,6 +1,6 @@
{
"name": "yoshiki",
"version": "0.3.2",
"version": "0.3.3",
"author": "Zoe Roux <zoe.roux@sdg.moe> (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",

View File

@@ -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<T> = { [key in keyof T]: T[key] extends object ? ToChild<T[key]> : string };
const traverseEntries = <T extends Record<string, Child>, 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 = <T extends Record<string, Child>>(theme: {
light: T;
dark: T;
}): ToChild<T> => {
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<string, Child>] => {
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<T>;
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;
};

View File

@@ -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]);

View File

@@ -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