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
|
- Atomic CSS generation
|
||||||
- Automatic vendor prefixing
|
- Automatic vendor prefixing
|
||||||
- SSR support
|
- SSR support
|
||||||
|
- Automatic theme (light/dark)
|
||||||
|
|
||||||
## Installation
|
## 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
|
## API
|
||||||
|
|
||||||
### useYoshiki
|
### useYoshiki
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
// 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 { useYoshiki, useMobileHover } from "yoshiki/web";
|
||||||
import { AppProps } from "next/app";
|
import { AppProps } from "next/app";
|
||||||
|
|
||||||
@@ -11,25 +11,31 @@ declare module "yoshiki" {
|
|||||||
export interface Theme {
|
export interface Theme {
|
||||||
spacing: string;
|
spacing: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
background: string;
|
||||||
|
light: { background: string; nested: { black: string } };
|
||||||
|
dark: { background: string; nested: { black: string } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const theme: Theme = {
|
export const theme: Theme = {
|
||||||
spacing: "24px",
|
spacing: "24px",
|
||||||
name: "yoshiki",
|
name: "yoshiki",
|
||||||
|
background: "white",
|
||||||
|
light: { background: "white", nested: { black: "black" } },
|
||||||
|
dark: { background: "black", nested: { black: "red" } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppName = () => {
|
const AppName = () => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
|
||||||
return <p {...css({ padding: (theme) => theme.spacing })}>{theme.name}</p>;
|
return <p {...css({ padding: (theme) => theme.spacing })}>{theme.name}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
useMobileHover();
|
useMobileHover();
|
||||||
|
const auto = useAutomaticTheme(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={{ ...theme, ...auto }}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
<AppName />
|
<AppName />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useYoshiki, Stylable, px, md } from "yoshiki/web";
|
import { useYoshiki, Stylable, md } from "yoshiki/web";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
const Box = ({ children, ...props }: { children?: ReactNode } & Stylable) => {
|
const Box = ({ children, ...props }: { children?: ReactNode } & Stylable) => {
|
||||||
@@ -27,6 +27,7 @@ export default function Home(props: object) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
paddingLeft: "2rem",
|
paddingLeft: "2rem",
|
||||||
paddingRight: "2rem",
|
paddingRight: "2rem",
|
||||||
|
bg: (theme) => theme.background,
|
||||||
},
|
},
|
||||||
md({
|
md({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
@@ -48,7 +49,7 @@ export default function Home(props: object) {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Box />
|
<Box />
|
||||||
<Box {...css({ bg: "blue", p: px(12) })} />
|
<Box {...css({ bg: "blue", padding: "12px" })} />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Get started by editing <code>pages/index.tsx</code>
|
Get started by editing <code>pages/index.tsx</code>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yoshiki",
|
"name": "yoshiki",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"author": "Zoe Roux <zoe.roux@sdg.moe> (https://github.com/AnonymusRaccoon)",
|
"author": "Zoe Roux <zoe.roux@sdg.moe> (https://github.com/AnonymusRaccoon)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"@types/node": "18.x.x",
|
"@types/node": "18.x.x",
|
||||||
"@types/react": "18.x.x",
|
"@types/react": "18.x.x",
|
||||||
"@types/react-native": ">= 0.70.0",
|
"@types/react-native": ">= 0.70.0",
|
||||||
"inline-style-prefixer": "^7.0.0"
|
"inline-style-prefixer": "^7.0.0",
|
||||||
|
"object-hash": "^3.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/inline-style-prefixer": "^5.0.0",
|
"@types/inline-style-prefixer": "^5.0.0",
|
||||||
|
"@types/object-hash": "^3.0.2",
|
||||||
"react": "18.x.x",
|
"react": "18.x.x",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-native-web": "^0.18.10",
|
"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 { useMobileHover } from "./hover";
|
||||||
export type { Theme };
|
export type { Theme };
|
||||||
export { breakpoints, useTheme } from "../theme";
|
export { breakpoints, useTheme } from "../theme";
|
||||||
|
export { useAutomaticTheme } from "./automatic-theme";
|
||||||
|
|
||||||
export const ThemeProvider = ({ theme, children }: { theme: Theme; children?: ReactNode }) =>
|
export const ThemeProvider = ({ theme, children }: { theme: Theme; children?: ReactNode }) =>
|
||||||
createElement(ThemeContext.Provider, { value: theme }, [children]);
|
createElement(ThemeContext.Provider, { value: theme }, [children]);
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -2692,6 +2692,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:*":
|
"@types/prop-types@npm:*":
|
||||||
version: 15.7.5
|
version: 15.7.5
|
||||||
resolution: "@types/prop-types@npm:15.7.5"
|
resolution: "@types/prop-types@npm:15.7.5"
|
||||||
@@ -11278,6 +11285,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"object-inspect@npm:^1.12.2, object-inspect@npm:^1.9.0":
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
resolution: "object-inspect@npm:1.12.2"
|
resolution: "object-inspect@npm:1.12.2"
|
||||||
@@ -16056,9 +16070,11 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/inline-style-prefixer": ^5.0.0
|
"@types/inline-style-prefixer": ^5.0.0
|
||||||
"@types/node": 18.x.x
|
"@types/node": 18.x.x
|
||||||
|
"@types/object-hash": ^3.0.2
|
||||||
"@types/react": 18.x.x
|
"@types/react": 18.x.x
|
||||||
"@types/react-native": ">= 0.70.0"
|
"@types/react-native": ">= 0.70.0"
|
||||||
inline-style-prefixer: ^7.0.0
|
inline-style-prefixer: ^7.0.0
|
||||||
|
object-hash: ^3.0.0
|
||||||
react: 18.x.x
|
react: 18.x.x
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
react-native-web: ^0.18.10
|
react-native-web: ^0.18.10
|
||||||
|
|||||||
Reference in New Issue
Block a user