@phreshos/react-ui 0.1.11 → 0.1.13
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/README.md +20 -11
- package/dist/appearance-provider.js +3 -2
- package/dist/document-scrollbars.d.ts +6 -0
- package/dist/document-scrollbars.js +164 -0
- package/dist/main.d.ts +0 -1
- package/dist/main.js +0 -1
- package/dist/surface-material.js +13 -3
- package/dist/surface.js +1 -12
- package/package.json +2 -2
- package/dist/document-color-scheme.d.ts +0 -3
- package/dist/document-color-scheme.js +0 -10
package/README.md
CHANGED
|
@@ -27,21 +27,29 @@ collapses Appearance into a second retained object.
|
|
|
27
27
|
An application using the Client SDK composes the packages explicitly:
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
|
-
import {
|
|
30
|
+
import { useDesktopPreferences, useSystemAppearance } from "@phreshos/react"
|
|
31
31
|
|
|
32
32
|
const appearance = useSystemAppearance()
|
|
33
33
|
const { theme } = useDesktopPreferences()
|
|
34
|
-
useDocumentColorScheme(theme)
|
|
35
34
|
|
|
36
35
|
return <AppearanceProvider appearance={appearance} theme={theme}>
|
|
37
36
|
{children}
|
|
38
37
|
</AppearanceProvider>
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
Document color-scheme negotiation belongs to the System iframe and the Client
|
|
41
|
+
HTML document, not to a visual component or React hook.
|
|
42
|
+
|
|
43
|
+
`AppearanceProvider` also owns the native scrollbars in its document. It adds
|
|
44
|
+
no rendered container: one document stylesheet styles a six-pixel rounded
|
|
45
|
+
thumb from Appearance foreground and radius, leaves the track transparent, and
|
|
46
|
+
gives its container five pixels of transparent padding. With a precise
|
|
47
|
+
pointer, the thumb is transparent outside its scrollable area, uses foreground
|
|
48
|
+
at 10% inside it, and rises to 20% directly under the pointer. Touch documents
|
|
49
|
+
retain the 10% thumb. The standardized scrollbar API has no thumb-hover state,
|
|
50
|
+
so its fallback stops at 10%. Standard and WebKit rules are mutually exclusive
|
|
51
|
+
so the standard thin width cannot override the padded geometry. WebKit hover
|
|
52
|
+
also invalidates the scrollbar style to ensure Safari repaints each state.
|
|
45
53
|
|
|
46
54
|
## Levels
|
|
47
55
|
|
|
@@ -57,14 +65,16 @@ radii require Appearance because their concrete source is `spacing` or
|
|
|
57
65
|
## Surface
|
|
58
66
|
|
|
59
67
|
`Surface` is the shared visual material. It accepts native `div` properties
|
|
60
|
-
plus local overrides for
|
|
68
|
+
plus local overrides for grain, grain amount, backdrop blur, opacity,
|
|
61
69
|
distortion, waves, ripples, saturation, and brightness. Omitted controls derive
|
|
62
70
|
from the resolved Appearance. A zero-valued optional effect is omitted from the
|
|
63
71
|
rendered material so disabled work costs nothing.
|
|
64
72
|
|
|
65
|
-
Each Surface
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
Each Surface returns one plain geometry and content container. Its existing SVG
|
|
74
|
+
material paints both the fill and its material-derived border; no separate
|
|
75
|
+
border element is rendered. Backdrop refraction and frost remain separate
|
|
76
|
+
compositor layers. Radius and foreground resolve from Appearance; elevation
|
|
77
|
+
stays with the surrounding layout.
|
|
68
78
|
|
|
69
79
|
## Components
|
|
70
80
|
|
|
@@ -73,6 +83,5 @@ Appearance; elevation stays with the surrounding layout.
|
|
|
73
83
|
- `Flex` and `Grid`: small layout primitives that preserve native props.
|
|
74
84
|
- `AppearanceProvider`, `useAppearance`, `useTheme`, `useResolveTheme`:
|
|
75
85
|
environment-neutral appearance composition.
|
|
76
|
-
- `useDocumentColorScheme`: explicit browser document integration.
|
|
77
86
|
- `useScale`, `useColor`, `resolveSpacing`, `resolveRadius`: explicit visual
|
|
78
87
|
derivation helpers.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
|
+
import DocumentScrollbars from "./document-scrollbars.js";
|
|
3
4
|
const missing = Symbol("AppearanceProvider");
|
|
4
5
|
const AppearanceContext = createContext(missing);
|
|
5
6
|
const ThemeContext = createContext(missing);
|
|
6
7
|
/** Provides unresolved Appearance and one effective Theme to a React subtree. */
|
|
7
8
|
export function AppearanceProvider({ appearance, children, theme }) {
|
|
8
|
-
return _jsx(AppearanceContext.Provider, { value: appearance, children:
|
|
9
|
+
return _jsx(AppearanceContext.Provider, { value: appearance, children: _jsxs(ThemeContext.Provider, { value: theme, children: [_jsx(DocumentScrollbars, { appearance: appearance, theme: theme }), children] }) });
|
|
9
10
|
}
|
|
10
11
|
/** Returns the complete unresolved Appearance supplied by the nearest provider. */
|
|
11
12
|
export function useAppearance() {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Appearance, Theme } from "@phreshos/core";
|
|
2
|
+
/** Applies one Appearance to the complete owning document without rendering. */
|
|
3
|
+
export default function DocumentScrollbars({ appearance, theme }: Readonly<{
|
|
4
|
+
appearance: Appearance;
|
|
5
|
+
theme: Theme;
|
|
6
|
+
}>): null;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useInsertionEffect, useRef } from "react";
|
|
2
|
+
import { colorOpacity } from "./color.js";
|
|
3
|
+
const properties = {
|
|
4
|
+
thumb: "--phreshos-scrollbar-thumb",
|
|
5
|
+
thumbHover: "--phreshos-scrollbar-thumb-hover",
|
|
6
|
+
size: "--phreshos-scrollbar-size",
|
|
7
|
+
padding: "--phreshos-scrollbar-padding",
|
|
8
|
+
radius: "--phreshos-scrollbar-radius"
|
|
9
|
+
};
|
|
10
|
+
const stylesheet = `
|
|
11
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
12
|
+
* {
|
|
13
|
+
scrollbar-color: var(${properties.thumb}) transparent;
|
|
14
|
+
scrollbar-width: thin;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (hover: hover) and (pointer: fine) {
|
|
18
|
+
* {
|
|
19
|
+
scrollbar-color: transparent transparent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
*:hover {
|
|
23
|
+
scrollbar-color: var(${properties.thumb}) transparent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (forced-colors: active) {
|
|
28
|
+
*,
|
|
29
|
+
*:hover {
|
|
30
|
+
scrollbar-color: auto;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@supports selector(::-webkit-scrollbar) {
|
|
36
|
+
*::-webkit-scrollbar {
|
|
37
|
+
width: var(${properties.size});
|
|
38
|
+
height: var(${properties.size});
|
|
39
|
+
background: transparent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
*::-webkit-scrollbar-track,
|
|
43
|
+
*::-webkit-scrollbar-corner {
|
|
44
|
+
background: transparent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
*::-webkit-scrollbar-button {
|
|
48
|
+
display: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
*::-webkit-scrollbar-thumb {
|
|
52
|
+
border: var(${properties.padding}) solid transparent;
|
|
53
|
+
border-radius: var(${properties.radius});
|
|
54
|
+
background-color: var(${properties.thumb});
|
|
55
|
+
background-clip: padding-box;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@media (hover: hover) and (pointer: fine) {
|
|
59
|
+
*:hover {
|
|
60
|
+
--phreshos-scrollbar-repaint: ;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
*::-webkit-scrollbar-thumb {
|
|
64
|
+
background-color: transparent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
*:hover::-webkit-scrollbar-thumb {
|
|
68
|
+
background-color: var(${properties.thumb});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
72
|
+
background-color: var(${properties.thumbHover});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (forced-colors: active) {
|
|
77
|
+
*::-webkit-scrollbar-thumb,
|
|
78
|
+
*:hover::-webkit-scrollbar-thumb,
|
|
79
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
80
|
+
background-color: ButtonText;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
const documents = new WeakMap();
|
|
86
|
+
/** Applies one Appearance to the complete owning document without rendering. */
|
|
87
|
+
export default function DocumentScrollbars({ appearance, theme }) {
|
|
88
|
+
const identity = useRef(Symbol("AppearanceProvider")).current;
|
|
89
|
+
const foreground = theme === "dark" ? appearance.foreground.dark : appearance.foreground.light;
|
|
90
|
+
const padding = 5;
|
|
91
|
+
const thumbSize = 6;
|
|
92
|
+
const size = thumbSize + padding * 2;
|
|
93
|
+
const values = {
|
|
94
|
+
thumb: colorOpacity(foreground, 0.1),
|
|
95
|
+
thumbHover: colorOpacity(foreground, 0.2),
|
|
96
|
+
size: `${size}px`,
|
|
97
|
+
padding: `${padding}px`,
|
|
98
|
+
radius: `${Math.min(appearance.radius.light, padding + thumbSize / 2)}px`
|
|
99
|
+
};
|
|
100
|
+
useInsertionEffect(function () {
|
|
101
|
+
if (typeof document === "undefined")
|
|
102
|
+
return;
|
|
103
|
+
return register(document, identity, values);
|
|
104
|
+
}, []);
|
|
105
|
+
useInsertionEffect(function () {
|
|
106
|
+
if (typeof document !== "undefined")
|
|
107
|
+
update(document, identity, values);
|
|
108
|
+
}, [values.thumb, values.thumbHover, values.size, values.padding, values.radius]);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function register(owner, identity, values) {
|
|
112
|
+
const state = documents.get(owner) ?? create(owner);
|
|
113
|
+
state.providers.set(identity, values);
|
|
114
|
+
apply(state);
|
|
115
|
+
return function () {
|
|
116
|
+
state.providers.delete(identity);
|
|
117
|
+
if (state.providers.size) {
|
|
118
|
+
apply(state);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
state.style.remove();
|
|
122
|
+
restore(state);
|
|
123
|
+
documents.delete(owner);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function update(owner, identity, values) {
|
|
127
|
+
const state = documents.get(owner);
|
|
128
|
+
if (!state?.providers.has(identity))
|
|
129
|
+
return;
|
|
130
|
+
state.providers.set(identity, values);
|
|
131
|
+
apply(state);
|
|
132
|
+
}
|
|
133
|
+
function create(owner) {
|
|
134
|
+
const root = owner.documentElement;
|
|
135
|
+
const style = owner.createElement("style");
|
|
136
|
+
const original = new Map(Object.values(properties).map(property => [property, {
|
|
137
|
+
value: root.style.getPropertyValue(property),
|
|
138
|
+
priority: root.style.getPropertyPriority(property)
|
|
139
|
+
}]));
|
|
140
|
+
style.dataset.phreshosScrollbars = "";
|
|
141
|
+
style.textContent = stylesheet;
|
|
142
|
+
owner.head.append(style);
|
|
143
|
+
const state = { root, style, original, providers: new Map() };
|
|
144
|
+
documents.set(owner, state);
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
function apply(state) {
|
|
148
|
+
const values = [...state.providers.values()].at(-1);
|
|
149
|
+
if (!values)
|
|
150
|
+
return;
|
|
151
|
+
state.root.style.setProperty(properties.thumb, values.thumb);
|
|
152
|
+
state.root.style.setProperty(properties.thumbHover, values.thumbHover);
|
|
153
|
+
state.root.style.setProperty(properties.size, values.size);
|
|
154
|
+
state.root.style.setProperty(properties.padding, values.padding);
|
|
155
|
+
state.root.style.setProperty(properties.radius, values.radius);
|
|
156
|
+
}
|
|
157
|
+
function restore(state) {
|
|
158
|
+
for (const [property, original] of state.original) {
|
|
159
|
+
if (original.value)
|
|
160
|
+
state.root.style.setProperty(property, original.value, original.priority);
|
|
161
|
+
else
|
|
162
|
+
state.root.style.removeProperty(property);
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
7
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
|
|
8
|
-
export { default as useDocumentColorScheme } from "./document-color-scheme.js";
|
|
9
8
|
export { Flex, type FlexProps } from "./flex.js";
|
|
10
9
|
export { Grid, type GridProps } from "./grid.js";
|
|
11
10
|
export { Surface, type SurfaceProps } from "./surface.js";
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* behavior contract has been established by the package's tests.
|
|
6
6
|
*/
|
|
7
7
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme } from "./appearance-provider.js";
|
|
8
|
-
export { default as useDocumentColorScheme } from "./document-color-scheme.js";
|
|
9
8
|
export { Flex } from "./flex.js";
|
|
10
9
|
export { Grid } from "./grid.js";
|
|
11
10
|
export { Surface } from "./surface.js";
|
package/dist/surface-material.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useMemo } from "react";
|
|
3
|
+
import { scale } from "./scale.js";
|
|
3
4
|
/** The locally owned SVG paint layer inside one Surface. */
|
|
4
5
|
export function SurfaceMaterial({ color, distortion, grain, grainAmount, identity, opacity, ripples, waves }) {
|
|
5
6
|
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
@@ -9,7 +10,7 @@ export function SurfaceMaterial({ color, distortion, grain, grainAmount, identit
|
|
|
9
10
|
const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
10
11
|
if (!hasPaint && !hasDistortion)
|
|
11
12
|
return null;
|
|
12
|
-
return _jsxs("svg", { "data-surface-material": "", "aria-hidden": "true", focusable: "false", style: {
|
|
13
|
+
return _jsxs("svg", { "data-surface-material": "", "data-surface-border": hasPaint ? "" : undefined, "aria-hidden": "true", focusable: "false", style: {
|
|
13
14
|
position: "absolute",
|
|
14
15
|
zIndex: -1,
|
|
15
16
|
inset: 0,
|
|
@@ -18,10 +19,19 @@ export function SurfaceMaterial({ color, distortion, grain, grainAmount, identit
|
|
|
18
19
|
height: "100%",
|
|
19
20
|
overflow: "hidden",
|
|
20
21
|
borderRadius: "inherit",
|
|
22
|
+
borderColor: edgeColor(color, opacity),
|
|
23
|
+
borderStyle: hasPaint ? "solid" : "none",
|
|
24
|
+
borderWidth: hasPaint ? 1 : 0,
|
|
21
25
|
boxSizing: "border-box",
|
|
22
|
-
opacity,
|
|
23
26
|
pointerEvents: "none"
|
|
24
|
-
}, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity, ripples: ripples, seed: seed, waves: waves }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-surface-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsx("rect", { "data-surface-base": "", width: "100%", height: "100%", fill: color }), hasGrain && _jsx("rect", { "data-surface-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] });
|
|
27
|
+
}, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity, ripples: ripples, seed: seed, waves: waves }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-surface-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsxs("g", { "data-surface-paint": "", opacity: opacity, children: [_jsx("rect", { "data-surface-base": "", width: "100%", height: "100%", fill: color }), hasGrain && _jsx("rect", { "data-surface-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] });
|
|
28
|
+
}
|
|
29
|
+
function edgeColor(color, opacity) {
|
|
30
|
+
const edge = `color-mix(in oklch, ${color} 94%, black)`;
|
|
31
|
+
const edgeOpacity = Math.min(1, scale(opacity, "large"));
|
|
32
|
+
if (edgeOpacity === 1)
|
|
33
|
+
return edge;
|
|
34
|
+
return `color-mix(in srgb, ${edge} ${Math.round(edgeOpacity * 10_000) / 100}%, transparent)`;
|
|
25
35
|
}
|
|
26
36
|
function DistortionFilter({ distortion, identity, ripples, seed, waves }) {
|
|
27
37
|
const fields = [
|
package/dist/surface.js
CHANGED
|
@@ -36,19 +36,8 @@ export const Surface = forwardRef(function Surface({ backdrop, brightness, child
|
|
|
36
36
|
borderRadius: radius,
|
|
37
37
|
color: foreground,
|
|
38
38
|
...style
|
|
39
|
-
}, children: [resolved.refracts && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), resolved.frost && _jsx(BackdropLayer, { name: "frost", filter: resolved.frost, zIndex: -2 }), _jsx(
|
|
39
|
+
}, children: [resolved.refracts && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), resolved.frost && _jsx(BackdropLayer, { name: "frost", filter: resolved.frost, zIndex: -2 }), _jsx(SurfaceMaterial, { identity: identity, ...resolved.material }), children] });
|
|
40
40
|
});
|
|
41
|
-
/** Draws one uniform inset edge from the same color as the Surface material. */
|
|
42
|
-
function SurfaceBorder({ color, opacity }) {
|
|
43
|
-
const edge = `color-mix(in oklch, ${color} 94%, black)`;
|
|
44
|
-
return _jsx("div", { "data-surface-border": "", "aria-hidden": "true", style: {
|
|
45
|
-
...layerStyle,
|
|
46
|
-
zIndex: 0,
|
|
47
|
-
boxSizing: "border-box",
|
|
48
|
-
boxShadow: `inset 0 0 0 1px ${edge}`,
|
|
49
|
-
opacity
|
|
50
|
-
} });
|
|
51
|
-
}
|
|
52
41
|
/** Keeps refraction and native frost in independent compositor passes. */
|
|
53
42
|
function BackdropLayer({ filter, name, zIndex }) {
|
|
54
43
|
return _jsx("div", { "data-surface-backdrop": name, "aria-hidden": "true", style: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@base-ui/react": "^1.7.0",
|
|
64
|
-
"@phreshos/core": "^0.1.
|
|
64
|
+
"@phreshos/core": "^0.1.28",
|
|
65
65
|
"@testing-library/dom": "^10.4.1",
|
|
66
66
|
"@testing-library/react": "^16.3.0",
|
|
67
67
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { useLayoutEffect } from "react";
|
|
2
|
-
/** Keeps the document root's native color scheme aligned with one effective Theme. */
|
|
3
|
-
export default function useDocumentColorScheme(theme) {
|
|
4
|
-
useLayoutEffect(() => {
|
|
5
|
-
const root = document.documentElement;
|
|
6
|
-
const previous = root.style.colorScheme;
|
|
7
|
-
root.style.colorScheme = theme;
|
|
8
|
-
return () => { root.style.colorScheme = previous; };
|
|
9
|
-
}, [theme]);
|
|
10
|
-
}
|