@phreshos/react-ui 0.2.0 → 0.2.1

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.
@@ -54,6 +54,13 @@ export declare function canvasDarkness(colors: AppearanceColors): number;
54
54
  * Depth therefore reads the same on a light, mid-tone, or dark canvas.
55
55
  */
56
56
  export declare function recessColor(base: string): string;
57
+ /**
58
+ * A paint moved just far enough from the paint it sits on to be told apart: at least `minimum`
59
+ * perceived difference (OKLab), stepping lighter, or darker only when lighter has no room. Raised
60
+ * and level things stand out the same way everywhere, whatever happens to be beneath them. A paint
61
+ * already far enough stays as it is.
62
+ */
63
+ export declare function separateColor(paint: string, surroundings: string, minimum: number): string;
57
64
  /** The lighter Appearance content candidate: the paint of anything lit from above, such as a thumb. */
58
65
  export declare function lightColor(colors: AppearanceColors): string;
59
66
  export declare function isColorLevel(value: string): value is ColorLevel;
@@ -252,6 +252,31 @@ export function recessColor(base) {
252
252
  }
253
253
  /** The constant difference between a recess and the paint it sinks into. */
254
254
  const recessContrast = 1.15;
255
+ const separateCache = new Map();
256
+ /**
257
+ * A paint moved just far enough from the paint it sits on to be told apart: at least `minimum`
258
+ * perceived difference (OKLab), stepping lighter, or darker only when lighter has no room. Raised
259
+ * and level things stand out the same way everywhere, whatever happens to be beneath them. A paint
260
+ * already far enough stays as it is.
261
+ */
262
+ export function separateColor(paint, surroundings, minimum) {
263
+ const key = `${paint}|${surroundings}|${minimum}`;
264
+ const cached = separateCache.get(key);
265
+ if (cached !== undefined)
266
+ return cached;
267
+ const distance = perceivedDistance(paint, surroundings);
268
+ if (distance === null || distance >= minimum)
269
+ return remember(separateCache, key, paint);
270
+ const directions = ["#ffffff", "#000000"];
271
+ for (const toward of directions) {
272
+ for (let amount = 0.01; amount <= 0.6; amount += 0.01) {
273
+ const result = mixColor(paint, toward, Math.round(amount * 1_000) / 1_000);
274
+ if ((perceivedDistance(result, surroundings) ?? 0) >= minimum)
275
+ return remember(separateCache, key, result);
276
+ }
277
+ }
278
+ return remember(separateCache, key, paint);
279
+ }
255
280
  /** The lighter Appearance content candidate: the paint of anything lit from above, such as a thumb. */
256
281
  export function lightColor(colors) {
257
282
  return darkCanvas(colors) ? colors.foreground : colors.default;
@@ -47,6 +47,11 @@ export interface SurfaceInteraction {
47
47
  /** Marks a value-holding Surface whose current value is rejected. */
48
48
  readonly invalid?: boolean;
49
49
  }
50
+ /**
51
+ * How far a nested Surface keeps from the one it sits on: farther on a dark canvas, where
52
+ * differences read smaller.
53
+ */
54
+ export declare function nestingDistance(darkness: number): number;
50
55
  /**
51
56
  * The single visual primitive. Components express what they are through
52
57
  * color, depth, and interaction; paint, grain, frost, edge, shadow, focus,
@@ -81,8 +86,10 @@ export declare function surfaceColor(color: Color, depth: SurfaceDepth, colors:
81
86
  /** The color a Surface of this color and depth is painted with at rest, in the active Appearance. */
82
87
  export declare function useSurfaceColor(color?: Color, depth?: SurfaceDepth): string;
83
88
  /** Resolves every visual value of one Surface. Pure over its inputs. */
84
- export declare function surfacePaint(visual: Visual, color: Color, depth: SurfaceDepth, options: MaterialMode | MaterialOptions | undefined, shadowOptions: ShadowOverrides["shadow"], interaction: SurfaceInteraction | undefined): {
89
+ export declare function surfacePaint(visual: Visual, color: Color, depth: SurfaceDepth, options: MaterialMode | MaterialOptions | undefined, shadowOptions: ShadowOverrides["shadow"], interaction: SurfaceInteraction | undefined, surroundings?: string | null): {
85
90
  fill: string;
91
+ /** The paint at rest, or `null` for a clear Surface. */
92
+ rest: string | null;
86
93
  text: string;
87
94
  ring: string;
88
95
  shadow: string;
@@ -1,12 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createElement, forwardRef, useId, useInsertionEffect } from "react";
3
- import { canvasDarkness, colorOpacity, contrast, mixColor, readableColor, recessColor, resolveColor } from "../foundation/color.js";
2
+ import { createContext, createElement, forwardRef, useContext, useId, useInsertionEffect } from "react";
3
+ import { canvasDarkness, colorOpacity, contrast, mixColor, readableColor, recessColor, resolveColor, separateColor } from "../foundation/color.js";
4
4
  import { resolveRadius } from "../foundation/radius.js";
5
5
  import { useVisual } from "../foundation/visual.js";
6
6
  import { grainSeed, grainSize, grainTexture } from "./grain.js";
7
7
  import { flushPaintRules, paintClass, usePaintClasses } from "./paint-class.js";
8
8
  import { resolveMaterialOptions } from "./material-options.js";
9
9
  import { resolveShadowOptions, shadowStyle } from "./shadow-options.js";
10
+ /** The resting paint of the nearest painted Surface around, which a nested Surface keeps apart from. */
11
+ const SurroundingPaint = createContext(null);
12
+ /**
13
+ * How far a nested Surface keeps from the one it sits on: farther on a dark canvas, where
14
+ * differences read smaller.
15
+ */
16
+ export function nestingDistance(darkness) {
17
+ return 0.04 + 0.03 * darkness;
18
+ }
10
19
  /**
11
20
  * The single visual primitive. Components express what they are through
12
21
  * color, depth, and interaction; paint, grain, frost, edge, shadow, focus,
@@ -15,7 +24,8 @@ import { resolveShadowOptions, shadowStyle } from "./shadow-options.js";
15
24
  export const SurfaceView = forwardRef(function Surface({ as: Element = "div", color = "background", depth = "raised", material, shadow, radius, interaction, className, children, style, ...properties }, ref) {
16
25
  const visual = useVisual();
17
26
  const identity = useId();
18
- const paint = surfacePaint(visual, color, depth, material, shadow, interaction);
27
+ const surroundings = useContext(SurroundingPaint);
28
+ const paint = surfacePaint(visual, color, depth, material, shadow, interaction, surroundings);
19
29
  const borderRadius = radius === undefined && style?.borderRadius !== undefined
20
30
  ? style.borderRadius
21
31
  : resolveRadius(radius ?? "medium", visual.radius);
@@ -38,7 +48,9 @@ export const SurfaceView = forwardRef(function Surface({ as: Element = "div", co
38
48
  className: [surfaceClass, paint.grain?.name, painted, dimmedClass(interaction?.disabled ?? false), className].filter(Boolean).join(" "),
39
49
  // Without a document (server rendering), the same declarations travel inline.
40
50
  style: painted === undefined ? { ...inlineDeclarations(declarations), ...style } : style
41
- }, _jsx(SurfaceStyle, {}), paint.grain && _jsx(GrainStyle, { grain: paint.grain }), paint.distortion > 0 && _jsx(Refraction, { identity: identity, distortion: paint.distortion }), children);
51
+ }, _jsx(SurfaceStyle, {}), paint.grain && _jsx(GrainStyle, { grain: paint.grain }), paint.distortion > 0 && _jsx(Refraction, { identity: identity, distortion: paint.distortion }),
52
+ // A clear Surface paints nothing, so what is inside it sits on what is around it.
53
+ paint.rest === null ? children : _jsx(SurroundingPaint.Provider, { value: paint.rest, children: children }));
42
54
  });
43
55
  function inlineDeclarations(declarations) {
44
56
  const result = {};
@@ -85,7 +97,7 @@ export function useSurfaceColor(color = "background", depth = "raised") {
85
97
  return surfaceColor(color, depth, colors);
86
98
  }
87
99
  /** Resolves every visual value of one Surface. Pure over its inputs. */
88
- export function surfacePaint(visual, color, depth, options, shadowOptions, interaction) {
100
+ export function surfacePaint(visual, color, depth, options, shadowOptions, interaction, surroundings = null) {
89
101
  const { colors } = visual;
90
102
  const base = resolveColor(color, colors);
91
103
  const active = interaction !== undefined && !interaction.disabled;
@@ -95,12 +107,15 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
95
107
  const darkness = canvasDarkness(colors);
96
108
  // A recess moves away from its surroundings toward whichever extreme has
97
109
  // room, by one constant perceptual difference on any canvas.
98
- const rest = depth === "recessed" ? surfaceColor(color, depth, colors) : base;
110
+ const own = depth === "recessed" ? surfaceColor(color, depth, colors) : base;
99
111
  // A clear Surface, with no depth or a transparent color, has no substance of
100
112
  // its own: it paints nothing at rest, takes the content color of whatever it
101
113
  // sits on, and only reveals interaction as a veil. With no depth, a color
102
114
  // other than the neutral one colors its content instead of a fill.
103
115
  const clear = depth === "none" || base === "transparent";
116
+ // A raised or level Surface nested in another keeps apart from it, however deep the nesting goes.
117
+ // A recess keeps its own constant depth below its color, so a well reads the same wherever it is.
118
+ const rest = clear || depth === "recessed" || surroundings === null ? own : separateColor(own, surroundings, nestingDistance(darkness));
104
119
  const tinted = depth === "none" && base !== "transparent" && base !== colors.default;
105
120
  const fill = clear
106
121
  ? shift > 0 ? colorOpacity(colors.foreground, shift * clearVeil) : "transparent"
@@ -149,6 +164,8 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
149
164
  : shadowStyle(resolveShadowOptions(typeof shadowOptions === "object" ? shadowOptions : {}, shadow));
150
165
  return {
151
166
  fill,
167
+ /** The paint at rest, or `null` for a clear Surface. */
168
+ rest: clear ? null : rest,
152
169
  // The label is chosen once for the resting paint: interaction shades stay
153
170
  // close to it, and re-deciding per state would flip text on near-ties.
154
171
  text: tinted ? base : clear ? "inherit" : readableColor(rest, colors),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,