@phreshos/react-ui 0.1.15 → 0.1.16
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 +39 -30
- package/dist/color.d.ts +7 -0
- package/dist/color.js +16 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +1 -0
- package/dist/panel.d.ts +9 -0
- package/dist/panel.js +27 -0
- package/dist/surface-material.d.ts +3 -2
- package/dist/surface-material.js +50 -24
- package/dist/surface.js +2 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Environment-neutral React components and the visual language of PhreshOS.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
[Appearance](https://docs.phreshos.com/system/appearance) ·
|
|
6
|
+
[React SDK](https://docs.phreshos.com/sdks/react) ·
|
|
7
|
+
[Source](https://github.com/PhreshOS/react-ui)
|
|
8
|
+
|
|
9
|
+
## Role
|
|
10
|
+
|
|
11
|
+
React UI interprets Core Appearance and Theme contracts as reusable visual
|
|
12
|
+
primitives. The Desktop and Programs compose those primitives instead of
|
|
13
|
+
reimplementing material, spacing, color, radius, or interaction behavior.
|
|
14
|
+
|
|
15
|
+
The package does not depend on a Client or Server runtime and does not own
|
|
16
|
+
authoritative application state. React owns runtime-neutral state adaptation;
|
|
17
|
+
applications own composition.
|
|
7
18
|
|
|
8
19
|
## Installation
|
|
9
20
|
|
|
@@ -16,15 +27,9 @@ Client or Server runtime and does not own authoritative application state.
|
|
|
16
27
|
|
|
17
28
|
`@phreshos/core`, React, and React DOM are peer dependencies.
|
|
18
29
|
|
|
19
|
-
## Appearance
|
|
20
|
-
|
|
21
30
|
```tsx
|
|
22
31
|
import { standardAppearance } from "@phreshos/core"
|
|
23
|
-
import {
|
|
24
|
-
AppearanceProvider,
|
|
25
|
-
Button,
|
|
26
|
-
Surface,
|
|
27
|
-
} from "@phreshos/react-ui"
|
|
32
|
+
import { AppearanceProvider, Button, Surface } from "@phreshos/react-ui"
|
|
28
33
|
|
|
29
34
|
<AppearanceProvider appearance={standardAppearance} theme="light">
|
|
30
35
|
<Surface>
|
|
@@ -33,26 +38,23 @@ import {
|
|
|
33
38
|
</AppearanceProvider>
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`useTheme()` reads the effective mode, and `useResolveTheme()` resolves one
|
|
39
|
-
themed property where it is consumed.
|
|
41
|
+
See [Appearance](https://docs.phreshos.com/system/appearance) for the contract
|
|
42
|
+
interpreted by the provider and components.
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
`Panel` composes an outer `Surface`, an optional header, and an inset content
|
|
45
|
+
`Surface`. Both materials retain Surface defaults; the content inset follows
|
|
46
|
+
Appearance spacing. Positioning, modality, and lifecycle belong to the caller.
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
The package owns the reusable visual primitives used across the desktop and
|
|
47
|
-
official Programs:
|
|
48
|
+
```tsx
|
|
49
|
+
import { Panel } from "@phreshos/react-ui"
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
<Panel header={<h2>Title</h2>} contentProps={{ style: { padding: 16 } }}>
|
|
52
|
+
Content
|
|
53
|
+
</Panel>
|
|
54
|
+
```
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
Native properties and the forwarded ref target the outer Surface.
|
|
57
|
+
`contentProps` targets the inner Surface; `children` supplies its content.
|
|
56
58
|
|
|
57
59
|
## Development
|
|
58
60
|
|
|
@@ -62,13 +64,20 @@ bun run verify
|
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
`verify` checks the contracts, tests the components, builds the package, and
|
|
65
|
-
validates its
|
|
67
|
+
validates its published shape.
|
|
66
68
|
|
|
67
|
-
##
|
|
69
|
+
## Related repositories
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
- [`@phreshos/core`](https://github.com/PhreshOS/core) owns Appearance, Theme,
|
|
72
|
+
and the shared values interpreted here.
|
|
73
|
+
- [`@phreshos/react`](https://github.com/PhreshOS/react) owns runtime-neutral
|
|
74
|
+
React state adaptation.
|
|
75
|
+
- [PhreshOS System](https://github.com/PhreshOS/system) composes the visual
|
|
76
|
+
language into the Desktop.
|
|
77
|
+
- [Settings](https://github.com/PhreshOS/settings-program) presents owner-facing
|
|
78
|
+
Appearance controls.
|
|
79
|
+
|
|
80
|
+
## Contributing
|
|
72
81
|
|
|
73
82
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository workflow and
|
|
74
83
|
[SECURITY.md](SECURITY.md) for private vulnerability reporting.
|
package/dist/color.d.ts
CHANGED
|
@@ -8,3 +8,10 @@ export declare function color(value: string): ColorScale;
|
|
|
8
8
|
export declare function useColor(value: string): ColorScale;
|
|
9
9
|
/** Applies material opacity without restricting the source CSS color syntax. */
|
|
10
10
|
export declare function colorOpacity(value: string, opacity: number): string;
|
|
11
|
+
/** Reads perceptual lightness from a resolved CSS color. */
|
|
12
|
+
export declare function colorLightness(value: string): number;
|
|
13
|
+
/** Orders resolved CSS colors by perceptual lightness, never by their role. */
|
|
14
|
+
export declare function orderColors(first: string, second: string): {
|
|
15
|
+
lighter: string;
|
|
16
|
+
darker: string;
|
|
17
|
+
};
|
package/dist/color.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { ColorSpace, parse, sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65 } from "colorjs.io/fn";
|
|
3
|
+
// Register the CSS color spaces, without bundling unrelated color-model APIs.
|
|
4
|
+
for (const space of [sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65])
|
|
5
|
+
ColorSpace.register(space);
|
|
2
6
|
/** Derives visual treatments while preserving the concrete value at `base`. */
|
|
3
7
|
export function color(value) {
|
|
4
8
|
return Object.freeze({
|
|
@@ -18,3 +22,15 @@ export function colorOpacity(value, opacity) {
|
|
|
18
22
|
const percentage = Math.round(opacity * 10_000) / 100;
|
|
19
23
|
return `color-mix(in srgb, ${value} ${percentage}%, transparent)`;
|
|
20
24
|
}
|
|
25
|
+
/** Reads perceptual lightness from a resolved CSS color. */
|
|
26
|
+
export function colorLightness(value) {
|
|
27
|
+
// A missing CSS channel (`none`) contributes zero outside interpolation.
|
|
28
|
+
return OKLCH.from(parse(value))[0] ?? 0;
|
|
29
|
+
}
|
|
30
|
+
/** Orders resolved CSS colors by perceptual lightness, never by their role. */
|
|
31
|
+
export function orderColors(first, second) {
|
|
32
|
+
const difference = colorLightness(first) - colorLightness(second);
|
|
33
|
+
return difference > 0 || (difference === 0 && first >= second)
|
|
34
|
+
? { lighter: first, darker: second }
|
|
35
|
+
: { lighter: second, darker: first };
|
|
36
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { AppearanceProvider, useAppearance, useResolveTheme, useTheme, type Appe
|
|
|
8
8
|
export { Flex, type FlexProps } from "./flex.js";
|
|
9
9
|
export { Grid, type GridProps } from "./grid.js";
|
|
10
10
|
export { Surface, type SurfaceProps } from "./surface.js";
|
|
11
|
+
export { Panel, type PanelProps } from "./panel.js";
|
|
11
12
|
export { Button, type ButtonProps } from "./button.js";
|
|
12
13
|
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
13
14
|
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@ export { AppearanceProvider, useAppearance, useResolveTheme, useTheme } from "./
|
|
|
8
8
|
export { Flex } from "./flex.js";
|
|
9
9
|
export { Grid } from "./grid.js";
|
|
10
10
|
export { Surface } from "./surface.js";
|
|
11
|
+
export { Panel } from "./panel.js";
|
|
11
12
|
export { Button } from "./button.js";
|
|
12
13
|
export { resolveRadius } from "./radius.js";
|
|
13
14
|
export { resolveSpacing } from "./spacing.js";
|
package/dist/panel.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SurfaceProps } from "./surface.js";
|
|
3
|
+
/** A Surface with an optional header and an inset content Surface. */
|
|
4
|
+
export interface PanelProps extends SurfaceProps {
|
|
5
|
+
readonly header?: ReactNode;
|
|
6
|
+
readonly contentProps?: Omit<SurfaceProps, "children">;
|
|
7
|
+
}
|
|
8
|
+
/** Owns the shared shell layout, not positioning, interaction, or lifecycle. */
|
|
9
|
+
export declare const Panel: import("react").ForwardRefExoticComponent<PanelProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/panel.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
4
|
+
import { scale } from "./scale.js";
|
|
5
|
+
import { Surface } from "./surface.js";
|
|
6
|
+
/** Owns the shared shell layout, not positioning, interaction, or lifecycle. */
|
|
7
|
+
export const Panel = forwardRef(function Panel({ header, children, contentProps, style, ...properties }, ref) {
|
|
8
|
+
const appearance = useAppearance();
|
|
9
|
+
const inset = scale(useResolveTheme(appearance.spacing), "small");
|
|
10
|
+
const hasHeader = header !== undefined && header !== null && header !== false;
|
|
11
|
+
return _jsxs(Surface, { ...properties, ref: ref, style: {
|
|
12
|
+
display: "grid",
|
|
13
|
+
gridTemplateRows: hasHeader ? "auto minmax(0, 1fr)" : "minmax(0, 1fr)",
|
|
14
|
+
minWidth: 0,
|
|
15
|
+
minHeight: 0,
|
|
16
|
+
maxHeight: "inherit",
|
|
17
|
+
...style
|
|
18
|
+
}, children: [header, _jsx(Surface, { ...contentProps, style: {
|
|
19
|
+
position: "relative",
|
|
20
|
+
minWidth: 0,
|
|
21
|
+
minHeight: 0,
|
|
22
|
+
margin: inset,
|
|
23
|
+
marginTop: hasHeader ? 0 : inset,
|
|
24
|
+
overflow: "hidden",
|
|
25
|
+
...contentProps?.style
|
|
26
|
+
}, children: children })] });
|
|
27
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
interface SurfaceMaterialProps {
|
|
2
2
|
readonly color: string;
|
|
3
3
|
readonly distortion: number;
|
|
4
|
+
readonly foreground: string;
|
|
4
5
|
readonly grain: number;
|
|
5
6
|
readonly grainAmount: number;
|
|
6
7
|
readonly identity: string;
|
|
@@ -8,6 +9,6 @@ interface SurfaceMaterialProps {
|
|
|
8
9
|
readonly ripples: number;
|
|
9
10
|
readonly waves: number;
|
|
10
11
|
}
|
|
11
|
-
/** The locally owned
|
|
12
|
-
export declare function SurfaceMaterial({ color, distortion, grain, grainAmount, identity, opacity, ripples, waves }: SurfaceMaterialProps): import("react").JSX.Element | null;
|
|
12
|
+
/** The locally owned fill, refraction definition, and glass rim inside one Surface. */
|
|
13
|
+
export declare function SurfaceMaterial({ color, distortion, foreground, grain, grainAmount, identity, opacity, ripples, waves }: SurfaceMaterialProps): import("react").JSX.Element | null;
|
|
13
14
|
export {};
|
package/dist/surface-material.js
CHANGED
|
@@ -1,37 +1,63 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useMemo } from "react";
|
|
2
|
+
import { Fragment, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { colorLightness, colorOpacity, orderColors } from "./color.js";
|
|
3
4
|
import { scale } from "./scale.js";
|
|
4
|
-
/** The locally owned
|
|
5
|
-
export function SurfaceMaterial({ color, distortion, grain, grainAmount, identity, opacity, ripples, waves }) {
|
|
5
|
+
/** The locally owned fill, refraction definition, and glass rim inside one Surface. */
|
|
6
|
+
export function SurfaceMaterial({ color, distortion, foreground, grain, grainAmount, identity, opacity, ripples, waves }) {
|
|
7
|
+
const element = useRef(null);
|
|
8
|
+
const [colors, setColors] = useState(null);
|
|
6
9
|
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
7
10
|
const hasPaint = opacity > 0;
|
|
8
11
|
const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
|
|
9
12
|
const hasDistortion = distortion > 0 || waves > 0 || ripples > 0;
|
|
10
13
|
const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
14
|
+
// Resolve variables, currentColor, and CSS expressions in this Surface's
|
|
15
|
+
// own scope before comparing colors. Layout effects settle before paint.
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const material = element.current;
|
|
18
|
+
const view = material?.ownerDocument.defaultView;
|
|
19
|
+
if (!material || !view || !hasPaint)
|
|
20
|
+
return;
|
|
21
|
+
const computed = view.getComputedStyle(material);
|
|
22
|
+
const background = computed.fill;
|
|
23
|
+
const foreground = computed.color;
|
|
24
|
+
if (colors?.background === background && colors.foreground === foreground)
|
|
25
|
+
return;
|
|
26
|
+
setColors({ ...orderColors(background, foreground), background, foreground, lightness: Math.max(0, Math.min(1, colorLightness(background))) });
|
|
27
|
+
});
|
|
11
28
|
if (!hasPaint && !hasDistortion)
|
|
12
29
|
return null;
|
|
13
|
-
return _jsxs(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
return _jsxs(Fragment, { children: [_jsxs("svg", { "data-surface-material": "", "aria-hidden": "true", focusable: "false", style: {
|
|
31
|
+
position: "absolute",
|
|
32
|
+
zIndex: -1,
|
|
33
|
+
inset: 0,
|
|
34
|
+
display: "block",
|
|
35
|
+
width: "100%",
|
|
36
|
+
height: "100%",
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
borderRadius: "inherit",
|
|
39
|
+
boxShadow: hasPaint && colors ? `inset 0 1px 2px ${colorOpacity(colors.lighter, 0.28)}, 0 8px 24px ${colorOpacity(colors.darker, 0.04)}` : undefined,
|
|
40
|
+
pointerEvents: "none"
|
|
41
|
+
}, 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", { ref: element, "data-surface-base": "", width: "100%", height: "100%", style: { fill: color, color: foreground } }), hasGrain && _jsx("rect", { "data-surface-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] }), hasPaint && _jsx("div", { "data-surface-border": "", "aria-hidden": "true", style: {
|
|
42
|
+
position: "absolute",
|
|
43
|
+
// The material belongs behind content; its rim belongs above it.
|
|
44
|
+
// Opaque children must not erase the Surface's rounded edge.
|
|
45
|
+
zIndex: 1,
|
|
46
|
+
inset: 0,
|
|
47
|
+
padding: 1,
|
|
48
|
+
borderRadius: "inherit",
|
|
49
|
+
pointerEvents: "none",
|
|
50
|
+
opacity: Math.min(1, scale(opacity, "xlarge")) * (colors?.lightness ?? 0),
|
|
51
|
+
background: colors ? glassRim(colors.lighter, colors.darker) : undefined,
|
|
52
|
+
WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
|
|
53
|
+
WebkitMaskComposite: "xor",
|
|
54
|
+
mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
|
|
55
|
+
maskComposite: "exclude"
|
|
56
|
+
} })] });
|
|
28
57
|
}
|
|
29
|
-
function
|
|
30
|
-
const edge = `color-mix(in oklch, ${
|
|
31
|
-
|
|
32
|
-
if (edgeOpacity === 1)
|
|
33
|
-
return edge;
|
|
34
|
-
return `color-mix(in srgb, ${edge} ${Math.round(edgeOpacity * 10_000) / 100}%, transparent)`;
|
|
58
|
+
function glassRim(light, dark) {
|
|
59
|
+
const edge = `color-mix(in oklch, ${dark} 20%, ${light})`;
|
|
60
|
+
return `linear-gradient(145deg, ${colorOpacity(light, 0.92)}, ${colorOpacity(light, 0.4)} 35%, ${colorOpacity(edge, 0.18)} 55%, ${colorOpacity(light, 0.6)} 85%, ${colorOpacity(light, 0.3)})`;
|
|
35
61
|
}
|
|
36
62
|
function DistortionFilter({ distortion, identity, ripples, seed, waves }) {
|
|
37
63
|
const fields = [
|
package/dist/surface.js
CHANGED
|
@@ -15,7 +15,7 @@ export const Surface = forwardRef(function Surface({ backdrop, brightness, child
|
|
|
15
15
|
const appearance = useAppearance();
|
|
16
16
|
const background = useResolveTheme(appearance.background);
|
|
17
17
|
const foreground = useResolveTheme(appearance.foreground);
|
|
18
|
-
const radius = useResolveTheme(appearance.radius);
|
|
18
|
+
const radius = scale(useResolveTheme(appearance.radius), "medium");
|
|
19
19
|
const surface = useResolveTheme(appearance.surface);
|
|
20
20
|
const identity = `phresh-surface-${useId().replaceAll(":", "")}`;
|
|
21
21
|
const element = useRef(null);
|
|
@@ -36,7 +36,7 @@ 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(SurfaceMaterial, { identity: identity, ...resolved.material }), children] });
|
|
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, foreground: foreground, ...resolved.material }), children] });
|
|
40
40
|
});
|
|
41
41
|
/** Keeps refraction and native frost in independent compositor passes. */
|
|
42
42
|
function BackdropLayer({ filter, name, zIndex }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -52,16 +52,17 @@
|
|
|
52
52
|
"prepack": "node --run test && node --run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@phreshos/core": "^0.1.
|
|
55
|
+
"@phreshos/core": "^0.1.41",
|
|
56
56
|
"react": "^19.2.0",
|
|
57
57
|
"react-dom": "^19.2.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"colorjs.io": "^0.7.1",
|
|
60
61
|
"react-aria-components": "^1.20.0"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@base-ui/react": "^1.7.0",
|
|
64
|
-
"@phreshos/core": "^0.1.
|
|
65
|
+
"@phreshos/core": "^0.1.41",
|
|
65
66
|
"@testing-library/dom": "^10.4.1",
|
|
66
67
|
"@testing-library/react": "^16.3.0",
|
|
67
68
|
"@testing-library/user-event": "^14.6.1",
|