@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 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. The material uses a deterministic
87
- 64×64 micro-pattern derived from the former shader grain; it creates no canvas,
88
- WebGL context, or shared texture:
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`, and `opacity` resolve from `Theme.surface`, accept
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. Backdrop belongs
104
- to the outer div and emits no filter property when its resolved value is zero.
105
- Animation defaults to zero; only explicitly animated Surfaces join the internal
106
- document clock, while every texture and seed remains local to its own Surface.
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
- <GlassSurface radius="large">...</GlassSurface>
152
- <GlassSurface radius="2rem">...</GlassSurface>
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 {};
@@ -1,22 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useId, useMemo, useRef } from "react";
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 initial = useMemo(() => grainPaths(seed, 0), [seed]);
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: [_jsx("defs", { children: _jsx("pattern", { id: identity, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => {
34
- const channel = Math.round(tone / (toneCount - 1) * 255);
35
- return _jsx("path", { ref: node => { paths.current[tone] = node; }, "data-surface-grain-tone": tone, d: path, fill: `rgb(${channel}, ${channel}, ${channel})`, shapeRendering: "crispEdges" }, tone);
36
- }) }) }), _jsx("rect", { "data-surface-base": "", width: "100%", height: "100%", fill: color }), _jsx("rect", { "data-surface-grain": "", width: "100%", height: "100%", fill: `url(#${identity})`, opacity: grain, shapeRendering: "crispEdges" })] });
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 one independent pure-SVG Surface material. */
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
- /** Contains content above one independent pure-SVG Surface material. */
7
- export const Surface = forwardRef(function Surface({ animation, backdrop, children, color, grain, opacity, style, ...properties }, forwardedRef) {
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 ref = useCallback((node) => {
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 resolvedColor = resolveColor(color, theme.background);
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: ref, style: {
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(SurfaceMaterial, { animation: resolvedAnimation, color: resolvedColor, grain: resolvedGrain, opacity: resolvedOpacity }), children] });
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",
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.7",
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.7",
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",
@@ -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>>;
@@ -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
- }