@k8ordo/color-scheme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/provider.d.mts +31 -0
- package/dist/provider.mjs +58 -0
- package/dist/scheme.d.mts +36 -0
- package/dist/scheme.mjs +31 -0
- package/docs/GUIDE.md +130 -0
- package/docs/llms.txt +37 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Koki Sakano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @k8ordo/color-scheme
|
|
2
|
+
|
|
3
|
+
The colour-scheme axis of an application, owned. The visitor's preference —
|
|
4
|
+
light, dark, or nothing, which follows the system — lives in localStorage
|
|
5
|
+
through `@k8ordo/state`; one rule resolves it against `prefers-color-scheme`;
|
|
6
|
+
the result is the `dark` class on `<html>`, put there by an inline script
|
|
7
|
+
before the first paint and kept there after hydration. One provider does all
|
|
8
|
+
of that; a hook reads it. `@k8ordo/ui`'s tokens, and Tailwind's own dark
|
|
9
|
+
variant, read the class.
|
|
10
|
+
|
|
11
|
+
Like every [k8ordo](https://ordo.k8o.me) package it assumes React 19 and Server
|
|
12
|
+
Components, uses only what has reached Baseline newly available, and ships no
|
|
13
|
+
polyfills or legacy fallbacks.
|
|
14
|
+
|
|
15
|
+
- **Documentation**: https://ordo.k8o.me/color-scheme
|
|
16
|
+
- **Design guide**: [docs/GUIDE.md](docs/GUIDE.md) — shipped inside this package
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @k8ordo/color-scheme @k8ordo/state zod
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @k8ordo/color-scheme @k8ordo/state zod
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Peer Dependencies
|
|
27
|
+
|
|
28
|
+
| Package | Version | Needed for |
|
|
29
|
+
| --------------- | ------- | ------------------------------------------ |
|
|
30
|
+
| `@k8ordo/state` | ≥0.2.0 | where the preference lives (localStorage) |
|
|
31
|
+
| `react` | ≥19.3.0 | the provider and the hook |
|
|
32
|
+
| `zod` | ^4.4.3 | the one-field schema `@k8ordo/state` reads |
|
|
33
|
+
| `typescript` | ≥7.0.2 | the shipped type declarations (optional) |
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
The provider goes in the root layout, inside `<body>`, around everything. It
|
|
38
|
+
renders the inline script itself, so nothing goes in `<head>`:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// routes/layout.tsx — a Server Component
|
|
42
|
+
import { ColorSchemeProvider } from '@k8ordo/color-scheme';
|
|
43
|
+
|
|
44
|
+
export default function RootLayout({ children }) {
|
|
45
|
+
return (
|
|
46
|
+
<html suppressHydrationWarning>
|
|
47
|
+
<body>
|
|
48
|
+
<ColorSchemeProvider>{children}</ColorSchemeProvider>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A switcher reads and writes the preference:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
'use client';
|
|
59
|
+
|
|
60
|
+
import { useColorScheme } from '@k8ordo/color-scheme';
|
|
61
|
+
|
|
62
|
+
export function SchemeSwitcher() {
|
|
63
|
+
const { scheme, setPreference } = useColorScheme();
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => setPreference(scheme === 'dark' ? 'light' : 'dark')}
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
{scheme === 'dark' ? 'light mode' : 'dark mode'}
|
|
70
|
+
</button>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`scheme` is what is on screen; `preference` is what the visitor asked for
|
|
76
|
+
(`'system'` when nothing is stored); `setPreference('system')` clears it.
|
|
77
|
+
`<ColorSchemeProvider defaultPreference="dark">` starts every visitor dark
|
|
78
|
+
until they choose.
|
|
79
|
+
|
|
80
|
+
## AI Agent Documentation
|
|
81
|
+
|
|
82
|
+
This package ships its documentation so an AI coding assistant reads the exact
|
|
83
|
+
installed version:
|
|
84
|
+
|
|
85
|
+
- `node_modules/@k8ordo/color-scheme/docs/GUIDE.md` — the design guide
|
|
86
|
+
- `node_modules/@k8ordo/color-scheme/docs/llms.txt` — the index
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ColorScheme, ColorSchemePreference, colorSchemeState } from "./scheme.mjs";
|
|
2
|
+
import { ColorSchemeProvider, ColorSchemeProviderProps, UseColorScheme, useColorScheme } from "./provider.mjs";
|
|
3
|
+
export { type ColorScheme, type ColorSchemePreference, ColorSchemeProvider, type ColorSchemeProviderProps, type UseColorScheme, colorSchemeState, useColorScheme };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ColorScheme, ColorSchemePreference } from "./scheme.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/provider.d.ts
|
|
4
|
+
type UseColorScheme = {
|
|
5
|
+
/** What is on screen: the preference, or the system's answer. */
|
|
6
|
+
readonly scheme: ColorScheme;
|
|
7
|
+
/** What the visitor asked for; `'system'` when nothing is stored. */
|
|
8
|
+
readonly preference: ColorSchemePreference;
|
|
9
|
+
/** Stores a preference, or `'system'` to store none and follow it again. */
|
|
10
|
+
readonly setPreference: (preference: ColorSchemePreference) => void;
|
|
11
|
+
};
|
|
12
|
+
type ColorSchemeProviderProps = {
|
|
13
|
+
/**
|
|
14
|
+
* What applies while the visitor has chosen nothing. `'system'` follows
|
|
15
|
+
* `prefers-color-scheme`; a scheme starts every visitor there until they
|
|
16
|
+
* choose.
|
|
17
|
+
*/
|
|
18
|
+
readonly defaultPreference?: ColorSchemePreference;
|
|
19
|
+
readonly children: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The one place the scheme is decided and written. Goes in the root
|
|
23
|
+
* layout, inside `<body>`, around everything: it renders the inline script
|
|
24
|
+
* that puts the class on `<html>` before the first paint, keeps the class
|
|
25
|
+
* in step afterwards, and hands `useColorScheme` what it reads.
|
|
26
|
+
*/
|
|
27
|
+
declare function ColorSchemeProvider({ defaultPreference, children }: ColorSchemeProviderProps): ReactNode;
|
|
28
|
+
/** The visitor's colour scheme and a way to change it, from the provider above. */
|
|
29
|
+
declare function useColorScheme(): UseColorScheme;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ColorSchemeProvider, ColorSchemeProviderProps, UseColorScheme, useColorScheme };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { DARK_CLASS, DARK_QUERY, colorSchemeState, resolve, scriptFor } from "./scheme.mjs";
|
|
3
|
+
import { useAppState } from "@k8ordo/state";
|
|
4
|
+
import { createContext, use, useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/provider.tsx
|
|
7
|
+
const ColorSchemeContext = createContext(null);
|
|
8
|
+
const subscribeSystem = (onChange) => {
|
|
9
|
+
const query = matchMedia(DARK_QUERY);
|
|
10
|
+
query.addEventListener("change", onChange);
|
|
11
|
+
return () => {
|
|
12
|
+
query.removeEventListener("change", onChange);
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const readSystemDark = () => matchMedia(DARK_QUERY).matches;
|
|
16
|
+
const serverSystemDark = () => false;
|
|
17
|
+
const subscribeNever = () => () => {};
|
|
18
|
+
const useReadsStore = () => useSyncExternalStore(subscribeNever, () => true, () => false);
|
|
19
|
+
/**
|
|
20
|
+
* The one place the scheme is decided and written. Goes in the root
|
|
21
|
+
* layout, inside `<body>`, around everything: it renders the inline script
|
|
22
|
+
* that puts the class on `<html>` before the first paint, keeps the class
|
|
23
|
+
* in step afterwards, and hands `useColorScheme` what it reads.
|
|
24
|
+
*/
|
|
25
|
+
function ColorSchemeProvider({ defaultPreference = "system", children }) {
|
|
26
|
+
const [{ preference }, update] = useAppState(colorSchemeState);
|
|
27
|
+
const systemDark = useSyncExternalStore(subscribeSystem, readSystemDark, serverSystemDark);
|
|
28
|
+
const scheme = resolve(preference, defaultPreference, systemDark);
|
|
29
|
+
const readsStore = useReadsStore();
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!readsStore) return;
|
|
32
|
+
document.documentElement.classList.toggle(DARK_CLASS, scheme === "dark");
|
|
33
|
+
}, [readsStore, scheme]);
|
|
34
|
+
const setPreference = useCallback((next) => {
|
|
35
|
+
update({ preference: next === "system" ? void 0 : next });
|
|
36
|
+
}, [update]);
|
|
37
|
+
const value = useMemo(() => ({
|
|
38
|
+
scheme,
|
|
39
|
+
preference: preference ?? "system",
|
|
40
|
+
setPreference
|
|
41
|
+
}), [
|
|
42
|
+
scheme,
|
|
43
|
+
preference,
|
|
44
|
+
setPreference
|
|
45
|
+
]);
|
|
46
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("script", { children: scriptFor(defaultPreference) }), /* @__PURE__ */ jsx(ColorSchemeContext, {
|
|
47
|
+
value,
|
|
48
|
+
children
|
|
49
|
+
})] });
|
|
50
|
+
}
|
|
51
|
+
/** The visitor's colour scheme and a way to change it, from the provider above. */
|
|
52
|
+
function useColorScheme() {
|
|
53
|
+
const value = use(ColorSchemeContext);
|
|
54
|
+
if (value === null) throw new Error("useColorScheme needs <ColorSchemeProvider> above it — put one in the root layout, inside <body>");
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { ColorSchemeProvider, useColorScheme };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as z from "zod/mini";
|
|
2
|
+
//#region src/scheme.d.ts
|
|
3
|
+
type ColorScheme = 'light' | 'dark';
|
|
4
|
+
/** What the visitor asked for: a scheme, or to follow the system. */
|
|
5
|
+
type ColorSchemePreference = ColorScheme | 'system';
|
|
6
|
+
/**
|
|
7
|
+
* Where the preference lives: localStorage, under
|
|
8
|
+
* `k8ordo-state:color-scheme`. An absent `preference` is "nothing chosen",
|
|
9
|
+
* so a visitor who never chose is never pinned to what the system said on
|
|
10
|
+
* their first visit.
|
|
11
|
+
*/
|
|
12
|
+
declare const colorSchemeState: import("@k8ordo/state").LocalState<z.ZodMiniObject<{
|
|
13
|
+
preference: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
14
|
+
dark: "dark";
|
|
15
|
+
light: "light";
|
|
16
|
+
}>>;
|
|
17
|
+
}, z.core.$strip>>;
|
|
18
|
+
/** The media query the system answers through. */
|
|
19
|
+
declare const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
20
|
+
/** The class `@k8ordo/ui` — and Tailwind's own dark variant — read on `<html>`. */
|
|
21
|
+
declare const DARK_CLASS = "dark";
|
|
22
|
+
/**
|
|
23
|
+
* The one rule: what the visitor chose wins, then what the provider was
|
|
24
|
+
* told to start from, and `system` — the default's default — asks the
|
|
25
|
+
* system.
|
|
26
|
+
*/
|
|
27
|
+
declare const resolve: (preference: ColorScheme | undefined, defaultPreference: ColorSchemePreference, systemDark: boolean) => ColorScheme;
|
|
28
|
+
/**
|
|
29
|
+
* The inline script the provider renders before its children: the rule
|
|
30
|
+
* above, run before the first paint. The schema does not run there, so the
|
|
31
|
+
* stored value is checked by hand and anything else reads as nothing
|
|
32
|
+
* chosen — the same salvage the store applies.
|
|
33
|
+
*/
|
|
34
|
+
declare const scriptFor: (defaultPreference: ColorSchemePreference) => string;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { ColorScheme, ColorSchemePreference, DARK_CLASS, DARK_QUERY, colorSchemeState, resolve, scriptFor };
|
package/dist/scheme.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineLocalState } from "@k8ordo/state";
|
|
2
|
+
import * as z from "zod/mini";
|
|
3
|
+
//#region src/scheme.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where the preference lives: localStorage, under
|
|
6
|
+
* `k8ordo-state:color-scheme`. An absent `preference` is "nothing chosen",
|
|
7
|
+
* so a visitor who never chose is never pinned to what the system said on
|
|
8
|
+
* their first visit.
|
|
9
|
+
*/
|
|
10
|
+
const colorSchemeState = defineLocalState("color-scheme", z.object({ preference: z.optional(z.enum(["light", "dark"])) }));
|
|
11
|
+
/** The media query the system answers through. */
|
|
12
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
13
|
+
/** The class `@k8ordo/ui` — and Tailwind's own dark variant — read on `<html>`. */
|
|
14
|
+
const DARK_CLASS = "dark";
|
|
15
|
+
/**
|
|
16
|
+
* The one rule: what the visitor chose wins, then what the provider was
|
|
17
|
+
* told to start from, and `system` — the default's default — asks the
|
|
18
|
+
* system.
|
|
19
|
+
*/
|
|
20
|
+
const resolve = (preference, defaultPreference, systemDark) => {
|
|
21
|
+
return preference ?? (defaultPreference === "system" ? void 0 : defaultPreference) ?? (systemDark ? "dark" : "light");
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The inline script the provider renders before its children: the rule
|
|
25
|
+
* above, run before the first paint. The schema does not run there, so the
|
|
26
|
+
* stored value is checked by hand and anything else reads as nothing
|
|
27
|
+
* chosen — the same salvage the store applies.
|
|
28
|
+
*/
|
|
29
|
+
const scriptFor = (defaultPreference) => `(()=>{const s=${colorSchemeState.inlineRead()};const v=s&&s.preference;const p=v==="dark"||v==="light"?v:${JSON.stringify(defaultPreference)};if(p==="dark"||(p!=="light"&&matchMedia(${JSON.stringify(DARK_QUERY)}).matches))document.documentElement.classList.add(${JSON.stringify(DARK_CLASS)})})()`;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { DARK_CLASS, DARK_QUERY, colorSchemeState, resolve, scriptFor };
|
package/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @k8ordo/color-scheme
|
|
2
|
+
|
|
3
|
+
The colour-scheme axis of an application, owned.
|
|
4
|
+
|
|
5
|
+
A visitor's colour scheme is three things that have to agree: what they
|
|
6
|
+
asked for, what the system says, and what is on screen. This package keeps
|
|
7
|
+
the first in localStorage (through `@k8ordo/state`), reads the second from
|
|
8
|
+
`prefers-color-scheme`, and resolves both into the third — the `dark` class
|
|
9
|
+
on `<html>`, which `@k8ordo/ui`'s tokens and Tailwind's own dark variant
|
|
10
|
+
read. One provider does all of it: it renders the inline script that puts
|
|
11
|
+
the class there before the first paint, keeps it right after hydration, and
|
|
12
|
+
hands a hook what it reads.
|
|
13
|
+
|
|
14
|
+
Like every k8ordo package it assumes React 19 and Server Components, uses
|
|
15
|
+
only what has reached Baseline newly available, and ships no polyfills or
|
|
16
|
+
legacy fallbacks.
|
|
17
|
+
|
|
18
|
+
## What it does not own
|
|
19
|
+
|
|
20
|
+
- **The colours.** Which colour `dark` means is the stylesheet's business —
|
|
21
|
+
`@k8ordo/ui`'s tokens, or any other that reads the class.
|
|
22
|
+
- **Where the preference lives.** That is `@k8ordo/state`'s
|
|
23
|
+
`defineLocalState`; this package declares one and reads it, so the key
|
|
24
|
+
and the JSON envelope are never spelled here.
|
|
25
|
+
- **A guess on the server.** No cookie, no header: a server render shows
|
|
26
|
+
the provider's default, and the inline script is what makes the first
|
|
27
|
+
paint right.
|
|
28
|
+
|
|
29
|
+
## The shape of it
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
routes/layout.tsx <body><ColorSchemeProvider>{children}</ColorSchemeProvider></body>
|
|
33
|
+
│ renders <script> first → class on <html> before the first paint
|
|
34
|
+
│ reads the store + the system → keeps the class in step
|
|
35
|
+
components useColorScheme() → { scheme, preference, setPreference }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
The provider goes in the root layout — a Server Component — inside `<body>`
|
|
41
|
+
and around everything, so the script it renders comes before anything the
|
|
42
|
+
page paints:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// routes/layout.tsx
|
|
46
|
+
import { ColorSchemeProvider } from '@k8ordo/color-scheme';
|
|
47
|
+
import type { ReactNode } from 'react';
|
|
48
|
+
|
|
49
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
50
|
+
return (
|
|
51
|
+
<html lang="en" suppressHydrationWarning>
|
|
52
|
+
<body>
|
|
53
|
+
<ColorSchemeProvider>{children}</ColorSchemeProvider>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`suppressHydrationWarning` on `<html>`, because the script adds a class the
|
|
61
|
+
server did not render — that difference is the point, not a mistake.
|
|
62
|
+
|
|
63
|
+
Nothing else is required: the first paint is right, and a visitor who
|
|
64
|
+
never chooses follows the system, in either direction, for as long as the
|
|
65
|
+
page is open. To start every visitor somewhere other than the system, say
|
|
66
|
+
so once: `<ColorSchemeProvider defaultPreference="dark">`.
|
|
67
|
+
|
|
68
|
+
## Reading and changing it
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
'use client';
|
|
72
|
+
|
|
73
|
+
import { useColorScheme } from '@k8ordo/color-scheme';
|
|
74
|
+
|
|
75
|
+
export function SchemeSwitcher() {
|
|
76
|
+
const { scheme, preference, setPreference } = useColorScheme();
|
|
77
|
+
return (
|
|
78
|
+
<select
|
|
79
|
+
onChange={(event) =>
|
|
80
|
+
setPreference(event.currentTarget.value as typeof preference)
|
|
81
|
+
}
|
|
82
|
+
value={preference}
|
|
83
|
+
>
|
|
84
|
+
<option value="system">system ({scheme})</option>
|
|
85
|
+
<option value="light">light</option>
|
|
86
|
+
<option value="dark">dark</option>
|
|
87
|
+
</select>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Member | What it is |
|
|
93
|
+
| --------------- | ------------------------------------------------------------------------- |
|
|
94
|
+
| `scheme` | `'light' \| 'dark'` — what is on screen. |
|
|
95
|
+
| `preference` | `'light' \| 'dark' \| 'system'` — what the visitor asked for. |
|
|
96
|
+
| `setPreference` | Stores a preference; `'system'` stores none and follows the system again. |
|
|
97
|
+
|
|
98
|
+
A toggle is `setPreference(scheme === 'dark' ? 'light' : 'dark')`. The hook
|
|
99
|
+
reads the provider and touches nothing else, so a switcher and a preview
|
|
100
|
+
agree because there is one provider deciding.
|
|
101
|
+
|
|
102
|
+
`colorSchemeState` is the `defineLocalState` definition itself, exported for
|
|
103
|
+
anything that wants the row without the hook — `useAppState(colorSchemeState)`,
|
|
104
|
+
or `colorSchemeState.storageKey` in a test.
|
|
105
|
+
|
|
106
|
+
## What it guarantees
|
|
107
|
+
|
|
108
|
+
- **No flash.** The script runs before the first paint and reads the same
|
|
109
|
+
row the provider writes; a dark page loads dark.
|
|
110
|
+
- **Nothing chosen follows the default, and the default follows the
|
|
111
|
+
system.** A visitor who never chose is never pinned to what the system
|
|
112
|
+
said on their first visit, and a row an older version wrote, or one that
|
|
113
|
+
is not JSON, reads as nothing chosen.
|
|
114
|
+
- **The server renders the default**, and the provider corrects itself on
|
|
115
|
+
hydration without touching the document: the render that hydrates reads
|
|
116
|
+
the server's guesses and writes nothing, and the render that follows reads
|
|
117
|
+
the store and writes what the script already put there. Until then the
|
|
118
|
+
HTML shows the default — a switcher's icon, say — which is the one thing
|
|
119
|
+
no server can know; a component that must not show it even for a moment
|
|
120
|
+
says so with React's `use(browser())` under a `<Suspense>`.
|
|
121
|
+
- **Tabs agree.** The preference propagates across tabs through the
|
|
122
|
+
`storage` event, as any `@k8ordo/state` local state does.
|
|
123
|
+
|
|
124
|
+
## Testing
|
|
125
|
+
|
|
126
|
+
Under a browser, `localStorage.clear()`, remove the class from
|
|
127
|
+
`document.documentElement`, and `resetStateRegistry()` from `@k8ordo/state`
|
|
128
|
+
between tests; render the hook under `<ColorSchemeProvider>` (a `wrapper`).
|
|
129
|
+
A client-only mount also runs the script the provider renders — a script
|
|
130
|
+
element React creates executes — which is idempotent with the effect.
|
package/docs/llms.txt
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @k8ordo/color-scheme
|
|
2
|
+
|
|
3
|
+
> The colour-scheme axis of an application, owned. The visitor's preference
|
|
4
|
+
> — light, dark, or nothing, which follows the system — lives in
|
|
5
|
+
> localStorage through `@k8ordo/state`; one rule resolves it against
|
|
6
|
+
> `prefers-color-scheme`; the result is the `dark` class on `<html>` (what
|
|
7
|
+
> `@k8ordo/ui` and Tailwind's dark variant read). `<ColorSchemeProvider>` in
|
|
8
|
+
> the root layout renders the inline script that puts the class on before
|
|
9
|
+
> the first paint and keeps it in step afterwards; `useColorScheme()` reads
|
|
10
|
+
> it. The server renders the default.
|
|
11
|
+
>
|
|
12
|
+
> Like every k8ordo package it assumes React 19 and Server Components, uses
|
|
13
|
+
> only what has reached Baseline newly available, and ships no polyfills or
|
|
14
|
+
> legacy fallbacks.
|
|
15
|
+
|
|
16
|
+
When installed via npm, these docs live under `node_modules/@k8ordo/color-scheme/docs/`.
|
|
17
|
+
|
|
18
|
+
## Guide
|
|
19
|
+
|
|
20
|
+
- [Design Guide](GUIDE.md): what the package owns and does not (the class,
|
|
21
|
+
not the colours; the rule, not the storage), placing the provider in the
|
|
22
|
+
root layout, reading and changing the preference with the hook, what it
|
|
23
|
+
guarantees, and testing.
|
|
24
|
+
|
|
25
|
+
## Entry point
|
|
26
|
+
|
|
27
|
+
- `@k8ordo/color-scheme` — `<ColorSchemeProvider defaultPreference?>`
|
|
28
|
+
(client; goes inside `<body>` around everything) renders the inline
|
|
29
|
+
`<script>` that puts `dark` on `<html>` before the first paint from the
|
|
30
|
+
stored preference, the default (`'system'` unless told otherwise), or the
|
|
31
|
+
system, and keeps the class in step. `useColorScheme()` returns
|
|
32
|
+
`{ scheme, preference, setPreference }`: `scheme` is `'light' | 'dark'`
|
|
33
|
+
on screen, `preference` is `'light' | 'dark' | 'system'`, and
|
|
34
|
+
`setPreference('system')` clears the stored row. `colorSchemeState` is the
|
|
35
|
+
`@k8ordo/state` local definition itself (`k8ordo-state:color-scheme`).
|
|
36
|
+
Types: `ColorScheme`, `ColorSchemePreference`, `ColorSchemeProviderProps`,
|
|
37
|
+
`UseColorScheme`.
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@k8ordo/color-scheme",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The colour-scheme axis of an application, owned: the visitor's light / dark / system preference, kept in localStorage, resolved against the system, put on <html> before the first paint and kept there.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"color-scheme",
|
|
7
|
+
"dark-mode",
|
|
8
|
+
"prefers-color-scheme",
|
|
9
|
+
"react",
|
|
10
|
+
"server-components",
|
|
11
|
+
"theme"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://ordo.k8o.me",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/k35o/k8ordo/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "k8o <kosakanoki@gmail.com>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/k35o/k8ordo.git"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/**",
|
|
25
|
+
"docs/**"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
42
|
+
"@k8ordo/state": "^0.2.0",
|
|
43
|
+
"@types/react": "19.3.0",
|
|
44
|
+
"@types/react-dom": "19.3.0",
|
|
45
|
+
"@vitejs/plugin-react": "6.1.1",
|
|
46
|
+
"@vitest/browser-playwright": "5.0.0",
|
|
47
|
+
"publint": "0.3.24",
|
|
48
|
+
"react": "19.3.0",
|
|
49
|
+
"react-dom": "19.3.0",
|
|
50
|
+
"typescript": "7.0.2",
|
|
51
|
+
"vite": "8.2.2",
|
|
52
|
+
"vite-plus": "0.3.0",
|
|
53
|
+
"vitest": "5.0.0",
|
|
54
|
+
"vitest-browser-react": "2.3.0",
|
|
55
|
+
"zod": "4.5.4"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@k8ordo/state": "^0.2.0",
|
|
59
|
+
"@types/react": ">=19.3.0",
|
|
60
|
+
"react": ">=19.3.0",
|
|
61
|
+
"typescript": ">=7.0.2",
|
|
62
|
+
"zod": "^4.4.3"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@types/react": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"typescript": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "vp pack",
|
|
74
|
+
"test": "vp test",
|
|
75
|
+
"typecheck": "tsc --noEmit",
|
|
76
|
+
"check": "vp check",
|
|
77
|
+
"check:write": "vp check --fix",
|
|
78
|
+
"check:package": "publint && attw --pack . --profile esm-only"
|
|
79
|
+
}
|
|
80
|
+
}
|