mirror of
https://github.com/zoriya/yoshiki.git
synced 2025-12-06 07:06:13 +00:00
Add automatic theme
This commit is contained in:
28
README.md
28
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 (
|
||||
<div {...css({ bg: auto.background })}>
|
||||
<p {...css({ textColor: auto.text })}>Automatic theme</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### useYoshiki
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
57
packages/yoshiki/src/web/automatic-theme.ts
Normal file
57
packages/yoshiki/src/web/automatic-theme.ts
Normal 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;
|
||||
};
|
||||
@@ -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]);
|
||||
|
||||
16
yarn.lock
16
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
|
||||
|
||||
Reference in New Issue
Block a user