@vipengele/react-tokens 0.0.0 → 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 +48 -3
- package/dist/ThemeProvider.d.ts +22 -0
- package/dist/ThemeProvider.d.ts.map +1 -0
- package/dist/base-stylesheet.d.ts +22 -0
- package/dist/base-stylesheet.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +275 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.d.ts +105 -0
- package/dist/theme.d.ts.map +1 -0
- package/package.json +41 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Gomes
|
|
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
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
# @vipengele/react-tokens
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
be enrolled on this name — npm can enrol one only on a name the registry already holds.
|
|
3
|
+
Seed-and-derive theming for the vipengele design system: `createTheme` and `ThemeProvider`.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
```tsx
|
|
6
|
+
import { createTheme, ThemeProvider } from "@vipengele/react-tokens";
|
|
7
|
+
|
|
8
|
+
const theme = createTheme({ accent: "oklch(0.62 0.19 264)" });
|
|
9
|
+
|
|
10
|
+
<ThemeProvider theme={theme}>
|
|
11
|
+
<App />
|
|
12
|
+
</ThemeProvider>;
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Seeding
|
|
16
|
+
|
|
17
|
+
A `ThemeSeed` is the small set of values a consumer supplies — `accent`, `ink`, `surface`,
|
|
18
|
+
`radius`, `fontSans`, `fontMono`. Each has a default, so `createTheme()` returns a complete
|
|
19
|
+
theme. `createTheme` expands the seed into a frozen `Theme`: a flat, JSON-serializable record
|
|
20
|
+
of `--vpg-*` CSS custom properties.
|
|
21
|
+
|
|
22
|
+
## Ramps are CSS, not JavaScript
|
|
23
|
+
|
|
24
|
+
The hover/press/wash/dark ramps are `oklch()` relative-colour expressions, not colours
|
|
25
|
+
computed at build time:
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
--vpg-accent-hover: oklch(
|
|
29
|
+
from var(--vpg-accent) calc(l + var(--vpg-state-shift)) c h
|
|
30
|
+
);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The browser resolves them at paint time from whatever `--vpg-accent` currently is, so a
|
|
34
|
+
mode flip reassigns three colours and three scalars and the whole ramp follows — no second
|
|
35
|
+
theme object, no re-render.
|
|
36
|
+
|
|
37
|
+
## Colour mode
|
|
38
|
+
|
|
39
|
+
`colorMode="light" | "dark"` writes `data-vpg-mode` on the provider's root. Omit it and
|
|
40
|
+
the base stylesheet lets the host page's `:root[data-theme="dark"]` or the OS
|
|
41
|
+
`prefers-color-scheme` decide; an explicit `colorMode` always wins over both.
|
|
42
|
+
|
|
43
|
+
## No `useTheme()`
|
|
44
|
+
|
|
45
|
+
There is no hook. Components read theme values through CSS custom properties in their own
|
|
46
|
+
stylesheets. See `docs/adr/0001-theming-via-css-custom-properties-no-context-hook.md`.
|
|
47
|
+
|
|
48
|
+
## Peer dependencies
|
|
49
|
+
|
|
50
|
+
React 19 and React DOM 19. The base stylesheet is injected through React 19's
|
|
51
|
+
`<style href precedence>` de-duplication, which keeps this package `"sideEffects": false`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { type ColorMode, type Theme } from "./theme.js";
|
|
3
|
+
export interface ThemeProviderProps extends ComponentPropsWithoutRef<"div"> {
|
|
4
|
+
/** The `Theme` to scope to this subtree. Defaults to `createTheme()`. */
|
|
5
|
+
theme?: Theme;
|
|
6
|
+
/**
|
|
7
|
+
* Forces a mode for this subtree. Omitted, no `data-vpg-mode` attribute is written
|
|
8
|
+
* at all, which is what lets the host page's `[data-theme]` or `prefers-color-scheme`
|
|
9
|
+
* fall through via the base stylesheet.
|
|
10
|
+
*/
|
|
11
|
+
colorMode?: ColorMode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Applies a `Theme` as inline custom properties on a `.vpg-root` element of its own.
|
|
15
|
+
*
|
|
16
|
+
* Nothing is written to `:root` or `document.documentElement`, so two providers on one
|
|
17
|
+
* page are independently themed and neither can leak into the other or into the host.
|
|
18
|
+
* Components consume the result through CSS custom properties in their own stylesheets —
|
|
19
|
+
* there is deliberately no `useTheme()` hook (ADR-0001).
|
|
20
|
+
*/
|
|
21
|
+
export declare function ThemeProvider({ theme, colorMode, className, style, children, ...rest }: ThemeProviderProps): import("react").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=ThemeProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../src/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAiB,MAAM,OAAO,CAAC;AAErE,OAAO,EAAE,KAAK,SAAS,EAAe,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAIrE,MAAM,WAAW,kBAAmB,SAAQ,wBAAwB,CAAC,KAAK,CAAC;IACzE,yEAAyE;IACzE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,KAAqB,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,kBAAkB,+BAoB1H"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base stylesheet for every `.vpg-root`, injected as an inline `<style>` rather than a
|
|
3
|
+
* `.css` import so the package can stay `"sideEffects": false`.
|
|
4
|
+
*
|
|
5
|
+
* The three dark selectors are ordered by how explicit their signal is, and all three sit
|
|
6
|
+
* at the same specificity-free level of intent:
|
|
7
|
+
*
|
|
8
|
+
* - `.vpg-root[data-vpg-mode="dark"]` — the consumer said so on this provider.
|
|
9
|
+
* - `:root[data-theme="dark"] .vpg-root:not([data-vpg-mode="light"])` — the host
|
|
10
|
+
* page said so, and this provider did not say otherwise.
|
|
11
|
+
* - `prefers-color-scheme: dark` — the OS said so, and neither the host page nor this
|
|
12
|
+
* provider said otherwise.
|
|
13
|
+
*
|
|
14
|
+
* The `:not([data-vpg-mode="light"])` guards are what make an explicit `colorMode`
|
|
15
|
+
* win over inherited host state; the `:root:not([data-theme="light"])` guard on the media
|
|
16
|
+
* query is what stops the OS preference overriding a host page that has opted into light.
|
|
17
|
+
*
|
|
18
|
+
* The reduced-motion query is a second, independent axis: it matches `.vpg-root` plainly
|
|
19
|
+
* and reassigns nothing but the three motion durations, so it composes with any colour mode.
|
|
20
|
+
*/
|
|
21
|
+
export declare const baseStylesheet = "\n.vpg-root {\n /* color-scheme: light pins the default arm of the light-dark() colours below, so a host\n page declaring color-scheme: dark on an ancestor cannot darken a root whose Vipengele mode\n is light. The dark rules reassign color-scheme, and that is what selects the -dark arms.\n\n Every property whose value depends on an environment condition the cascade resolves \u2014 the\n mode-resolved colours, the three ramp scalars, the two shadow inks and the three motion\n durations \u2014 belongs here rather than in the Theme object ThemeProvider applies inline,\n alongside the color-scheme that resolves the colours. An inline style declaration always\n wins over a stylesheet rule for the same property on the same element, media query or not,\n and these sit on the very element the rules below match, so anything applied inline is\n beyond the reach of every one of them \u2014 the switch would be dead on arrival (ADR-0007). */\n color-scheme: light;\n --vpg-accent: light-dark(var(--vpg-accent-light), var(--vpg-accent-dark));\n --vpg-ink: light-dark(var(--vpg-ink-light), var(--vpg-ink-dark));\n --vpg-surface: light-dark(var(--vpg-surface-light), var(--vpg-surface-dark));\n\n /* The status colour, mode-resolved for the same reason the accent is: a red that reads as an\n error against a near-white ground is muddy against a dark one. */\n --vpg-danger: light-dark(var(--vpg-danger-light), var(--vpg-danger-dark));\n\n /* The two inks every elevation shadow is drawn in: a tight contact layer and a wide ambient\n one. Both are mode-resolved \u2014 the alphas that read as depth over a light surface disappear\n against a dark one, where the shadow has to be near-opaque to register at all \u2014 and both\n are colours, so light-dark() carries them exactly as it carries the colours above. */\n --vpg-shadow-contact: light-dark(oklch(0 0 0 / 0.08), oklch(0 0 0 / 0.44));\n --vpg-shadow-ambient: light-dark(oklch(0 0 0 / 0.06), oklch(0 0 0 / 0.32));\n\n /* The light arm of the ramp scalars. They are unitless numbers read inside calc(), which\n light-dark() cannot carry, so the dark rules declare their own values. */\n --vpg-state-shift: -0.05;\n --vpg-lift: 0.02;\n --vpg-sink: 0.04;\n\n /* Motion durations. fast covers a state change on a control the pointer is already over,\n normal an element entering or leaving the layout, slow a surface crossing the viewport.\n They live here rather than in the Theme for the same reason as everything above: the\n reduced-motion query below reassigns them, and a media query is still a stylesheet rule,\n so an inline declaration on this element would be beyond its reach (ADR-0007). */\n --vpg-duration-fast: 120ms;\n --vpg-duration-normal: 200ms;\n --vpg-duration-slow: 320ms;\n\n color: var(--vpg-ink);\n background-color: var(--vpg-surface);\n font-family: var(--vpg-font-sans);\n}\n\n.vpg-root[data-vpg-mode=\"dark\"] {\n color-scheme: dark;\n --vpg-state-shift: 0.05;\n --vpg-lift: 0.055;\n --vpg-sink: 0.025;\n}\n\n:root[data-theme=\"dark\"] .vpg-root:not([data-vpg-mode=\"light\"]) {\n color-scheme: dark;\n --vpg-state-shift: 0.05;\n --vpg-lift: 0.055;\n --vpg-sink: 0.025;\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) .vpg-root:not([data-vpg-mode=\"light\"]) {\n color-scheme: dark;\n --vpg-state-shift: 0.05;\n --vpg-lift: 0.055;\n --vpg-sink: 0.025;\n}\n}\n\n@media (prefers-reduced-motion: reduce) {\n /* 0.01ms rather than 0s: a duration of zero makes a transition instantaneous, and an engine\n that skips it fires no transitionend, stranding any listener that drives a state change off\n that event. 0.01ms is equally imperceptible and still completes a transition properly. */\n .vpg-root {\n --vpg-duration-fast: 0.01ms;\n --vpg-duration-normal: 0.01ms;\n --vpg-duration-slow: 0.01ms;\n }\n}\n";
|
|
22
|
+
//# sourceMappingURL=base-stylesheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-stylesheet.d.ts","sourceRoot":"","sources":["../src/base-stylesheet.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc,u2HAmE1B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { baseStylesheet } from "./base-stylesheet.js";
|
|
2
|
+
export { ThemeProvider, type ThemeProviderProps } from "./ThemeProvider.js";
|
|
3
|
+
export { type ColorMode, createTheme, type StylesheetOwnedProperty, type Theme, type ThemeOverrides, type ThemeSeed, } from "./theme.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EACL,KAAK,SAAS,EACd,WAAW,EACX,KAAK,uBAAuB,EAC5B,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
// src/base-stylesheet.ts
|
|
4
|
+
var DARK_DECLARATIONS = `
|
|
5
|
+
color-scheme: dark;
|
|
6
|
+
--vpg-state-shift: 0.05;
|
|
7
|
+
--vpg-lift: 0.055;
|
|
8
|
+
--vpg-sink: 0.025;
|
|
9
|
+
`;
|
|
10
|
+
var baseStylesheet = `
|
|
11
|
+
.vpg-root {
|
|
12
|
+
/* color-scheme: light pins the default arm of the light-dark() colours below, so a host
|
|
13
|
+
page declaring color-scheme: dark on an ancestor cannot darken a root whose Vipengele mode
|
|
14
|
+
is light. The dark rules reassign color-scheme, and that is what selects the -dark arms.
|
|
15
|
+
|
|
16
|
+
Every property whose value depends on an environment condition the cascade resolves \u2014 the
|
|
17
|
+
mode-resolved colours, the three ramp scalars, the two shadow inks and the three motion
|
|
18
|
+
durations \u2014 belongs here rather than in the Theme object ThemeProvider applies inline,
|
|
19
|
+
alongside the color-scheme that resolves the colours. An inline style declaration always
|
|
20
|
+
wins over a stylesheet rule for the same property on the same element, media query or not,
|
|
21
|
+
and these sit on the very element the rules below match, so anything applied inline is
|
|
22
|
+
beyond the reach of every one of them \u2014 the switch would be dead on arrival (ADR-0007). */
|
|
23
|
+
color-scheme: light;
|
|
24
|
+
--vpg-accent: light-dark(var(--vpg-accent-light), var(--vpg-accent-dark));
|
|
25
|
+
--vpg-ink: light-dark(var(--vpg-ink-light), var(--vpg-ink-dark));
|
|
26
|
+
--vpg-surface: light-dark(var(--vpg-surface-light), var(--vpg-surface-dark));
|
|
27
|
+
|
|
28
|
+
/* The status colour, mode-resolved for the same reason the accent is: a red that reads as an
|
|
29
|
+
error against a near-white ground is muddy against a dark one. */
|
|
30
|
+
--vpg-danger: light-dark(var(--vpg-danger-light), var(--vpg-danger-dark));
|
|
31
|
+
|
|
32
|
+
/* The two inks every elevation shadow is drawn in: a tight contact layer and a wide ambient
|
|
33
|
+
one. Both are mode-resolved \u2014 the alphas that read as depth over a light surface disappear
|
|
34
|
+
against a dark one, where the shadow has to be near-opaque to register at all \u2014 and both
|
|
35
|
+
are colours, so light-dark() carries them exactly as it carries the colours above. */
|
|
36
|
+
--vpg-shadow-contact: light-dark(oklch(0 0 0 / 0.08), oklch(0 0 0 / 0.44));
|
|
37
|
+
--vpg-shadow-ambient: light-dark(oklch(0 0 0 / 0.06), oklch(0 0 0 / 0.32));
|
|
38
|
+
|
|
39
|
+
/* The light arm of the ramp scalars. They are unitless numbers read inside calc(), which
|
|
40
|
+
light-dark() cannot carry, so the dark rules declare their own values. */
|
|
41
|
+
--vpg-state-shift: -0.05;
|
|
42
|
+
--vpg-lift: 0.02;
|
|
43
|
+
--vpg-sink: 0.04;
|
|
44
|
+
|
|
45
|
+
/* Motion durations. fast covers a state change on a control the pointer is already over,
|
|
46
|
+
normal an element entering or leaving the layout, slow a surface crossing the viewport.
|
|
47
|
+
They live here rather than in the Theme for the same reason as everything above: the
|
|
48
|
+
reduced-motion query below reassigns them, and a media query is still a stylesheet rule,
|
|
49
|
+
so an inline declaration on this element would be beyond its reach (ADR-0007). */
|
|
50
|
+
--vpg-duration-fast: 120ms;
|
|
51
|
+
--vpg-duration-normal: 200ms;
|
|
52
|
+
--vpg-duration-slow: 320ms;
|
|
53
|
+
|
|
54
|
+
color: var(--vpg-ink);
|
|
55
|
+
background-color: var(--vpg-surface);
|
|
56
|
+
font-family: var(--vpg-font-sans);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.vpg-root[data-vpg-mode="dark"] {${DARK_DECLARATIONS}}
|
|
60
|
+
|
|
61
|
+
:root[data-theme="dark"] .vpg-root:not([data-vpg-mode="light"]) {${DARK_DECLARATIONS}}
|
|
62
|
+
|
|
63
|
+
@media (prefers-color-scheme: dark) {
|
|
64
|
+
:root:not([data-theme="light"]) .vpg-root:not([data-vpg-mode="light"]) {${DARK_DECLARATIONS}}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@media (prefers-reduced-motion: reduce) {
|
|
68
|
+
/* 0.01ms rather than 0s: a duration of zero makes a transition instantaneous, and an engine
|
|
69
|
+
that skips it fires no transitionend, stranding any listener that drives a state change off
|
|
70
|
+
that event. 0.01ms is equally imperceptible and still completes a transition properly. */
|
|
71
|
+
.vpg-root {
|
|
72
|
+
--vpg-duration-fast: 0.01ms;
|
|
73
|
+
--vpg-duration-normal: 0.01ms;
|
|
74
|
+
--vpg-duration-slow: 0.01ms;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
// src/theme.ts
|
|
80
|
+
var STYLESHEET_OWNED_PROPERTIES = [
|
|
81
|
+
"--vpg-accent",
|
|
82
|
+
"--vpg-ink",
|
|
83
|
+
"--vpg-surface",
|
|
84
|
+
"--vpg-danger",
|
|
85
|
+
"--vpg-shadow-contact",
|
|
86
|
+
"--vpg-shadow-ambient",
|
|
87
|
+
"--vpg-state-shift",
|
|
88
|
+
"--vpg-lift",
|
|
89
|
+
"--vpg-sink",
|
|
90
|
+
"--vpg-duration-fast",
|
|
91
|
+
"--vpg-duration-normal",
|
|
92
|
+
"--vpg-duration-slow"
|
|
93
|
+
];
|
|
94
|
+
var DEFAULT_SEED = {
|
|
95
|
+
accent: "oklch(0.58 0.19 264)",
|
|
96
|
+
danger: "oklch(0.55 0.21 27)",
|
|
97
|
+
ink: "oklch(0.22 0.02 264)",
|
|
98
|
+
surface: "oklch(0.99 0.003 264)",
|
|
99
|
+
radius: "0.5rem",
|
|
100
|
+
fontSans: '"Poppins", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
101
|
+
fontMono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
|
|
102
|
+
};
|
|
103
|
+
function createTheme(seed = {}, overrides = {}) {
|
|
104
|
+
const { accent, danger, ink, surface, radius, fontSans, fontMono } = {
|
|
105
|
+
...DEFAULT_SEED,
|
|
106
|
+
...seed
|
|
107
|
+
};
|
|
108
|
+
const shadowed = STYLESHEET_OWNED_PROPERTIES.filter((property) => property in overrides);
|
|
109
|
+
if (shadowed.length > 0) {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
`createTheme cannot override the stylesheet-owned ${shadowed.length === 1 ? "property" : "properties"} ${shadowed.join(", ")}: ThemeProvider applies a Theme inline, where no colour-mode or reduced-motion rule can reach it. Assign them in a stylesheet rule of your own instead.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const theme = {
|
|
115
|
+
// The light appearance, carrying the seed verbatim. Never overridden by a mode rule,
|
|
116
|
+
// so the dark variants below always resolve against the colour the consumer passed.
|
|
117
|
+
"--vpg-accent-light": accent,
|
|
118
|
+
"--vpg-danger-light": danger,
|
|
119
|
+
"--vpg-ink-light": ink,
|
|
120
|
+
"--vpg-surface-light": surface,
|
|
121
|
+
// The dark appearance, derived from the light variants.
|
|
122
|
+
"--vpg-accent-dark": "oklch(from var(--vpg-accent-light) calc(l + 0.08) calc(c * 0.92) h)",
|
|
123
|
+
"--vpg-danger-dark": "oklch(from var(--vpg-danger-light) calc(l + 0.08) calc(c * 0.92) h)",
|
|
124
|
+
"--vpg-ink-dark": "oklch(from var(--vpg-ink-light) 0.94 calc(c * 0.6) h)",
|
|
125
|
+
// `max(..., 0.015)` floors the chroma rather than letting it scale purely off the seed's
|
|
126
|
+
// own: a near-neutral seed (the default's c is 0.003) would otherwise multiply down to a
|
|
127
|
+
// chroma so small the surface reads as flat, colourless near-black instead of a dark tint
|
|
128
|
+
// of the seed's hue. 0.40 reads as a dark charcoal rather than near-black, while staying
|
|
129
|
+
// dark enough that `--vpg-ink-dark`'s 0.94 lightness keeps strong text contrast on it.
|
|
130
|
+
"--vpg-surface-dark": "oklch(from var(--vpg-surface-light) 0.40 max(c * 3, 0.015) h)",
|
|
131
|
+
// Accent ramp. `--vpg-state-shift` comes from the stylesheet, not from here: its
|
|
132
|
+
// sign flips with the mode, so a hover lightens on a dark ground and darkens on a light
|
|
133
|
+
// one, and the ramps below re-derive themselves when the dark rule reassigns it.
|
|
134
|
+
"--vpg-accent-hover": "oklch(from var(--vpg-accent) calc(l + var(--vpg-state-shift)) c h)",
|
|
135
|
+
"--vpg-accent-press": "oklch(from var(--vpg-accent) calc(l + var(--vpg-state-shift) * 2) c h)",
|
|
136
|
+
"--vpg-accent-wash": "oklch(from var(--vpg-accent) calc(l - var(--vpg-state-shift) * 6.5) calc(c * 0.16) h)",
|
|
137
|
+
"--vpg-accent-ring": "oklch(from var(--vpg-accent) l c h / 0.45)",
|
|
138
|
+
// Black or white, whichever reads on the accent: `(0.68 - l) * 1000` saturates the
|
|
139
|
+
// clamp to 0 or 1 either side of the lightness threshold.
|
|
140
|
+
"--vpg-accent-contrast": "oklch(from var(--vpg-accent) clamp(0, (0.68 - l) * 1000, 1) 0 h)",
|
|
141
|
+
// Danger ramp, derived from `--vpg-danger` exactly as the accent ramp is derived from
|
|
142
|
+
// `--vpg-accent`: a destructive control carries the same hover, press, ring and
|
|
143
|
+
// contrast relationships as a primary one, differing only in the colour it ramps off.
|
|
144
|
+
"--vpg-danger-hover": "oklch(from var(--vpg-danger) calc(l + var(--vpg-state-shift)) c h)",
|
|
145
|
+
"--vpg-danger-press": "oklch(from var(--vpg-danger) calc(l + var(--vpg-state-shift) * 2) c h)",
|
|
146
|
+
"--vpg-danger-ring": "oklch(from var(--vpg-danger) l c h / 0.45)",
|
|
147
|
+
"--vpg-danger-contrast": "oklch(from var(--vpg-danger) clamp(0, (0.68 - l) * 1000, 1) 0 h)",
|
|
148
|
+
// Ink ramp. Alpha rather than lightness, so these stay legible against any surface
|
|
149
|
+
// and flip with the mode for free.
|
|
150
|
+
"--vpg-ink-muted": "oklch(from var(--vpg-ink) l c h / 0.68)",
|
|
151
|
+
"--vpg-ink-subtle": "oklch(from var(--vpg-ink) l c h / 0.45)",
|
|
152
|
+
"--vpg-border": "oklch(from var(--vpg-ink) l c h / 0.16)",
|
|
153
|
+
"--vpg-border-strong": "oklch(from var(--vpg-ink) l c h / 0.32)",
|
|
154
|
+
// Surface ramp. `--vpg-lift` and `--vpg-sink` also come from the stylesheet: a
|
|
155
|
+
// dark ground needs a wider lift to read as raised and a narrower sink before it reads
|
|
156
|
+
// as a hole.
|
|
157
|
+
"--vpg-surface-raised": "oklch(from var(--vpg-surface) calc(l + var(--vpg-lift)) c h)",
|
|
158
|
+
"--vpg-surface-sunken": "oklch(from var(--vpg-surface) calc(l - var(--vpg-sink)) c h)",
|
|
159
|
+
"--vpg-surface-hover": "oklch(from var(--vpg-surface) calc(l - var(--vpg-sink) * 0.5) c h)",
|
|
160
|
+
"--vpg-surface-press": "oklch(from var(--vpg-surface) calc(l - var(--vpg-sink) * 1.5) c h)",
|
|
161
|
+
"--vpg-radius": radius,
|
|
162
|
+
// Both steps are `calc()` multiples of the seed, so a consumer who reseeds `radius` keeps a
|
|
163
|
+
// coherent ladder. The multipliers land the default 0.5rem seed on 6px inner and 12px outer,
|
|
164
|
+
// the range at which a 2rem control reads as rounded rather than as a pill or a rectangle.
|
|
165
|
+
"--vpg-radius-sm": "calc(var(--vpg-radius) * 0.75)",
|
|
166
|
+
"--vpg-radius-lg": "calc(var(--vpg-radius) * 1.5)",
|
|
167
|
+
"--vpg-radius-full": "9999px",
|
|
168
|
+
"--vpg-font-sans": fontSans,
|
|
169
|
+
"--vpg-font-mono": fontMono,
|
|
170
|
+
// Control size scale: the outer box height of anything a pointer targets — button, field,
|
|
171
|
+
// option row, toggle. `md` is the default control height every other step is read against.
|
|
172
|
+
"--vpg-size-xs": "1.5rem",
|
|
173
|
+
"--vpg-size-sm": "1.75rem",
|
|
174
|
+
"--vpg-size-md": "2rem",
|
|
175
|
+
"--vpg-size-lg": "2.25rem",
|
|
176
|
+
"--vpg-size-xl": "2.5rem",
|
|
177
|
+
// Past the range a pointer aims at: a display step, for something sized like a large
|
|
178
|
+
// avatar rather than targeted.
|
|
179
|
+
"--vpg-size-2xl": "3rem",
|
|
180
|
+
// Glyph box of an icon sitting inside a control. Sized independently of the control: an
|
|
181
|
+
// icon scaled off the control height crowds a dense row long before the text does.
|
|
182
|
+
"--vpg-icon-sm": "0.875rem",
|
|
183
|
+
"--vpg-icon-md": "1rem",
|
|
184
|
+
"--vpg-icon-lg": "1.25rem",
|
|
185
|
+
"--vpg-icon-xl": "1.5rem",
|
|
186
|
+
// Spacing scale, `n * 0.25rem`. Every gap, padding and inset steps through it, so two
|
|
187
|
+
// components side by side align without either knowing the other's measurements.
|
|
188
|
+
"--vpg-space-1": "0.25rem",
|
|
189
|
+
"--vpg-space-2": "0.5rem",
|
|
190
|
+
"--vpg-space-3": "0.75rem",
|
|
191
|
+
"--vpg-space-4": "1rem",
|
|
192
|
+
"--vpg-space-5": "1.25rem",
|
|
193
|
+
"--vpg-space-6": "1.5rem",
|
|
194
|
+
"--vpg-space-7": "1.75rem",
|
|
195
|
+
"--vpg-space-8": "2rem",
|
|
196
|
+
// Type scale. `sm` is the body and label size — the size a control's own text takes.
|
|
197
|
+
"--vpg-font-size-xs": "0.75rem",
|
|
198
|
+
"--vpg-font-size-sm": "0.875rem",
|
|
199
|
+
"--vpg-font-size-md": "1rem",
|
|
200
|
+
"--vpg-font-size-lg": "1.125rem",
|
|
201
|
+
"--vpg-font-size-xl": "1.25rem",
|
|
202
|
+
"--vpg-font-size-2xl": "1.5rem",
|
|
203
|
+
"--vpg-font-size-3xl": "1.875rem",
|
|
204
|
+
"--vpg-font-size-4xl": "2.25rem",
|
|
205
|
+
"--vpg-font-size-5xl": "3rem",
|
|
206
|
+
"--vpg-font-weight-regular": "400",
|
|
207
|
+
"--vpg-font-weight-medium": "500",
|
|
208
|
+
"--vpg-font-weight-semibold": "600",
|
|
209
|
+
"--vpg-font-weight-bold": "700",
|
|
210
|
+
// Unitless, so a line box scales with whatever font size the element resolves to.
|
|
211
|
+
"--vpg-line-height-tight": "1.2",
|
|
212
|
+
"--vpg-line-height-snug": "1.35",
|
|
213
|
+
"--vpg-line-height-normal": "1.5",
|
|
214
|
+
"--vpg-line-height-relaxed": "1.65",
|
|
215
|
+
// In `em`, so tracking tightens with the type rather than staying a fixed distance that
|
|
216
|
+
// over-tightens small text.
|
|
217
|
+
"--vpg-letter-spacing-tight": "-0.02em",
|
|
218
|
+
"--vpg-letter-spacing-normal": "0em",
|
|
219
|
+
"--vpg-letter-spacing-wide": "0.02em",
|
|
220
|
+
// Focus-ring geometry, shared by every component that draws a ring on `:focus-visible`, so
|
|
221
|
+
// one ring is the same thickness at the same distance everywhere. The family carries no
|
|
222
|
+
// colour: `--vpg-accent-ring` is already that colour, and a ring drawn inside its
|
|
223
|
+
// element negates the offset rather than declaring its own.
|
|
224
|
+
"--vpg-focus-ring-width": "2px",
|
|
225
|
+
"--vpg-focus-ring-offset": "2px",
|
|
226
|
+
// Motion. The durations come from the stylesheet, not from here: they collapse under
|
|
227
|
+
// `prefers-reduced-motion: reduce`, and a media query cannot reach an inline declaration.
|
|
228
|
+
// The easings stay — a curve shapes a transition's progress and is meaningless at a
|
|
229
|
+
// collapsed duration, so none of them depends on the preference.
|
|
230
|
+
"--vpg-ease-standard": "cubic-bezier(0.2, 0, 0, 1)",
|
|
231
|
+
"--vpg-ease-entrance": "cubic-bezier(0, 0, 0.2, 1)",
|
|
232
|
+
"--vpg-ease-exit": "cubic-bezier(0.4, 0, 1, 1)",
|
|
233
|
+
// Elevation. Two layers each: a tight contact shadow that anchors the element to the
|
|
234
|
+
// ground it sits on, and a wide ambient one that carries the height. A single blurred
|
|
235
|
+
// layer reads as a blob at any offset large enough to be seen.
|
|
236
|
+
//
|
|
237
|
+
// The inks come from the stylesheet, not from here: a shadow that reads as depth on a
|
|
238
|
+
// light ground is invisible at the same alpha on a dark one, so the two inks are
|
|
239
|
+
// mode-resolved and these three compositions re-derive themselves when the mode flips.
|
|
240
|
+
"--vpg-shadow-low": "0 1px 1px var(--vpg-shadow-contact), 0 1px 3px -1px var(--vpg-shadow-ambient)",
|
|
241
|
+
"--vpg-shadow-med": "0 1px 2px var(--vpg-shadow-contact), 0 4px 10px -2px var(--vpg-shadow-ambient)",
|
|
242
|
+
"--vpg-shadow-high": "0 2px 4px var(--vpg-shadow-contact), 0 12px 28px -6px var(--vpg-shadow-ambient)",
|
|
243
|
+
// Stacking. Every floating surface portals into the same `.vpg-root`, so all of them
|
|
244
|
+
// are siblings in one stacking context and a shared z-index leaves the order to whichever
|
|
245
|
+
// mounted last. The order is containment: a listbox belongs to the control that opened it,
|
|
246
|
+
// a popover is a surface over the page that can contain that control, a tooltip can be
|
|
247
|
+
// triggered from inside either and must not be occluded by its own trigger. The 100-step
|
|
248
|
+
// gaps are where a consumer's own content goes between two adjacent Vipengele surfaces.
|
|
249
|
+
"--vpg-layer-listbox": "1000",
|
|
250
|
+
"--vpg-layer-popover": "1100",
|
|
251
|
+
"--vpg-layer-tooltip": "1200"
|
|
252
|
+
};
|
|
253
|
+
Object.assign(theme, overrides);
|
|
254
|
+
return Object.freeze(theme);
|
|
255
|
+
}
|
|
256
|
+
var DEFAULT_THEME = createTheme();
|
|
257
|
+
function ThemeProvider({ theme = DEFAULT_THEME, colorMode, className, style, children, ...rest }) {
|
|
258
|
+
return /* @__PURE__ */ jsxs(
|
|
259
|
+
"div",
|
|
260
|
+
{
|
|
261
|
+
...rest,
|
|
262
|
+
className: className ? `vpg-root ${className}` : "vpg-root",
|
|
263
|
+
"data-vpg-mode": colorMode,
|
|
264
|
+
style: { ...theme, ...style },
|
|
265
|
+
children: [
|
|
266
|
+
/* @__PURE__ */ jsx("style", { href: "vpg-base", precedence: "vpg-base", children: baseStylesheet }),
|
|
267
|
+
children
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { ThemeProvider, baseStylesheet, createTheme };
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
275
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/base-stylesheet.ts","../src/theme.ts","../src/ThemeProvider.tsx"],"names":[],"mappings":";;;AAaA,IAAM,iBAAA,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA2BnB,IAAM,cAAA,GAAiB;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA,iCAAA,EAiDK,iBAAiB,CAAA;;AAAA,iEAAA,EAEe,iBAAiB,CAAA;;AAAA;AAAA,0EAAA,EAGR,iBAAiB,CAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC9CtF,IAAM,2BAAA,GAA8B;AAAA,EACzC,cAAA;AAAA,EACA,WAAA;AAAA,EACA,eAAA;AAAA,EACA,cAAA;AAAA,EACA,sBAAA;AAAA,EACA,sBAAA;AAAA,EACA,mBAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,qBAAA;AAAA,EACA,uBAAA;AAAA,EACA;AACF,CAAA;AA2BA,IAAM,YAAA,GAAoC;AAAA,EACxC,MAAA,EAAQ,sBAAA;AAAA,EACR,MAAA,EAAQ,qBAAA;AAAA,EACR,GAAA,EAAK,sBAAA;AAAA,EACL,OAAA,EAAS,uBAAA;AAAA,EACT,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,4EAAA;AAAA,EACV,QAAA,EAAU;AACZ,CAAA;AAsCO,SAAS,YAAY,IAAA,GAAkB,EAAC,EAAG,SAAA,GAA4B,EAAC,EAAU;AACvF,EAAA,MAAM,EAAE,QAAQ,MAAA,EAAQ,GAAA,EAAK,SAAS,MAAA,EAAQ,QAAA,EAAU,UAAS,GAAI;AAAA,IACnE,GAAG,YAAA;AAAA,IACH,GAAG;AAAA,GACL;AAEA,EAAA,MAAM,WAAW,2BAAA,CAA4B,MAAA,CAAO,CAAC,QAAA,KAAa,YAAY,SAAS,CAAA;AACvF,EAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,SAAA;AAAA,MACR,CAAA,iDAAA,EAAoD,QAAA,CAAS,MAAA,KAAW,CAAA,GAAI,UAAA,GAAa,YAAY,CAAA,CAAA,EAAI,QAAA,CAAS,IAAA,CAAK,IAAI,CAAC,CAAA,uJAAA;AAAA,KAC9H;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAA2C;AAAA;AAAA;AAAA,IAG/C,oBAAA,EAAsB,MAAA;AAAA,IACtB,oBAAA,EAAsB,MAAA;AAAA,IACtB,iBAAA,EAAmB,GAAA;AAAA,IACnB,qBAAA,EAAuB,OAAA;AAAA;AAAA,IAGvB,mBAAA,EAAqB,qEAAA;AAAA,IACrB,mBAAA,EAAqB,qEAAA;AAAA,IACrB,gBAAA,EAAkB,uDAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMlB,oBAAA,EAAsB,+DAAA;AAAA;AAAA;AAAA;AAAA,IAKtB,oBAAA,EAAsB,oEAAA;AAAA,IACtB,oBAAA,EAAsB,wEAAA;AAAA,IACtB,mBAAA,EAAqB,uFAAA;AAAA,IACrB,mBAAA,EAAqB,4CAAA;AAAA;AAAA;AAAA,IAGrB,uBAAA,EAAyB,kEAAA;AAAA;AAAA;AAAA;AAAA,IAKzB,oBAAA,EAAsB,oEAAA;AAAA,IACtB,oBAAA,EAAsB,wEAAA;AAAA,IACtB,mBAAA,EAAqB,4CAAA;AAAA,IACrB,uBAAA,EAAyB,kEAAA;AAAA;AAAA;AAAA,IAIzB,iBAAA,EAAmB,yCAAA;AAAA,IACnB,kBAAA,EAAoB,yCAAA;AAAA,IACpB,cAAA,EAAgB,yCAAA;AAAA,IAChB,qBAAA,EAAuB,yCAAA;AAAA;AAAA;AAAA;AAAA,IAKvB,sBAAA,EAAwB,8DAAA;AAAA,IACxB,sBAAA,EAAwB,8DAAA;AAAA,IACxB,qBAAA,EAAuB,oEAAA;AAAA,IACvB,qBAAA,EAAuB,oEAAA;AAAA,IAEvB,cAAA,EAAgB,MAAA;AAAA;AAAA;AAAA;AAAA,IAIhB,iBAAA,EAAmB,gCAAA;AAAA,IACnB,iBAAA,EAAmB,+BAAA;AAAA,IACnB,mBAAA,EAAqB,QAAA;AAAA,IAErB,iBAAA,EAAmB,QAAA;AAAA,IACnB,iBAAA,EAAmB,QAAA;AAAA;AAAA;AAAA,IAInB,eAAA,EAAiB,QAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,MAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,QAAA;AAAA;AAAA;AAAA,IAGjB,gBAAA,EAAkB,MAAA;AAAA;AAAA;AAAA,IAIlB,eAAA,EAAiB,UAAA;AAAA,IACjB,eAAA,EAAiB,MAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,QAAA;AAAA;AAAA;AAAA,IAIjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,QAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,MAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,QAAA;AAAA,IACjB,eAAA,EAAiB,SAAA;AAAA,IACjB,eAAA,EAAiB,MAAA;AAAA;AAAA,IAGjB,oBAAA,EAAsB,SAAA;AAAA,IACtB,oBAAA,EAAsB,UAAA;AAAA,IACtB,oBAAA,EAAsB,MAAA;AAAA,IACtB,oBAAA,EAAsB,UAAA;AAAA,IACtB,oBAAA,EAAsB,SAAA;AAAA,IACtB,qBAAA,EAAuB,QAAA;AAAA,IACvB,qBAAA,EAAuB,UAAA;AAAA,IACvB,qBAAA,EAAuB,SAAA;AAAA,IACvB,qBAAA,EAAuB,MAAA;AAAA,IAEvB,2BAAA,EAA6B,KAAA;AAAA,IAC7B,0BAAA,EAA4B,KAAA;AAAA,IAC5B,4BAAA,EAA8B,KAAA;AAAA,IAC9B,wBAAA,EAA0B,KAAA;AAAA;AAAA,IAG1B,yBAAA,EAA2B,KAAA;AAAA,IAC3B,wBAAA,EAA0B,MAAA;AAAA,IAC1B,0BAAA,EAA4B,KAAA;AAAA,IAC5B,2BAAA,EAA6B,MAAA;AAAA;AAAA;AAAA,IAI7B,4BAAA,EAA8B,SAAA;AAAA,IAC9B,6BAAA,EAA+B,KAAA;AAAA,IAC/B,2BAAA,EAA6B,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAM7B,wBAAA,EAA0B,KAAA;AAAA,IAC1B,yBAAA,EAA2B,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAM3B,qBAAA,EAAuB,4BAAA;AAAA,IACvB,qBAAA,EAAuB,4BAAA;AAAA,IACvB,iBAAA,EAAmB,4BAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASnB,kBAAA,EAAoB,+EAAA;AAAA,IACpB,kBAAA,EAAoB,gFAAA;AAAA,IACpB,mBAAA,EAAqB,iFAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQrB,qBAAA,EAAuB,MAAA;AAAA,IACvB,qBAAA,EAAuB,MAAA;AAAA,IACvB,qBAAA,EAAuB;AAAA,GACzB;AAGA,EAAA,MAAA,CAAO,MAAA,CAAO,OAAO,SAAS,CAAA;AAE9B,EAAA,OAAO,MAAA,CAAO,OAAO,KAAK,CAAA;AAC5B;ACjTA,IAAM,gBAAgB,WAAA,EAAY;AAqB3B,SAAS,aAAA,CAAc,EAAE,KAAA,GAAQ,aAAA,EAAe,SAAA,EAAW,WAAW,KAAA,EAAO,QAAA,EAAU,GAAG,IAAA,EAAK,EAAuB;AAC3H,EAAA,uBACE,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACE,GAAG,IAAA;AAAA,MACJ,SAAA,EAAW,SAAA,GAAY,CAAA,SAAA,EAAY,SAAS,CAAA,CAAA,GAAK,UAAA;AAAA,MACjD,eAAA,EAAe,SAAA;AAAA,MACf,KAAA,EAAO,EAAE,GAAG,KAAA,EAAO,GAAG,KAAA,EAAM;AAAA,MAQ5B,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,OAAA,EAAA,EAAM,IAAA,EAAK,UAAA,EAAW,UAAA,EAAW,YAC/B,QAAA,EAAA,cAAA,EACH,CAAA;AAAA,QACC;AAAA;AAAA;AAAA,GACH;AAEJ","file":"index.js","sourcesContent":["/**\n * The dark-mode overrides, shared verbatim by all three selectors below.\n *\n * `color-scheme: dark` is what moves the mode-resolved colours and the two shadow inks: each is a\n * `light-dark()` in the base rule, and `light-dark()` picks its arm from the element's computed\n * `color-scheme`, so none of them needs a declaration here. The three ramp scalars do —\n * `light-dark()` is defined over `<color>` values and cannot carry a unitless number, so each\n * scalar takes its light value in the base rule and is reassigned here.\n *\n * Every other `--vpg-*` entry in a `Theme` is an expression reading the colours and\n * scalars back through `var()`, so the browser re-derives the whole ramp from this block\n * plus the `color-scheme` switch.\n */\nconst DARK_DECLARATIONS = `\n color-scheme: dark;\n --vpg-state-shift: 0.05;\n --vpg-lift: 0.055;\n --vpg-sink: 0.025;\n`;\n\n/**\n * Base stylesheet for every `.vpg-root`, injected as an inline `<style>` rather than a\n * `.css` import so the package can stay `\"sideEffects\": false`.\n *\n * The three dark selectors are ordered by how explicit their signal is, and all three sit\n * at the same specificity-free level of intent:\n *\n * - `.vpg-root[data-vpg-mode=\"dark\"]` — the consumer said so on this provider.\n * - `:root[data-theme=\"dark\"] .vpg-root:not([data-vpg-mode=\"light\"])` — the host\n * page said so, and this provider did not say otherwise.\n * - `prefers-color-scheme: dark` — the OS said so, and neither the host page nor this\n * provider said otherwise.\n *\n * The `:not([data-vpg-mode=\"light\"])` guards are what make an explicit `colorMode`\n * win over inherited host state; the `:root:not([data-theme=\"light\"])` guard on the media\n * query is what stops the OS preference overriding a host page that has opted into light.\n *\n * The reduced-motion query is a second, independent axis: it matches `.vpg-root` plainly\n * and reassigns nothing but the three motion durations, so it composes with any colour mode.\n */\nexport const baseStylesheet = `\n.vpg-root {\n /* color-scheme: light pins the default arm of the light-dark() colours below, so a host\n page declaring color-scheme: dark on an ancestor cannot darken a root whose Vipengele mode\n is light. The dark rules reassign color-scheme, and that is what selects the -dark arms.\n\n Every property whose value depends on an environment condition the cascade resolves — the\n mode-resolved colours, the three ramp scalars, the two shadow inks and the three motion\n durations — belongs here rather than in the Theme object ThemeProvider applies inline,\n alongside the color-scheme that resolves the colours. An inline style declaration always\n wins over a stylesheet rule for the same property on the same element, media query or not,\n and these sit on the very element the rules below match, so anything applied inline is\n beyond the reach of every one of them — the switch would be dead on arrival (ADR-0007). */\n color-scheme: light;\n --vpg-accent: light-dark(var(--vpg-accent-light), var(--vpg-accent-dark));\n --vpg-ink: light-dark(var(--vpg-ink-light), var(--vpg-ink-dark));\n --vpg-surface: light-dark(var(--vpg-surface-light), var(--vpg-surface-dark));\n\n /* The status colour, mode-resolved for the same reason the accent is: a red that reads as an\n error against a near-white ground is muddy against a dark one. */\n --vpg-danger: light-dark(var(--vpg-danger-light), var(--vpg-danger-dark));\n\n /* The two inks every elevation shadow is drawn in: a tight contact layer and a wide ambient\n one. Both are mode-resolved — the alphas that read as depth over a light surface disappear\n against a dark one, where the shadow has to be near-opaque to register at all — and both\n are colours, so light-dark() carries them exactly as it carries the colours above. */\n --vpg-shadow-contact: light-dark(oklch(0 0 0 / 0.08), oklch(0 0 0 / 0.44));\n --vpg-shadow-ambient: light-dark(oklch(0 0 0 / 0.06), oklch(0 0 0 / 0.32));\n\n /* The light arm of the ramp scalars. They are unitless numbers read inside calc(), which\n light-dark() cannot carry, so the dark rules declare their own values. */\n --vpg-state-shift: -0.05;\n --vpg-lift: 0.02;\n --vpg-sink: 0.04;\n\n /* Motion durations. fast covers a state change on a control the pointer is already over,\n normal an element entering or leaving the layout, slow a surface crossing the viewport.\n They live here rather than in the Theme for the same reason as everything above: the\n reduced-motion query below reassigns them, and a media query is still a stylesheet rule,\n so an inline declaration on this element would be beyond its reach (ADR-0007). */\n --vpg-duration-fast: 120ms;\n --vpg-duration-normal: 200ms;\n --vpg-duration-slow: 320ms;\n\n color: var(--vpg-ink);\n background-color: var(--vpg-surface);\n font-family: var(--vpg-font-sans);\n}\n\n.vpg-root[data-vpg-mode=\"dark\"] {${DARK_DECLARATIONS}}\n\n:root[data-theme=\"dark\"] .vpg-root:not([data-vpg-mode=\"light\"]) {${DARK_DECLARATIONS}}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) .vpg-root:not([data-vpg-mode=\"light\"]) {${DARK_DECLARATIONS}}\n}\n\n@media (prefers-reduced-motion: reduce) {\n /* 0.01ms rather than 0s: a duration of zero makes a transition instantaneous, and an engine\n that skips it fires no transitionend, stranding any listener that drives a state change off\n that event. 0.01ms is equally imperceptible and still completes a transition properly. */\n .vpg-root {\n --vpg-duration-fast: 0.01ms;\n --vpg-duration-normal: 0.01ms;\n --vpg-duration-slow: 0.01ms;\n }\n}\n`;\n","/**\n * `'light' | 'dark'`, applied as `data-vpg-mode` on `ThemeProvider`'s root.\n * Omitted, the root inherits the host page's `[data-theme]` or `prefers-color-scheme`.\n */\nexport type ColorMode = \"light\" | \"dark\";\n\n/**\n * The small set of user-supplied values `createTheme` expands into a full `Theme`.\n * Every field has a default, so `createTheme()` produces a complete theme.\n */\nexport interface ThemeSeed {\n /** Brand colour every interactive state ramps off. Must be an `oklch()` colour: the\n * ramps are `oklch(from ...)` relative colours, and a non-oklch seed makes the browser\n * convert it first, which loses the chroma headroom the wash and press steps assume. */\n accent?: string;\n /** Status colour every destructive and error state ramps off. Must be an `oklch()` colour,\n * for the same reason the accent must. */\n danger?: string;\n /** Foreground text colour. Also the source of every border and muted-text alpha. */\n ink?: string;\n /** Page background. Raised/sunken surfaces are lightness steps off it. */\n surface?: string;\n /** Base corner radius. `--vpg-radius-sm`/`-lg` are `calc()` multiples of it, at ×0.75\n * and ×1.5, so reseeding it moves the whole ladder together. */\n radius?: string;\n fontSans?: string;\n fontMono?: string;\n}\n\n/**\n * The frozen set of `--vpg-*` custom properties `ThemeProvider` applies inline to its\n * root element. Values are CSS strings, never JS-computed colours — the browser resolves\n * the ramps at paint time, so a mode flip is a pure-CSS cascade change. Deliberately\n * excludes every stylesheet-owned property: the colours `--vpg-accent`/`--vpg-ink`/\n * `--vpg-surface`/`--vpg-danger` (as opposed to their `-light`/`-dark` variants, which this DOES\n * include), the ramp scalars `--vpg-state-shift`/`--vpg-lift`/`--vpg-sink`, the\n * shadow inks `--vpg-shadow-contact`/`--vpg-shadow-ambient` and the motion durations\n * `--vpg-duration-fast`/`-normal`/`-slow`. The base stylesheet owns every one of them,\n * alongside the `color-scheme` that decides which arm of the colours' and inks' `light-dark()`\n * applies.\n */\nexport type Theme = Readonly<Record<`--vpg-${string}`, string>>;\n\n/**\n * The properties whose declared value depends on an environment condition only the cascade\n * resolves — the colour mode, the reduced-motion preference. The base stylesheet assigns every\n * one of them on `.vpg-root`, and `createTheme` emits none of them (ADR-0007).\n */\nexport const STYLESHEET_OWNED_PROPERTIES = [\n \"--vpg-accent\",\n \"--vpg-ink\",\n \"--vpg-surface\",\n \"--vpg-danger\",\n \"--vpg-shadow-contact\",\n \"--vpg-shadow-ambient\",\n \"--vpg-state-shift\",\n \"--vpg-lift\",\n \"--vpg-sink\",\n \"--vpg-duration-fast\",\n \"--vpg-duration-normal\",\n \"--vpg-duration-slow\",\n] as const;\n\n/**\n * A `--vpg-*` property the base stylesheet owns because its value depends on an environment\n * condition the cascade resolves: the colour mode for the colours, inks and ramp scalars, the\n * reduced-motion preference for the durations. Neither `createTheme`'s output nor a\n * `ThemeOverrides` may carry one: both reach the element as an inline style, which no mode rule\n * and no media query can override.\n */\nexport type StylesheetOwnedProperty = (typeof STYLESHEET_OWNED_PROPERTIES)[number];\n\n/**\n * A partial map of `--vpg-*` properties to CSS strings, composed over the seed-derived\n * result by `createTheme`.\n *\n * Any `--vpg-*` name is accepted, not just the ones `createTheme` emits, so a consumer can\n * carry their own properties on the same root and have them frozen into the same object.\n *\n * The stylesheet-owned properties are excluded: each is typed `never`, so naming one in an object\n * literal is a type error, and `createTheme` throws on one that reaches it through a wider type.\n */\nexport type ThemeOverrides = Readonly<\n Partial<Record<`--vpg-${string}`, string>> & {\n [K in StylesheetOwnedProperty]?: never;\n }\n>;\n\nconst DEFAULT_SEED: Required<ThemeSeed> = {\n accent: \"oklch(0.58 0.19 264)\",\n danger: \"oklch(0.55 0.21 27)\",\n ink: \"oklch(0.22 0.02 264)\",\n surface: \"oklch(0.99 0.003 264)\",\n radius: \"0.5rem\",\n fontSans: '\"Poppins\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif',\n fontMono: 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace',\n};\n\n/**\n * Expands a seed into a `Theme`.\n *\n * Only `--vpg-*-light`/`-dark` and the radius/font entries carry literal seed values.\n * Every other entry is a CSS expression that reads back through `var()`.\n *\n * The stylesheet-owned properties are deliberately ABSENT from this object: the colours\n * `--vpg-accent`, `--vpg-ink`, `--vpg-surface` and `--vpg-danger`, the ramp scalars\n * `--vpg-state-shift`, `--vpg-lift` and `--vpg-sink`, the shadow inks\n * `--vpg-shadow-contact` and `--vpg-shadow-ambient`, and the motion durations\n * `--vpg-duration-fast`, `--vpg-duration-normal` and `--vpg-duration-slow`. The\n * base stylesheet assigns each of them on `.vpg-root` — the colours and inks as\n * `light-dark(<light>, <dark>)` next to the `color-scheme` that picks the arm, the scalars as\n * their light values, the durations as their full-motion values — and its mode and\n * reduced-motion rules reassign them. `ThemeProvider` applies every key here as an inline\n * style, and an inline style declaration always wins over a stylesheet rule for the same\n * property on the same element — including one inside a media query — so anything inline is\n * beyond the reach of a rule matching that same element. Only the `-light`/`-dark` variants\n * below and the expressions that read a stylesheet-owned property back through `var()` are safe\n * to apply inline (ADR-0007).\n *\n * The seed always describes the light appearance — `-light` variants carry it verbatim, and\n * `-dark` variants derive from it via `oklch(from ...)`. They're kept as separate properties\n * (rather than letting `--vpg-accent-dark` derive from `--vpg-accent`) to break a\n * cycle: `--vpg-accent` is a `light-dark()` over both variants, so a\n * `--vpg-accent-dark` reading `var(--vpg-accent)` back would be self-referential and\n * invalid at computed-value time.\n *\n * `overrides` compose over the derived result, replacing or adding individual `--vpg-*`\n * values without restating a seed. They are subject to the same invariant, and more sharply:\n * everything here lands inline on `.vpg-root`, so an override naming a stylesheet-owned\n * property would shadow the base stylesheet's declaration and pin that property to one colour\n * mode, or to full motion, for the life of the provider. Passing one throws. The route to a\n * different value is a stylesheet rule of the consumer's own, at ordinary specificity, which the\n * mode and reduced-motion rules can still beat where they should.\n */\nexport function createTheme(seed: ThemeSeed = {}, overrides: ThemeOverrides = {}): Theme {\n const { accent, danger, ink, surface, radius, fontSans, fontMono } = {\n ...DEFAULT_SEED,\n ...seed,\n };\n\n const shadowed = STYLESHEET_OWNED_PROPERTIES.filter((property) => property in overrides);\n if (shadowed.length > 0) {\n throw new TypeError(\n `createTheme cannot override the stylesheet-owned ${shadowed.length === 1 ? \"property\" : \"properties\"} ${shadowed.join(\", \")}: ThemeProvider applies a Theme inline, where no colour-mode or reduced-motion rule can reach it. Assign them in a stylesheet rule of your own instead.`,\n );\n }\n\n const theme: Record<`--vpg-${string}`, string> = {\n // The light appearance, carrying the seed verbatim. Never overridden by a mode rule,\n // so the dark variants below always resolve against the colour the consumer passed.\n \"--vpg-accent-light\": accent,\n \"--vpg-danger-light\": danger,\n \"--vpg-ink-light\": ink,\n \"--vpg-surface-light\": surface,\n\n // The dark appearance, derived from the light variants.\n \"--vpg-accent-dark\": \"oklch(from var(--vpg-accent-light) calc(l + 0.08) calc(c * 0.92) h)\",\n \"--vpg-danger-dark\": \"oklch(from var(--vpg-danger-light) calc(l + 0.08) calc(c * 0.92) h)\",\n \"--vpg-ink-dark\": \"oklch(from var(--vpg-ink-light) 0.94 calc(c * 0.6) h)\",\n // `max(..., 0.015)` floors the chroma rather than letting it scale purely off the seed's\n // own: a near-neutral seed (the default's c is 0.003) would otherwise multiply down to a\n // chroma so small the surface reads as flat, colourless near-black instead of a dark tint\n // of the seed's hue. 0.40 reads as a dark charcoal rather than near-black, while staying\n // dark enough that `--vpg-ink-dark`'s 0.94 lightness keeps strong text contrast on it.\n \"--vpg-surface-dark\": \"oklch(from var(--vpg-surface-light) 0.40 max(c * 3, 0.015) h)\",\n\n // Accent ramp. `--vpg-state-shift` comes from the stylesheet, not from here: its\n // sign flips with the mode, so a hover lightens on a dark ground and darkens on a light\n // one, and the ramps below re-derive themselves when the dark rule reassigns it.\n \"--vpg-accent-hover\": \"oklch(from var(--vpg-accent) calc(l + var(--vpg-state-shift)) c h)\",\n \"--vpg-accent-press\": \"oklch(from var(--vpg-accent) calc(l + var(--vpg-state-shift) * 2) c h)\",\n \"--vpg-accent-wash\": \"oklch(from var(--vpg-accent) calc(l - var(--vpg-state-shift) * 6.5) calc(c * 0.16) h)\",\n \"--vpg-accent-ring\": \"oklch(from var(--vpg-accent) l c h / 0.45)\",\n // Black or white, whichever reads on the accent: `(0.68 - l) * 1000` saturates the\n // clamp to 0 or 1 either side of the lightness threshold.\n \"--vpg-accent-contrast\": \"oklch(from var(--vpg-accent) clamp(0, (0.68 - l) * 1000, 1) 0 h)\",\n\n // Danger ramp, derived from `--vpg-danger` exactly as the accent ramp is derived from\n // `--vpg-accent`: a destructive control carries the same hover, press, ring and\n // contrast relationships as a primary one, differing only in the colour it ramps off.\n \"--vpg-danger-hover\": \"oklch(from var(--vpg-danger) calc(l + var(--vpg-state-shift)) c h)\",\n \"--vpg-danger-press\": \"oklch(from var(--vpg-danger) calc(l + var(--vpg-state-shift) * 2) c h)\",\n \"--vpg-danger-ring\": \"oklch(from var(--vpg-danger) l c h / 0.45)\",\n \"--vpg-danger-contrast\": \"oklch(from var(--vpg-danger) clamp(0, (0.68 - l) * 1000, 1) 0 h)\",\n\n // Ink ramp. Alpha rather than lightness, so these stay legible against any surface\n // and flip with the mode for free.\n \"--vpg-ink-muted\": \"oklch(from var(--vpg-ink) l c h / 0.68)\",\n \"--vpg-ink-subtle\": \"oklch(from var(--vpg-ink) l c h / 0.45)\",\n \"--vpg-border\": \"oklch(from var(--vpg-ink) l c h / 0.16)\",\n \"--vpg-border-strong\": \"oklch(from var(--vpg-ink) l c h / 0.32)\",\n\n // Surface ramp. `--vpg-lift` and `--vpg-sink` also come from the stylesheet: a\n // dark ground needs a wider lift to read as raised and a narrower sink before it reads\n // as a hole.\n \"--vpg-surface-raised\": \"oklch(from var(--vpg-surface) calc(l + var(--vpg-lift)) c h)\",\n \"--vpg-surface-sunken\": \"oklch(from var(--vpg-surface) calc(l - var(--vpg-sink)) c h)\",\n \"--vpg-surface-hover\": \"oklch(from var(--vpg-surface) calc(l - var(--vpg-sink) * 0.5) c h)\",\n \"--vpg-surface-press\": \"oklch(from var(--vpg-surface) calc(l - var(--vpg-sink) * 1.5) c h)\",\n\n \"--vpg-radius\": radius,\n // Both steps are `calc()` multiples of the seed, so a consumer who reseeds `radius` keeps a\n // coherent ladder. The multipliers land the default 0.5rem seed on 6px inner and 12px outer,\n // the range at which a 2rem control reads as rounded rather than as a pill or a rectangle.\n \"--vpg-radius-sm\": \"calc(var(--vpg-radius) * 0.75)\",\n \"--vpg-radius-lg\": \"calc(var(--vpg-radius) * 1.5)\",\n \"--vpg-radius-full\": \"9999px\",\n\n \"--vpg-font-sans\": fontSans,\n \"--vpg-font-mono\": fontMono,\n\n // Control size scale: the outer box height of anything a pointer targets — button, field,\n // option row, toggle. `md` is the default control height every other step is read against.\n \"--vpg-size-xs\": \"1.5rem\",\n \"--vpg-size-sm\": \"1.75rem\",\n \"--vpg-size-md\": \"2rem\",\n \"--vpg-size-lg\": \"2.25rem\",\n \"--vpg-size-xl\": \"2.5rem\",\n // Past the range a pointer aims at: a display step, for something sized like a large\n // avatar rather than targeted.\n \"--vpg-size-2xl\": \"3rem\",\n\n // Glyph box of an icon sitting inside a control. Sized independently of the control: an\n // icon scaled off the control height crowds a dense row long before the text does.\n \"--vpg-icon-sm\": \"0.875rem\",\n \"--vpg-icon-md\": \"1rem\",\n \"--vpg-icon-lg\": \"1.25rem\",\n \"--vpg-icon-xl\": \"1.5rem\",\n\n // Spacing scale, `n * 0.25rem`. Every gap, padding and inset steps through it, so two\n // components side by side align without either knowing the other's measurements.\n \"--vpg-space-1\": \"0.25rem\",\n \"--vpg-space-2\": \"0.5rem\",\n \"--vpg-space-3\": \"0.75rem\",\n \"--vpg-space-4\": \"1rem\",\n \"--vpg-space-5\": \"1.25rem\",\n \"--vpg-space-6\": \"1.5rem\",\n \"--vpg-space-7\": \"1.75rem\",\n \"--vpg-space-8\": \"2rem\",\n\n // Type scale. `sm` is the body and label size — the size a control's own text takes.\n \"--vpg-font-size-xs\": \"0.75rem\",\n \"--vpg-font-size-sm\": \"0.875rem\",\n \"--vpg-font-size-md\": \"1rem\",\n \"--vpg-font-size-lg\": \"1.125rem\",\n \"--vpg-font-size-xl\": \"1.25rem\",\n \"--vpg-font-size-2xl\": \"1.5rem\",\n \"--vpg-font-size-3xl\": \"1.875rem\",\n \"--vpg-font-size-4xl\": \"2.25rem\",\n \"--vpg-font-size-5xl\": \"3rem\",\n\n \"--vpg-font-weight-regular\": \"400\",\n \"--vpg-font-weight-medium\": \"500\",\n \"--vpg-font-weight-semibold\": \"600\",\n \"--vpg-font-weight-bold\": \"700\",\n\n // Unitless, so a line box scales with whatever font size the element resolves to.\n \"--vpg-line-height-tight\": \"1.2\",\n \"--vpg-line-height-snug\": \"1.35\",\n \"--vpg-line-height-normal\": \"1.5\",\n \"--vpg-line-height-relaxed\": \"1.65\",\n\n // In `em`, so tracking tightens with the type rather than staying a fixed distance that\n // over-tightens small text.\n \"--vpg-letter-spacing-tight\": \"-0.02em\",\n \"--vpg-letter-spacing-normal\": \"0em\",\n \"--vpg-letter-spacing-wide\": \"0.02em\",\n\n // Focus-ring geometry, shared by every component that draws a ring on `:focus-visible`, so\n // one ring is the same thickness at the same distance everywhere. The family carries no\n // colour: `--vpg-accent-ring` is already that colour, and a ring drawn inside its\n // element negates the offset rather than declaring its own.\n \"--vpg-focus-ring-width\": \"2px\",\n \"--vpg-focus-ring-offset\": \"2px\",\n\n // Motion. The durations come from the stylesheet, not from here: they collapse under\n // `prefers-reduced-motion: reduce`, and a media query cannot reach an inline declaration.\n // The easings stay — a curve shapes a transition's progress and is meaningless at a\n // collapsed duration, so none of them depends on the preference.\n \"--vpg-ease-standard\": \"cubic-bezier(0.2, 0, 0, 1)\",\n \"--vpg-ease-entrance\": \"cubic-bezier(0, 0, 0.2, 1)\",\n \"--vpg-ease-exit\": \"cubic-bezier(0.4, 0, 1, 1)\",\n\n // Elevation. Two layers each: a tight contact shadow that anchors the element to the\n // ground it sits on, and a wide ambient one that carries the height. A single blurred\n // layer reads as a blob at any offset large enough to be seen.\n //\n // The inks come from the stylesheet, not from here: a shadow that reads as depth on a\n // light ground is invisible at the same alpha on a dark one, so the two inks are\n // mode-resolved and these three compositions re-derive themselves when the mode flips.\n \"--vpg-shadow-low\": \"0 1px 1px var(--vpg-shadow-contact), 0 1px 3px -1px var(--vpg-shadow-ambient)\",\n \"--vpg-shadow-med\": \"0 1px 2px var(--vpg-shadow-contact), 0 4px 10px -2px var(--vpg-shadow-ambient)\",\n \"--vpg-shadow-high\": \"0 2px 4px var(--vpg-shadow-contact), 0 12px 28px -6px var(--vpg-shadow-ambient)\",\n\n // Stacking. Every floating surface portals into the same `.vpg-root`, so all of them\n // are siblings in one stacking context and a shared z-index leaves the order to whichever\n // mounted last. The order is containment: a listbox belongs to the control that opened it,\n // a popover is a surface over the page that can contain that control, a tooltip can be\n // triggered from inside either and must not be occluded by its own trigger. The 100-step\n // gaps are where a consumer's own content goes between two adjacent Vipengele surfaces.\n \"--vpg-layer-listbox\": \"1000\",\n \"--vpg-layer-popover\": \"1100\",\n \"--vpg-layer-tooltip\": \"1200\",\n };\n\n // Applied last, so a consumer's value replaces the derived one for the same property.\n Object.assign(theme, overrides);\n\n return Object.freeze(theme);\n}\n","import type { ComponentPropsWithoutRef, CSSProperties } from \"react\";\nimport { baseStylesheet } from \"./base-stylesheet.js\";\nimport { type ColorMode, createTheme, type Theme } from \"./theme.js\";\n\nconst DEFAULT_THEME = createTheme();\n\nexport interface ThemeProviderProps extends ComponentPropsWithoutRef<\"div\"> {\n /** The `Theme` to scope to this subtree. Defaults to `createTheme()`. */\n theme?: Theme;\n /**\n * Forces a mode for this subtree. Omitted, no `data-vpg-mode` attribute is written\n * at all, which is what lets the host page's `[data-theme]` or `prefers-color-scheme`\n * fall through via the base stylesheet.\n */\n colorMode?: ColorMode;\n}\n\n/**\n * Applies a `Theme` as inline custom properties on a `.vpg-root` element of its own.\n *\n * Nothing is written to `:root` or `document.documentElement`, so two providers on one\n * page are independently themed and neither can leak into the other or into the host.\n * Components consume the result through CSS custom properties in their own stylesheets —\n * there is deliberately no `useTheme()` hook (ADR-0001).\n */\nexport function ThemeProvider({ theme = DEFAULT_THEME, colorMode, className, style, children, ...rest }: ThemeProviderProps) {\n return (\n <div\n {...rest}\n className={className ? `vpg-root ${className}` : \"vpg-root\"}\n data-vpg-mode={colorMode}\n style={{ ...theme, ...style } as CSSProperties}\n >\n {/*\n React 19 hoists and de-duplicates this by `href`, so N providers on a page inject\n one stylesheet. Injecting the CSS as a string rather than importing a `.css` file\n keeps the package free of the import side effect that `\"sideEffects\": false` would\n otherwise have to carve an exception for.\n */}\n <style href=\"vpg-base\" precedence=\"vpg-base\">\n {baseStylesheet}\n </style>\n {children}\n </div>\n );\n}\n"]}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `'light' | 'dark'`, applied as `data-vpg-mode` on `ThemeProvider`'s root.
|
|
3
|
+
* Omitted, the root inherits the host page's `[data-theme]` or `prefers-color-scheme`.
|
|
4
|
+
*/
|
|
5
|
+
export type ColorMode = "light" | "dark";
|
|
6
|
+
/**
|
|
7
|
+
* The small set of user-supplied values `createTheme` expands into a full `Theme`.
|
|
8
|
+
* Every field has a default, so `createTheme()` produces a complete theme.
|
|
9
|
+
*/
|
|
10
|
+
export interface ThemeSeed {
|
|
11
|
+
/** Brand colour every interactive state ramps off. Must be an `oklch()` colour: the
|
|
12
|
+
* ramps are `oklch(from ...)` relative colours, and a non-oklch seed makes the browser
|
|
13
|
+
* convert it first, which loses the chroma headroom the wash and press steps assume. */
|
|
14
|
+
accent?: string;
|
|
15
|
+
/** Status colour every destructive and error state ramps off. Must be an `oklch()` colour,
|
|
16
|
+
* for the same reason the accent must. */
|
|
17
|
+
danger?: string;
|
|
18
|
+
/** Foreground text colour. Also the source of every border and muted-text alpha. */
|
|
19
|
+
ink?: string;
|
|
20
|
+
/** Page background. Raised/sunken surfaces are lightness steps off it. */
|
|
21
|
+
surface?: string;
|
|
22
|
+
/** Base corner radius. `--vpg-radius-sm`/`-lg` are `calc()` multiples of it, at ×0.75
|
|
23
|
+
* and ×1.5, so reseeding it moves the whole ladder together. */
|
|
24
|
+
radius?: string;
|
|
25
|
+
fontSans?: string;
|
|
26
|
+
fontMono?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The frozen set of `--vpg-*` custom properties `ThemeProvider` applies inline to its
|
|
30
|
+
* root element. Values are CSS strings, never JS-computed colours — the browser resolves
|
|
31
|
+
* the ramps at paint time, so a mode flip is a pure-CSS cascade change. Deliberately
|
|
32
|
+
* excludes every stylesheet-owned property: the colours `--vpg-accent`/`--vpg-ink`/
|
|
33
|
+
* `--vpg-surface`/`--vpg-danger` (as opposed to their `-light`/`-dark` variants, which this DOES
|
|
34
|
+
* include), the ramp scalars `--vpg-state-shift`/`--vpg-lift`/`--vpg-sink`, the
|
|
35
|
+
* shadow inks `--vpg-shadow-contact`/`--vpg-shadow-ambient` and the motion durations
|
|
36
|
+
* `--vpg-duration-fast`/`-normal`/`-slow`. The base stylesheet owns every one of them,
|
|
37
|
+
* alongside the `color-scheme` that decides which arm of the colours' and inks' `light-dark()`
|
|
38
|
+
* applies.
|
|
39
|
+
*/
|
|
40
|
+
export type Theme = Readonly<Record<`--vpg-${string}`, string>>;
|
|
41
|
+
/**
|
|
42
|
+
* The properties whose declared value depends on an environment condition only the cascade
|
|
43
|
+
* resolves — the colour mode, the reduced-motion preference. The base stylesheet assigns every
|
|
44
|
+
* one of them on `.vpg-root`, and `createTheme` emits none of them (ADR-0007).
|
|
45
|
+
*/
|
|
46
|
+
export declare const STYLESHEET_OWNED_PROPERTIES: readonly ["--vpg-accent", "--vpg-ink", "--vpg-surface", "--vpg-danger", "--vpg-shadow-contact", "--vpg-shadow-ambient", "--vpg-state-shift", "--vpg-lift", "--vpg-sink", "--vpg-duration-fast", "--vpg-duration-normal", "--vpg-duration-slow"];
|
|
47
|
+
/**
|
|
48
|
+
* A `--vpg-*` property the base stylesheet owns because its value depends on an environment
|
|
49
|
+
* condition the cascade resolves: the colour mode for the colours, inks and ramp scalars, the
|
|
50
|
+
* reduced-motion preference for the durations. Neither `createTheme`'s output nor a
|
|
51
|
+
* `ThemeOverrides` may carry one: both reach the element as an inline style, which no mode rule
|
|
52
|
+
* and no media query can override.
|
|
53
|
+
*/
|
|
54
|
+
export type StylesheetOwnedProperty = (typeof STYLESHEET_OWNED_PROPERTIES)[number];
|
|
55
|
+
/**
|
|
56
|
+
* A partial map of `--vpg-*` properties to CSS strings, composed over the seed-derived
|
|
57
|
+
* result by `createTheme`.
|
|
58
|
+
*
|
|
59
|
+
* Any `--vpg-*` name is accepted, not just the ones `createTheme` emits, so a consumer can
|
|
60
|
+
* carry their own properties on the same root and have them frozen into the same object.
|
|
61
|
+
*
|
|
62
|
+
* The stylesheet-owned properties are excluded: each is typed `never`, so naming one in an object
|
|
63
|
+
* literal is a type error, and `createTheme` throws on one that reaches it through a wider type.
|
|
64
|
+
*/
|
|
65
|
+
export type ThemeOverrides = Readonly<Partial<Record<`--vpg-${string}`, string>> & {
|
|
66
|
+
[K in StylesheetOwnedProperty]?: never;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Expands a seed into a `Theme`.
|
|
70
|
+
*
|
|
71
|
+
* Only `--vpg-*-light`/`-dark` and the radius/font entries carry literal seed values.
|
|
72
|
+
* Every other entry is a CSS expression that reads back through `var()`.
|
|
73
|
+
*
|
|
74
|
+
* The stylesheet-owned properties are deliberately ABSENT from this object: the colours
|
|
75
|
+
* `--vpg-accent`, `--vpg-ink`, `--vpg-surface` and `--vpg-danger`, the ramp scalars
|
|
76
|
+
* `--vpg-state-shift`, `--vpg-lift` and `--vpg-sink`, the shadow inks
|
|
77
|
+
* `--vpg-shadow-contact` and `--vpg-shadow-ambient`, and the motion durations
|
|
78
|
+
* `--vpg-duration-fast`, `--vpg-duration-normal` and `--vpg-duration-slow`. The
|
|
79
|
+
* base stylesheet assigns each of them on `.vpg-root` — the colours and inks as
|
|
80
|
+
* `light-dark(<light>, <dark>)` next to the `color-scheme` that picks the arm, the scalars as
|
|
81
|
+
* their light values, the durations as their full-motion values — and its mode and
|
|
82
|
+
* reduced-motion rules reassign them. `ThemeProvider` applies every key here as an inline
|
|
83
|
+
* style, and an inline style declaration always wins over a stylesheet rule for the same
|
|
84
|
+
* property on the same element — including one inside a media query — so anything inline is
|
|
85
|
+
* beyond the reach of a rule matching that same element. Only the `-light`/`-dark` variants
|
|
86
|
+
* below and the expressions that read a stylesheet-owned property back through `var()` are safe
|
|
87
|
+
* to apply inline (ADR-0007).
|
|
88
|
+
*
|
|
89
|
+
* The seed always describes the light appearance — `-light` variants carry it verbatim, and
|
|
90
|
+
* `-dark` variants derive from it via `oklch(from ...)`. They're kept as separate properties
|
|
91
|
+
* (rather than letting `--vpg-accent-dark` derive from `--vpg-accent`) to break a
|
|
92
|
+
* cycle: `--vpg-accent` is a `light-dark()` over both variants, so a
|
|
93
|
+
* `--vpg-accent-dark` reading `var(--vpg-accent)` back would be self-referential and
|
|
94
|
+
* invalid at computed-value time.
|
|
95
|
+
*
|
|
96
|
+
* `overrides` compose over the derived result, replacing or adding individual `--vpg-*`
|
|
97
|
+
* values without restating a seed. They are subject to the same invariant, and more sharply:
|
|
98
|
+
* everything here lands inline on `.vpg-root`, so an override naming a stylesheet-owned
|
|
99
|
+
* property would shadow the base stylesheet's declaration and pin that property to one colour
|
|
100
|
+
* mode, or to full motion, for the life of the provider. Passing one throws. The route to a
|
|
101
|
+
* different value is a stylesheet rule of the consumer's own, at ordinary specificity, which the
|
|
102
|
+
* mode and reduced-motion rules can still beat where they should.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createTheme(seed?: ThemeSeed, overrides?: ThemeOverrides): Theme;
|
|
105
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEzC;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB;;4FAEwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;8CAC0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;oEACgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,YACtC,cAAc,EACd,WAAW,EACX,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,CACb,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnF;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CACnC,OAAO,CAAC,MAAM,CAAC,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG;KAC1C,CAAC,IAAI,uBAAuB,CAAC,CAAC,EAAE,KAAK;CACvC,CACF,CAAC;AAYF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAc,EAAE,SAAS,GAAE,cAAmB,GAAG,KAAK,CA+KvF"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vipengele/react-tokens",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "vipengele design tokens — seed-and-derive theming (createTheme) and the scoped ThemeProvider that applies a theme as --vpg-* CSS custom properties",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"provenance": true
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/vipengele/react.git",
|
|
15
|
+
"directory": "source/react-ui/packages/tokens"
|
|
16
|
+
},
|
|
7
17
|
"homepage": "https://vipengele.github.io/react/react-ui/",
|
|
8
|
-
"
|
|
9
|
-
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": "^19",
|
|
32
|
+
"react-dom": "^19"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.0.0",
|
|
36
|
+
"@types/react-dom": "^19.0.0",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
42
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"test": "vitest run --coverage"
|
|
44
|
+
}
|
|
45
|
+
}
|