@phreshos/react-ui 0.1.3 → 0.1.5
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 -32
- package/dist/main.d.ts +0 -1
- package/dist/main.js +0 -1
- package/dist/surface-material.d.ts +6 -1
- package/dist/surface-material.js +39 -13
- package/dist/surface.d.ts +25 -1
- package/dist/surface.js +69 -17
- package/package.json +3 -3
- package/dist/glass-surface.d.ts +0 -34
- package/dist/glass-surface.js +0 -40
package/README.md
CHANGED
|
@@ -83,9 +83,10 @@ Explicit values such as `4rem` are used directly — passing them through a
|
|
|
83
83
|
Theme hook would perform no additional work.
|
|
84
84
|
|
|
85
85
|
`Surface` keeps a native `<div>` as its public container and gives every
|
|
86
|
-
instance one locally owned pure-SVG material
|
|
87
|
-
64×64 micro-pattern derived
|
|
88
|
-
WebGL context, or shared
|
|
86
|
+
instance one locally owned pure-SVG material plus only the backdrop layers its
|
|
87
|
+
settings enable. The material uses a deterministic 64×64 micro-pattern derived
|
|
88
|
+
from the former shader grain; it creates no canvas, WebGL context, or shared
|
|
89
|
+
texture:
|
|
89
90
|
|
|
90
91
|
```tsx
|
|
91
92
|
<Surface className="grid rounded-xl shadow-lg">
|
|
@@ -94,16 +95,25 @@ WebGL context, or shared texture:
|
|
|
94
95
|
|
|
95
96
|
<Surface color="strong" grain="large">...</Surface>
|
|
96
97
|
<Surface color="#101114" grain={0.2} animation={8} backdrop={4} opacity={0.9}>...</Surface>
|
|
98
|
+
<Surface distortion={70} waves={8} ripples={4} saturation={1.4} brightness={1.04}>...</Surface>
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
`color` resolves from `Theme.background`; its semantic levels derive from that
|
|
100
102
|
same source and a direct color remains an explicit local override. `grain`,
|
|
101
|
-
`animation`, `backdrop`,
|
|
103
|
+
`grainAmount`, `animation`, `backdrop`, `opacity`, `distortion`, `waves`,
|
|
104
|
+
`ripples`, `saturation`, and `brightness` resolve from `Theme.surface`, accept
|
|
102
105
|
their semantic levels or direct values, and remain bounded by Core's Theme
|
|
103
|
-
limits. Radius and foreground remain ordinary Theme styles.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
limits. Radius and foreground remain ordinary Theme styles. Grain intensity
|
|
107
|
+
controls tonal distance while grain amount controls retained cell density.
|
|
108
|
+
When either is zero, Surface creates no grain pattern or paths. Refraction and
|
|
109
|
+
native frost use independent backdrop layers so blur does not soften the
|
|
110
|
+
displaced image. Enabled organic, wave, and ripple fields are combined
|
|
111
|
+
mathematically before one displacement pass; each zero-valued field is absent,
|
|
112
|
+
and the filter and refraction layer are absent when all three are zero.
|
|
113
|
+
Backdrop blur emits no CSS function at zero. Neutral saturation and brightness
|
|
114
|
+
at one are also omitted. Animation defaults to zero; only a Surface with
|
|
115
|
+
visible grain and a positive rate joins the internal document clock, while
|
|
116
|
+
every texture and seed remains local to its own Surface.
|
|
107
117
|
|
|
108
118
|
The Theme stores unrestricted CSS background, foreground, and accent sources.
|
|
109
119
|
Core derives the fixed `subtle`, `soft`, `base`, `strong`, and `intense`
|
|
@@ -120,36 +130,14 @@ const colors = useColor(theme.accent)
|
|
|
120
130
|
<strong style={{ color: colors.strong }} />
|
|
121
131
|
```
|
|
122
132
|
|
|
123
|
-
`GlassSurface` is the shared translucent material. The Theme supplies its
|
|
124
|
-
background, foreground, and concrete default values for distortion, blur,
|
|
125
|
-
saturation, brightness, and material opacity. The component derives its tint
|
|
126
|
-
from the background and applies the foreground to its content, while accent
|
|
127
|
-
remains independent for emphasis and interaction. The surface may also derive
|
|
128
|
-
a small or large treatment from each numeric default without turning those
|
|
129
|
-
levels into system state.
|
|
130
|
-
|
|
131
|
-
Derived opacity is capped at thirty percent and never fades the surface's
|
|
132
|
-
content. Layout, spacing, radius, and external elevation remain ordinary
|
|
133
|
-
container concerns:
|
|
134
|
-
|
|
135
|
-
```tsx
|
|
136
|
-
<GlassSurface className="rounded-xl p-3">
|
|
137
|
-
...
|
|
138
|
-
</GlassSurface>
|
|
139
|
-
|
|
140
|
-
<GlassSurface color="soft" distortion="large" blur="small" opacity="medium">
|
|
141
|
-
...
|
|
142
|
-
</GlassSurface>
|
|
143
|
-
```
|
|
144
|
-
|
|
145
133
|
Shape-owning components accept the shared `Radius` value directly. Semantic
|
|
146
134
|
levels are derived from the Theme's concrete radius through the same
|
|
147
135
|
`scale()` rule used for spacing, while numbers and CSS values remain explicit
|
|
148
136
|
overrides:
|
|
149
137
|
|
|
150
138
|
```tsx
|
|
151
|
-
<
|
|
152
|
-
<
|
|
139
|
+
<Button radius="large">...</Button>
|
|
140
|
+
<Button radius="2rem">...</Button>
|
|
153
141
|
```
|
|
154
142
|
|
|
155
143
|
Structures whose native element owns the shape derive from the explicit
|
package/dist/main.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export { ThemeProvider, useTheme, type ThemeProviderProps } from "./theme-provid
|
|
|
8
8
|
export { Flex, type FlexProps } from "./flex.js";
|
|
9
9
|
export { Grid, type GridProps } from "./grid.js";
|
|
10
10
|
export { Surface, type SurfaceColor, type SurfaceProps } from "./surface.js";
|
|
11
|
-
export { GlassSurface, type GlassSurfaceProps } from "./glass-surface.js";
|
|
12
11
|
export { Button, type ButtonProps } from "./button.js";
|
|
13
12
|
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
14
13
|
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
package/dist/main.js
CHANGED
|
@@ -8,7 +8,6 @@ export { ThemeProvider, useTheme } from "./theme-provider.js";
|
|
|
8
8
|
export { Flex } from "./flex.js";
|
|
9
9
|
export { Grid } from "./grid.js";
|
|
10
10
|
export { Surface } from "./surface.js";
|
|
11
|
-
export { GlassSurface } from "./glass-surface.js";
|
|
12
11
|
export { Button } from "./button.js";
|
|
13
12
|
export { resolveRadius } from "./radius.js";
|
|
14
13
|
export { resolveSpacing } from "./spacing.js";
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
interface SurfaceMaterialProps {
|
|
2
2
|
readonly animation: number;
|
|
3
3
|
readonly color: string;
|
|
4
|
+
readonly distortion: number;
|
|
4
5
|
readonly grain: number;
|
|
6
|
+
readonly grainAmount: number;
|
|
7
|
+
readonly identity: string;
|
|
5
8
|
readonly opacity: number;
|
|
9
|
+
readonly ripples: number;
|
|
10
|
+
readonly waves: number;
|
|
6
11
|
}
|
|
7
12
|
/** The locally owned SVG paint layer inside one Surface. */
|
|
8
|
-
export declare function SurfaceMaterial({ animation, color, grain, opacity }: SurfaceMaterialProps): import("react").JSX.Element;
|
|
13
|
+
export declare function SurfaceMaterial({ animation, color, distortion, grain, grainAmount, identity, opacity, ripples, waves }: SurfaceMaterialProps): import("react").JSX.Element | null;
|
|
9
14
|
export {};
|
package/dist/surface-material.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Fragment, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
/** The locally owned SVG paint layer inside one Surface. */
|
|
4
|
-
export function SurfaceMaterial({ animation, color, grain, opacity }) {
|
|
5
|
-
const identity = `phresh-surface-${useId().replaceAll(":", "")}`;
|
|
4
|
+
export function SurfaceMaterial({ animation, color, distortion, grain, grainAmount, identity, opacity, ripples, waves }) {
|
|
6
5
|
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
7
|
-
const
|
|
6
|
+
const hasPaint = opacity > 0;
|
|
7
|
+
const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
|
|
8
|
+
const hasDistortion = distortion > 0 || waves > 0 || ripples > 0;
|
|
9
|
+
const initial = useMemo(() => hasGrain ? grainPaths(seed, 0, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
8
10
|
const svg = useRef(null);
|
|
9
11
|
const paths = useRef([]);
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
paths.current.forEach((path, tone) => path?.setAttribute("d", initial[tone] ?? ""));
|
|
12
|
-
if (animation === 0 || !svg.current)
|
|
14
|
+
if (animation === 0 || !hasGrain || !svg.current)
|
|
13
15
|
return;
|
|
14
16
|
const paint = (frame) => {
|
|
15
|
-
const values = grainPaths(seed, frame);
|
|
17
|
+
const values = grainPaths(seed, frame, grainAmount);
|
|
16
18
|
paths.current.forEach((path, tone) => path?.setAttribute("d", values[tone] ?? ""));
|
|
17
19
|
};
|
|
18
20
|
return animate(svg.current, animation, paint);
|
|
19
|
-
}, [animation, initial, seed]);
|
|
21
|
+
}, [animation, grainAmount, hasGrain, initial, seed]);
|
|
22
|
+
if (!hasPaint && !hasDistortion)
|
|
23
|
+
return null;
|
|
20
24
|
return _jsxs("svg", { ref: svg, "data-surface-material": "", "aria-hidden": "true", focusable: "false", style: {
|
|
21
25
|
position: "absolute",
|
|
22
26
|
zIndex: -1,
|
|
@@ -26,14 +30,33 @@ export function SurfaceMaterial({ animation, color, grain, opacity }) {
|
|
|
26
30
|
height: "100%",
|
|
27
31
|
overflow: "hidden",
|
|
28
32
|
borderRadius: "inherit",
|
|
29
|
-
border: "1px solid rgba(15, 17, 21, 0.08)",
|
|
30
33
|
boxSizing: "border-box",
|
|
31
34
|
opacity,
|
|
32
35
|
pointerEvents: "none"
|
|
33
|
-
}, children: [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
}, 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", { ref: node => { paths.current[tone] = node; }, "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" })] });
|
|
37
|
+
}
|
|
38
|
+
function DistortionFilter({ distortion, identity, ripples, seed, waves }) {
|
|
39
|
+
const fields = [
|
|
40
|
+
distortion > 0 && { name: "organic", strength: distortion, x: "R" },
|
|
41
|
+
waves > 0 && { name: "waves", strength: waves, x: "R" },
|
|
42
|
+
ripples > 0 && { name: "ripples", strength: ripples, x: "B" }
|
|
43
|
+
].filter((field) => Boolean(field));
|
|
44
|
+
const scale = fields.reduce((total, field) => total + field.strength, 0);
|
|
45
|
+
const weighted = fields.map(field => `${identity}-${field.name}-weighted`);
|
|
46
|
+
const map = `${identity}-distortion-map`;
|
|
47
|
+
return _jsxs("filter", { id: `${identity}-distortion`, "data-surface-distortion": "", x: "-20%", y: "-20%", width: "140%", height: "140%", colorInterpolationFilters: "sRGB", children: [distortion > 0 && _jsxs(Fragment, { children: [_jsx("feTurbulence", { "data-surface-distortion-noise": "", "data-surface-distortion-field": "organic", type: "fractalNoise", baseFrequency: "0.008 0.008", numOctaves: 2, seed: 92, result: `${identity}-organic-noise` }), _jsx("feGaussianBlur", { in: `${identity}-organic-noise`, stdDeviation: 2, result: `${identity}-organic-noise-blurred` })] }), waves > 0 && _jsx(Fragment, { children: _jsx("feTurbulence", { "data-surface-distortion-field": "waves", type: "turbulence", baseFrequency: "0.006 0.045", numOctaves: 1, seed: seed + 17, result: `${identity}-waves-noise` }) }), ripples > 0 && _jsx(Fragment, { children: _jsx("feTurbulence", { "data-surface-distortion-field": "ripples", type: "turbulence", baseFrequency: "0.055", numOctaves: 2, seed: seed + 31, result: `${identity}-ripples-noise` }) }), fields.map(field => _jsx("feColorMatrix", { "data-surface-distortion-weight": field.name, in: `${identity}-${field.name}-noise${field.name === "organic" ? "-blurred" : ""}`, type: "matrix", values: weightMatrix(field.x, field.strength / scale), result: `${identity}-${field.name}-weighted` }, field.name)), weighted.slice(1).map((field, index) => _jsx("feComposite", { "data-surface-distortion-combine": "", in: index === 0 ? weighted[0] : `${identity}-distortion-sum-${index}`, in2: field, operator: "arithmetic", k2: 1, k3: 1, result: index === weighted.length - 2 ? map : `${identity}-distortion-sum-${index + 1}` }, field)), _jsx("feDisplacementMap", { "data-surface-distortion-stage": "combined", in: "SourceGraphic", in2: weighted.length === 1 ? weighted[0] : map, scale: scale, xChannelSelector: "R", yChannelSelector: "G" })] });
|
|
48
|
+
}
|
|
49
|
+
function weightMatrix(x, weight) {
|
|
50
|
+
const red = x === "R" ? `${weight} 0 0 0 0` : `0 0 ${weight} 0 0`;
|
|
51
|
+
return `${red}
|
|
52
|
+
0 ${weight} 0 0 0
|
|
53
|
+
0 0 0 0 0
|
|
54
|
+
0 0 0 1 0`;
|
|
55
|
+
}
|
|
56
|
+
function grainTone(color, tone, intensity) {
|
|
57
|
+
const channel = Math.round(tone / (toneCount - 1) * 255);
|
|
58
|
+
const percentage = Math.round(intensity * 10_000) / 100;
|
|
59
|
+
return `color-mix(in srgb, ${color} ${100 - percentage}%, rgb(${channel} ${channel} ${channel}) ${percentage}%)`;
|
|
37
60
|
}
|
|
38
61
|
class AnimationClock {
|
|
39
62
|
#entries = new Set();
|
|
@@ -88,7 +111,7 @@ function animate(element, rate, paint) {
|
|
|
88
111
|
}
|
|
89
112
|
return clock.subscribe(rate, paint);
|
|
90
113
|
}
|
|
91
|
-
function grainPaths(seed, frame) {
|
|
114
|
+
function grainPaths(seed, frame, amount) {
|
|
92
115
|
const tones = Array.from({ length: toneCount }, () => []);
|
|
93
116
|
const frameX = frame * 19.17;
|
|
94
117
|
const frameY = frame * 7.31;
|
|
@@ -96,6 +119,9 @@ function grainPaths(seed, frame) {
|
|
|
96
119
|
for (let x = 0; x < patternSize; x += 1) {
|
|
97
120
|
const pointX = x + seed * 41;
|
|
98
121
|
const pointY = y + seed * 17;
|
|
122
|
+
const presence = shaderHash(pointX + frameX + 71.9, pointY + frameY + 13.7);
|
|
123
|
+
if (presence > amount)
|
|
124
|
+
continue;
|
|
99
125
|
const fine = shaderHash(Math.floor(pointX * 1.18) + frameX, Math.floor(pointY * 1.18) + frameY);
|
|
100
126
|
const clustered = shaderHash(Math.floor(pointX * 0.47) + frameX + 31.7, Math.floor(pointY * 0.47) + frameY + 31.7);
|
|
101
127
|
const value = clamp(fine * 0.8 + clustered * 0.2, 0, 1);
|
package/dist/surface.d.ts
CHANGED
|
@@ -8,23 +8,47 @@ export type SurfaceProps = Omit<ComponentPropsWithoutRef<"div">, "color" | "opac
|
|
|
8
8
|
color?: SurfaceColor;
|
|
9
9
|
/** Theme-derived level or direct grain intensity from zero to one. */
|
|
10
10
|
grain?: ScaleLevel | number;
|
|
11
|
+
/** Theme-derived level or direct retained grain amount from zero to one. */
|
|
12
|
+
grainAmount?: ScaleLevel | number;
|
|
11
13
|
/** Theme-derived level or direct grain changes per second from zero to 16. */
|
|
12
14
|
animation?: ScaleLevel | number;
|
|
13
15
|
/** Theme-derived level or direct backdrop blur from zero to 24 CSS pixels. */
|
|
14
16
|
backdrop?: ScaleLevel | number;
|
|
15
17
|
/** Theme-derived level or direct material opacity from zero to one. */
|
|
16
18
|
opacity?: ScaleLevel | number;
|
|
19
|
+
/** Theme-derived level or direct organic displacement from zero to 140 pixels. */
|
|
20
|
+
distortion?: ScaleLevel | number;
|
|
21
|
+
/** Theme-derived level or direct directional displacement from zero to 40 pixels. */
|
|
22
|
+
waves?: ScaleLevel | number;
|
|
23
|
+
/** Theme-derived level or direct ripple displacement from zero to 40 pixels. */
|
|
24
|
+
ripples?: ScaleLevel | number;
|
|
25
|
+
/** Theme-derived level or direct backdrop saturation multiplier. */
|
|
26
|
+
saturation?: ScaleLevel | number;
|
|
27
|
+
/** Theme-derived level or direct backdrop brightness multiplier. */
|
|
28
|
+
brightness?: ScaleLevel | number;
|
|
17
29
|
}>;
|
|
18
|
-
/** Contains content above
|
|
30
|
+
/** Contains content above locally owned Surface material layers. */
|
|
19
31
|
export declare const Surface: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "color" | "opacity"> & Readonly<{
|
|
20
32
|
/** Theme-derived treatment or direct CSS material color. */
|
|
21
33
|
color?: SurfaceColor;
|
|
22
34
|
/** Theme-derived level or direct grain intensity from zero to one. */
|
|
23
35
|
grain?: ScaleLevel | number;
|
|
36
|
+
/** Theme-derived level or direct retained grain amount from zero to one. */
|
|
37
|
+
grainAmount?: ScaleLevel | number;
|
|
24
38
|
/** Theme-derived level or direct grain changes per second from zero to 16. */
|
|
25
39
|
animation?: ScaleLevel | number;
|
|
26
40
|
/** Theme-derived level or direct backdrop blur from zero to 24 CSS pixels. */
|
|
27
41
|
backdrop?: ScaleLevel | number;
|
|
28
42
|
/** Theme-derived level or direct material opacity from zero to one. */
|
|
29
43
|
opacity?: ScaleLevel | number;
|
|
44
|
+
/** Theme-derived level or direct organic displacement from zero to 140 pixels. */
|
|
45
|
+
distortion?: ScaleLevel | number;
|
|
46
|
+
/** Theme-derived level or direct directional displacement from zero to 40 pixels. */
|
|
47
|
+
waves?: ScaleLevel | number;
|
|
48
|
+
/** Theme-derived level or direct ripple displacement from zero to 40 pixels. */
|
|
49
|
+
ripples?: ScaleLevel | number;
|
|
50
|
+
/** Theme-derived level or direct backdrop saturation multiplier. */
|
|
51
|
+
saturation?: ScaleLevel | number;
|
|
52
|
+
/** Theme-derived level or direct backdrop brightness multiplier. */
|
|
53
|
+
brightness?: ScaleLevel | number;
|
|
30
54
|
}> & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/surface.js
CHANGED
|
@@ -1,40 +1,87 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useCallback, useLayoutEffect, useRef } from "react";
|
|
3
|
-
import { color as deriveColor, isScaleLevel, scale, themeLimits } from "@phreshos/core";
|
|
2
|
+
import { forwardRef, useCallback, useId, useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { color as deriveColor, isScaleLevel, scale, scaleMultiplier, themeLimits } from "@phreshos/core";
|
|
4
4
|
import { SurfaceMaterial } from "./surface-material.js";
|
|
5
5
|
import { useTheme } from "./theme-provider.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const layerStyle = {
|
|
7
|
+
position: "absolute",
|
|
8
|
+
inset: 0,
|
|
9
|
+
borderRadius: "inherit",
|
|
10
|
+
pointerEvents: "none"
|
|
11
|
+
};
|
|
12
|
+
const borderOpacity = 0.3;
|
|
13
|
+
/** Contains content above locally owned Surface material layers. */
|
|
14
|
+
export const Surface = forwardRef(function Surface({ animation, backdrop, brightness, children, color, distortion, grain, grainAmount, opacity, ripples, saturation, style, waves, ...properties }, forwardedRef) {
|
|
8
15
|
const theme = useTheme();
|
|
16
|
+
const identity = `phresh-surface-${useId().replaceAll(":", "")}`;
|
|
9
17
|
const element = useRef(null);
|
|
10
|
-
const
|
|
18
|
+
const capture = useCallback((node) => {
|
|
11
19
|
element.current = node;
|
|
12
20
|
if (typeof forwardedRef === "function")
|
|
13
21
|
forwardedRef(node);
|
|
14
22
|
else if (forwardedRef)
|
|
15
23
|
forwardedRef.current = node;
|
|
16
24
|
}, [forwardedRef]);
|
|
17
|
-
const
|
|
18
|
-
const resolvedGrain = resolveScale(grain, theme.surface.grain, themeLimits.surface.grain);
|
|
19
|
-
const resolvedAnimation = resolveScale(animation, theme.surface.animation, themeLimits.surface.animation);
|
|
20
|
-
const resolvedBackdrop = resolveScale(backdrop, theme.surface.backdrop, themeLimits.surface.backdrop);
|
|
21
|
-
const resolvedOpacity = resolveScale(opacity, theme.surface.opacity, themeLimits.surface.opacity);
|
|
22
|
-
const blur = resolvedBackdrop === 0 ? {} : {
|
|
23
|
-
backdropFilter: `blur(${resolvedBackdrop}px)`,
|
|
24
|
-
WebkitBackdropFilter: `blur(${resolvedBackdrop}px)`
|
|
25
|
-
};
|
|
25
|
+
const resolved = resolveSurface({ animation, backdrop, brightness, color, distortion, grain, grainAmount, opacity, ripples, saturation, waves }, theme);
|
|
26
26
|
useLayoutEffect(() => {
|
|
27
27
|
const surface = element.current;
|
|
28
28
|
if (surface)
|
|
29
29
|
return prepareSurfaceLayout(surface);
|
|
30
30
|
});
|
|
31
|
-
return _jsxs("div", { ...properties, ref:
|
|
31
|
+
return _jsxs("div", { ...properties, ref: capture, style: {
|
|
32
32
|
borderRadius: theme.radius,
|
|
33
33
|
color: theme.foreground,
|
|
34
|
-
...blur,
|
|
35
34
|
...style
|
|
36
|
-
}, children: [_jsx(
|
|
35
|
+
}, 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(SurfaceBorder, { color: resolved.material.color }), _jsx(SurfaceMaterial, { identity: identity, ...resolved.material }), children] });
|
|
37
36
|
});
|
|
37
|
+
/** Draws one continuous edge from the same color as the Surface material. */
|
|
38
|
+
function SurfaceBorder({ color }) {
|
|
39
|
+
const highlight = `color-mix(in oklch, ${color} 88%, white)`;
|
|
40
|
+
const shade = `color-mix(in oklch, ${color} 94%, black)`;
|
|
41
|
+
return _jsx("div", { "data-surface-border": "", "aria-hidden": "true", style: {
|
|
42
|
+
...layerStyle,
|
|
43
|
+
zIndex: 0,
|
|
44
|
+
borderStyle: "solid",
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
borderColor: `${highlight} ${shade} ${shade} ${highlight}`,
|
|
47
|
+
boxSizing: "border-box",
|
|
48
|
+
opacity: borderOpacity
|
|
49
|
+
} });
|
|
50
|
+
}
|
|
51
|
+
/** Keeps refraction and native frost in independent compositor passes. */
|
|
52
|
+
function BackdropLayer({ filter, name, zIndex }) {
|
|
53
|
+
return _jsx("div", { "data-surface-backdrop": name, "aria-hidden": "true", style: {
|
|
54
|
+
...layerStyle,
|
|
55
|
+
zIndex,
|
|
56
|
+
backdropFilter: filter,
|
|
57
|
+
WebkitBackdropFilter: filter
|
|
58
|
+
} });
|
|
59
|
+
}
|
|
60
|
+
function resolveSurface(values, theme) {
|
|
61
|
+
const material = {
|
|
62
|
+
animation: resolveScale(values.animation, theme.surface.animation, themeLimits.surface.animation),
|
|
63
|
+
color: resolveColor(values.color, theme.background),
|
|
64
|
+
distortion: resolveScale(values.distortion, theme.surface.distortion, themeLimits.surface.distortion),
|
|
65
|
+
grain: resolveScale(values.grain, theme.surface.grain, themeLimits.surface.grain),
|
|
66
|
+
grainAmount: resolveScale(values.grainAmount, theme.surface.grainAmount, themeLimits.surface.grainAmount),
|
|
67
|
+
opacity: resolveScale(values.opacity, theme.surface.opacity, themeLimits.surface.opacity),
|
|
68
|
+
ripples: resolveScale(values.ripples, theme.surface.ripples, themeLimits.surface.ripples),
|
|
69
|
+
waves: resolveScale(values.waves, theme.surface.waves, themeLimits.surface.waves)
|
|
70
|
+
};
|
|
71
|
+
const backdrop = resolveScale(values.backdrop, theme.surface.backdrop, themeLimits.surface.backdrop);
|
|
72
|
+
const saturation = resolveMultiplier(values.saturation, theme.surface.saturation, themeLimits.surface.saturation);
|
|
73
|
+
const brightness = resolveMultiplier(values.brightness, theme.surface.brightness, themeLimits.surface.brightness);
|
|
74
|
+
const frost = [
|
|
75
|
+
backdrop === 0 ? "" : `blur(${backdrop}px)`,
|
|
76
|
+
saturation === 1 ? "" : `saturate(${saturation})`,
|
|
77
|
+
brightness === 1 ? "" : `brightness(${brightness})`
|
|
78
|
+
].filter(Boolean).join(" ");
|
|
79
|
+
return {
|
|
80
|
+
material,
|
|
81
|
+
frost,
|
|
82
|
+
refracts: material.distortion > 0 || material.waves > 0 || material.ripples > 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
38
85
|
function resolveColor(value, base) {
|
|
39
86
|
if (value === undefined)
|
|
40
87
|
return base;
|
|
@@ -48,6 +95,11 @@ function resolveScale(value, base, range) {
|
|
|
48
95
|
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
49
96
|
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
50
97
|
}
|
|
98
|
+
function resolveMultiplier(value, base, range) {
|
|
99
|
+
const resolved = isScaleLevel(value) ? scaleMultiplier(base, value) : value ?? base;
|
|
100
|
+
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
101
|
+
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
102
|
+
}
|
|
51
103
|
function prepareSurfaceLayout(element) {
|
|
52
104
|
const view = element.ownerDocument.defaultView;
|
|
53
105
|
const computed = view?.getComputedStyle(element);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prepack": "node --run test && node --run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@phreshos/core": "^0.1.
|
|
55
|
+
"@phreshos/core": "^0.1.9",
|
|
56
56
|
"react": "^19.2.0",
|
|
57
57
|
"react-dom": "^19.2.0"
|
|
58
58
|
},
|
|
@@ -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.9",
|
|
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",
|
package/dist/glass-surface.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
-
import { type Colorable, type ColorLevel, type ScaleLevel } from "@phreshos/core";
|
|
3
|
-
import { type RadiusProps } from "./radius.js";
|
|
4
|
-
/** Native properties accepted by the shared glass container. */
|
|
5
|
-
export type GlassSurfaceProps = ComponentPropsWithoutRef<"div"> & Colorable<ColorLevel> & RadiusProps & Readonly<{
|
|
6
|
-
/** Distortion derived from the Theme's concrete default. */
|
|
7
|
-
distortion?: ScaleLevel;
|
|
8
|
-
/** Blur derived from the Theme's concrete default. */
|
|
9
|
-
blur?: ScaleLevel;
|
|
10
|
-
/** Saturation derived from the Theme's concrete default. */
|
|
11
|
-
saturation?: ScaleLevel;
|
|
12
|
-
/** Brightness derived from the Theme's concrete default. */
|
|
13
|
-
brightness?: ScaleLevel;
|
|
14
|
-
/** Material opacity derived from the Theme's concrete default. */
|
|
15
|
-
opacity?: ScaleLevel;
|
|
16
|
-
}>;
|
|
17
|
-
/**
|
|
18
|
-
* Contains content within the shared refracted glass material.
|
|
19
|
-
*
|
|
20
|
-
* Layout, spacing, radius, and external elevation remain the responsibility of
|
|
21
|
-
* the containing interface. Only the material itself is owned here.
|
|
22
|
-
*/
|
|
23
|
-
export declare const GlassSurface: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & Colorable<ColorLevel> & RadiusProps & Readonly<{
|
|
24
|
-
/** Distortion derived from the Theme's concrete default. */
|
|
25
|
-
distortion?: ScaleLevel;
|
|
26
|
-
/** Blur derived from the Theme's concrete default. */
|
|
27
|
-
blur?: ScaleLevel;
|
|
28
|
-
/** Saturation derived from the Theme's concrete default. */
|
|
29
|
-
saturation?: ScaleLevel;
|
|
30
|
-
/** Brightness derived from the Theme's concrete default. */
|
|
31
|
-
brightness?: ScaleLevel;
|
|
32
|
-
/** Material opacity derived from the Theme's concrete default. */
|
|
33
|
-
opacity?: ScaleLevel;
|
|
34
|
-
}> & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/glass-surface.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useId } from "react";
|
|
3
|
-
import { color as deriveColor, scale, scaleMultiplier, themeLimits } from "@phreshos/core";
|
|
4
|
-
import { colorOpacity } from "./color.js";
|
|
5
|
-
import { resolveRadius } from "./radius.js";
|
|
6
|
-
import { useTheme } from "./theme-provider.js";
|
|
7
|
-
const shell = "inset 1px 1px 0 rgba(255, 255, 255, 0.7), inset -1px -1px 0 rgba(255, 255, 255, 0.28)";
|
|
8
|
-
/**
|
|
9
|
-
* Contains content within the shared refracted glass material.
|
|
10
|
-
*
|
|
11
|
-
* Layout, spacing, radius, and external elevation remain the responsibility of
|
|
12
|
-
* the containing interface. Only the material itself is owned here.
|
|
13
|
-
*/
|
|
14
|
-
export const GlassSurface = forwardRef(function GlassSurface({ blur = "medium", brightness = "medium", children, color = "base", distortion = "medium", opacity = "medium", radius, saturation = "medium", style, ...properties }, ref) {
|
|
15
|
-
const theme = useTheme();
|
|
16
|
-
const filter = `phresh-glass-${useId().replaceAll(":", "")}`;
|
|
17
|
-
const backdrop = glassBackdrop(theme.glass, { blur, brightness, saturation });
|
|
18
|
-
const alpha = boundedOpacity(scale(theme.glass.opacity, opacity));
|
|
19
|
-
const tint = deriveColor(theme.background)[color];
|
|
20
|
-
const borderRadius = resolveRadius(radius, theme);
|
|
21
|
-
return _jsxs("div", { ...properties, ref: ref, style: {
|
|
22
|
-
...style,
|
|
23
|
-
...(borderRadius === undefined ? {} : { borderRadius }),
|
|
24
|
-
color: theme.foreground,
|
|
25
|
-
backgroundColor: colorOpacity(tint, alpha),
|
|
26
|
-
backgroundImage: `linear-gradient(to bottom, ${colorOpacity(tint, boundedOpacity(alpha * 5 / 3))}, ${colorOpacity(tint, boundedOpacity(alpha / 2))})`,
|
|
27
|
-
boxShadow: shell,
|
|
28
|
-
backdropFilter: `url(#${filter}) ${backdrop}`,
|
|
29
|
-
WebkitBackdropFilter: backdrop,
|
|
30
|
-
isolation: "isolate",
|
|
31
|
-
overflow: "hidden"
|
|
32
|
-
}, children: [_jsx("svg", { "aria-hidden": "true", width: "0", height: "0", style: { position: "absolute" }, children: _jsxs("filter", { id: filter, x: "0%", y: "0%", width: "100%", height: "100%", children: [_jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "0.008 0.008", numOctaves: "2", seed: "92", result: "noise" }), _jsx("feGaussianBlur", { in: "noise", stdDeviation: "2", result: "blurred" }), _jsx("feDisplacementMap", { in: "SourceGraphic", in2: "blurred", scale: scale(theme.glass.distortion, distortion), xChannelSelector: "R", yChannelSelector: "G" })] }) }), children] });
|
|
33
|
-
});
|
|
34
|
-
function glassBackdrop(glass, levels) {
|
|
35
|
-
return `blur(${scale(glass.blur, levels.blur)}px) saturate(${scaleMultiplier(glass.saturation, levels.saturation)}) brightness(${scaleMultiplier(glass.brightness, levels.brightness)})`;
|
|
36
|
-
}
|
|
37
|
-
/** Keeps every derived tint within the system's maximum glass opacity. */
|
|
38
|
-
function boundedOpacity(value) {
|
|
39
|
-
return Math.round(Math.min(themeLimits.glass.opacity.maximum, Math.max(0, value)) * 10_000) / 10_000;
|
|
40
|
-
}
|