@phreshos/react-ui 0.2.0 → 0.2.2

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/dist/dialog.js CHANGED
@@ -11,7 +11,7 @@ import { scale } from "./foundation/scale.js";
11
11
  import { containerPadding } from "./foundation/spacing.js";
12
12
  import { useVisual } from "./foundation/visual.js";
13
13
  import { floatingShadow } from "./surface/shadow-options.js";
14
- import { Surface } from "./surface/surface.js";
14
+ import { FloatingLayer, Surface } from "./surface/surface.js";
15
15
  export function DialogRoot({ children, ...state }) {
16
16
  return _jsx(AriaDialogTrigger, { ...ariaOpenState(state), children: children });
17
17
  }
@@ -63,24 +63,24 @@ export const DialogContent = forwardRef(function DialogContent({ children, class
63
63
  maxWidth: maxWidth ?? `calc(100vw - ${inset * 2}px)`,
64
64
  maxHeight: `calc(100vh - ${inset * 2}px)`,
65
65
  outline: "none"
66
- }, children: _jsx(Surface, { className: className, color: color, material: material,
67
- // Dialog content is composed by its consumer, so no owned rounded shape
68
- // sits at a known inset; it keeps the plain Appearance radius like Popover.
69
- radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
70
- boxSizing: "border-box",
71
- width: "100%",
72
- maxHeight: "inherit",
73
- padding,
74
- overflow: "hidden",
75
- ...surfaceStyle
76
- }, children: _jsx(AriaDialog, { ...properties, ref: ref, style: {
77
- display: "grid",
78
- gap: inset,
79
- minWidth: 0,
80
- minHeight: 0,
66
+ }, children: _jsx(FloatingLayer, { children: _jsx(Surface, { className: className, color: color, material: material,
67
+ // Dialog content is composed by its consumer, so no owned rounded shape
68
+ // sits at a known inset; it keeps the plain Appearance radius like Popover.
69
+ radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
70
+ boxSizing: "border-box",
71
+ width: "100%",
81
72
  maxHeight: "inherit",
82
- outline: "none"
83
- }, children: children }) }) })] });
73
+ padding,
74
+ overflow: "hidden",
75
+ ...surfaceStyle
76
+ }, children: _jsx(AriaDialog, { ...properties, ref: ref, style: {
77
+ display: "grid",
78
+ gap: inset,
79
+ minWidth: 0,
80
+ minHeight: 0,
81
+ maxHeight: "inherit",
82
+ outline: "none"
83
+ }, children: children }) }) }) })] });
84
84
  });
85
85
  export const DialogHeader = forwardRef(function DialogHeader({ style, ...properties }, ref) {
86
86
  const gap = scale(useVisual().spacing, "small");
@@ -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;
package/dist/panel.js CHANGED
@@ -6,7 +6,7 @@ import { resolveRadius } from "./foundation/radius.js";
6
6
  import { scale } from "./foundation/scale.js";
7
7
  import { useVisual } from "./foundation/visual.js";
8
8
  import { paintClass, usePaintClasses } from "./surface/paint-class.js";
9
- import { Surface } from "./surface/surface.js";
9
+ import { FloatingLayer, Surface } from "./surface/surface.js";
10
10
  import { containerPadding } from "./foundation/spacing.js";
11
11
  const PanelContext = createContext({ color: undefined, hasHeader: false, radius: undefined });
12
12
  export const PanelHeader = forwardRef(function PanelHeader({ style, ...properties }, ref) {
@@ -29,14 +29,14 @@ export const PanelRoot = forwardRef(function Panel({ children, style, ...propert
29
29
  const hasHeader = Children.toArray(children).some(child => isValidElement(child) && child.type === PanelHeader);
30
30
  // The shell and its content share one radius: the Appearance radius.
31
31
  const radius = resolveRadius(properties.radius ?? "medium", useVisual().radius);
32
- return _jsx(PanelContext.Provider, { value: { color: properties.color, hasHeader, radius }, children: _jsx(Surface, { material: "full", ...properties, ref: ref, radius: radius, style: {
33
- display: "grid",
34
- gridTemplateRows: hasHeader ? "auto minmax(0, 1fr)" : "minmax(0, 1fr)",
35
- minWidth: 0,
36
- minHeight: 0,
37
- maxHeight: "inherit",
38
- ...style
39
- }, children: children }) });
32
+ return _jsx(PanelContext.Provider, { value: { color: properties.color, hasHeader, radius }, children: _jsx(FloatingLayer, { children: _jsx(Surface, { material: "full", ...properties, ref: ref, radius: radius, style: {
33
+ display: "grid",
34
+ gridTemplateRows: hasHeader ? "auto minmax(0, 1fr)" : "minmax(0, 1fr)",
35
+ minWidth: 0,
36
+ minHeight: 0,
37
+ maxHeight: "inherit",
38
+ ...style
39
+ }, children: children }) }) });
40
40
  });
41
41
  export const PanelContent = forwardRef(function PanelContent({ className, style, ...properties }, ref) {
42
42
  const { spacing } = useVisual();
package/dist/popover.js CHANGED
@@ -10,7 +10,7 @@ import { resolveDirectionalPlacement } from "./foundation/overlay-placement.js";
10
10
  import { scale } from "./foundation/scale.js";
11
11
  import { useVisual } from "./foundation/visual.js";
12
12
  import { floatingShadow } from "./surface/shadow-options.js";
13
- import { Surface } from "./surface/surface.js";
13
+ import { FloatingLayer, Surface } from "./surface/surface.js";
14
14
  export function PopoverRoot({ children, ...state }) {
15
15
  return _jsx(AriaDialogTrigger, { ...ariaOpenState(state), children: children });
16
16
  }
@@ -22,13 +22,13 @@ const PositionedContent = forwardRef(function PositionedContent({ children, clas
22
22
  const inset = scale(visual.spacing, "small");
23
23
  const transition = overlayTransition(visual);
24
24
  const direction = resolveDirection(dir, useDirection());
25
- return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaPopover, { ...attributes, isNonModal: nonModal, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), UNSTABLE_portalContainer: portalContainer, className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, shadow: shadow ?? floatingShadow(visual.shadow), radius: radius, style: {
26
- boxSizing: "border-box",
27
- maxWidth: `calc(100vw - ${inset * 2}px)`,
28
- maxHeight: `calc(100vh - ${inset * 2}px)`,
29
- outline: "none",
30
- ...style
31
- }, children: children }) })] });
25
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaPopover, { ...attributes, isNonModal: nonModal, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), UNSTABLE_portalContainer: portalContainer, className: overlayMotionClass, style: transition, children: _jsx(FloatingLayer, { children: _jsx(Surface, { dir: direction, className: className, color: color, material: material, shadow: shadow ?? floatingShadow(visual.shadow), radius: radius, style: {
26
+ boxSizing: "border-box",
27
+ maxWidth: `calc(100vw - ${inset * 2}px)`,
28
+ maxHeight: `calc(100vh - ${inset * 2}px)`,
29
+ outline: "none",
30
+ ...style
31
+ }, children: children }) }) })] });
32
32
  });
33
33
  export const PopoverContent = forwardRef(function PopoverContent(properties, ref) {
34
34
  return _jsx(PositionedContent, { ...properties, ref: ref });
@@ -47,6 +47,18 @@ export interface SurfaceInteraction {
47
47
  /** Marks a value-holding Surface whose current value is rejected. */
48
48
  readonly invalid?: boolean;
49
49
  }
50
+ /**
51
+ * A layer that floats above everything, such as a popover, a dialog, or a panel: it is not inside the
52
+ * Surface it was opened from, so what it holds has nothing around it to keep apart from.
53
+ */
54
+ export declare function FloatingLayer({ children }: Readonly<{
55
+ children: ReactNode;
56
+ }>): import("react").JSX.Element;
57
+ /**
58
+ * How far a nested Surface keeps from the one it sits on: farther on a dark canvas, where
59
+ * differences read smaller.
60
+ */
61
+ export declare function nestingDistance(darkness: number): number;
50
62
  /**
51
63
  * The single visual primitive. Components express what they are through
52
64
  * color, depth, and interaction; paint, grain, frost, edge, shadow, focus,
@@ -81,8 +93,10 @@ export declare function surfaceColor(color: Color, depth: SurfaceDepth, colors:
81
93
  /** The color a Surface of this color and depth is painted with at rest, in the active Appearance. */
82
94
  export declare function useSurfaceColor(color?: Color, depth?: SurfaceDepth): string;
83
95
  /** 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): {
96
+ export declare function surfacePaint(visual: Visual, color: Color, depth: SurfaceDepth, options: MaterialMode | MaterialOptions | undefined, shadowOptions: ShadowOverrides["shadow"], interaction: SurfaceInteraction | undefined, surroundings?: string | null): {
85
97
  fill: string;
98
+ /** The paint at rest, or `null` for a clear Surface. */
99
+ rest: string | null;
86
100
  text: string;
87
101
  ring: string;
88
102
  shadow: string;
@@ -1,12 +1,28 @@
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
+ * A layer that floats above everything, such as a popover, a dialog, or a panel: it is not inside the
14
+ * Surface it was opened from, so what it holds has nothing around it to keep apart from.
15
+ */
16
+ export function FloatingLayer({ children }) {
17
+ return _jsx(SurroundingPaint.Provider, { value: null, children: children });
18
+ }
19
+ /**
20
+ * How far a nested Surface keeps from the one it sits on: farther on a dark canvas, where
21
+ * differences read smaller.
22
+ */
23
+ export function nestingDistance(darkness) {
24
+ return 0.04 + 0.03 * darkness;
25
+ }
10
26
  /**
11
27
  * The single visual primitive. Components express what they are through
12
28
  * color, depth, and interaction; paint, grain, frost, edge, shadow, focus,
@@ -15,7 +31,8 @@ import { resolveShadowOptions, shadowStyle } from "./shadow-options.js";
15
31
  export const SurfaceView = forwardRef(function Surface({ as: Element = "div", color = "background", depth = "raised", material, shadow, radius, interaction, className, children, style, ...properties }, ref) {
16
32
  const visual = useVisual();
17
33
  const identity = useId();
18
- const paint = surfacePaint(visual, color, depth, material, shadow, interaction);
34
+ const surroundings = useContext(SurroundingPaint);
35
+ const paint = surfacePaint(visual, color, depth, material, shadow, interaction, surroundings);
19
36
  const borderRadius = radius === undefined && style?.borderRadius !== undefined
20
37
  ? style.borderRadius
21
38
  : resolveRadius(radius ?? "medium", visual.radius);
@@ -38,7 +55,9 @@ export const SurfaceView = forwardRef(function Surface({ as: Element = "div", co
38
55
  className: [surfaceClass, paint.grain?.name, painted, dimmedClass(interaction?.disabled ?? false), className].filter(Boolean).join(" "),
39
56
  // Without a document (server rendering), the same declarations travel inline.
40
57
  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);
58
+ }, _jsx(SurfaceStyle, {}), paint.grain && _jsx(GrainStyle, { grain: paint.grain }), paint.distortion > 0 && _jsx(Refraction, { identity: identity, distortion: paint.distortion }),
59
+ // A clear Surface paints nothing, so what is inside it sits on what is around it.
60
+ paint.rest === null ? children : _jsx(SurroundingPaint.Provider, { value: paint.rest, children: children }));
42
61
  });
43
62
  function inlineDeclarations(declarations) {
44
63
  const result = {};
@@ -85,7 +104,7 @@ export function useSurfaceColor(color = "background", depth = "raised") {
85
104
  return surfaceColor(color, depth, colors);
86
105
  }
87
106
  /** Resolves every visual value of one Surface. Pure over its inputs. */
88
- export function surfacePaint(visual, color, depth, options, shadowOptions, interaction) {
107
+ export function surfacePaint(visual, color, depth, options, shadowOptions, interaction, surroundings = null) {
89
108
  const { colors } = visual;
90
109
  const base = resolveColor(color, colors);
91
110
  const active = interaction !== undefined && !interaction.disabled;
@@ -95,12 +114,15 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
95
114
  const darkness = canvasDarkness(colors);
96
115
  // A recess moves away from its surroundings toward whichever extreme has
97
116
  // room, by one constant perceptual difference on any canvas.
98
- const rest = depth === "recessed" ? surfaceColor(color, depth, colors) : base;
117
+ const own = depth === "recessed" ? surfaceColor(color, depth, colors) : base;
99
118
  // A clear Surface, with no depth or a transparent color, has no substance of
100
119
  // its own: it paints nothing at rest, takes the content color of whatever it
101
120
  // sits on, and only reveals interaction as a veil. With no depth, a color
102
121
  // other than the neutral one colors its content instead of a fill.
103
122
  const clear = depth === "none" || base === "transparent";
123
+ // A raised or level Surface nested in another keeps apart from it, however deep the nesting goes.
124
+ // A recess keeps its own constant depth below its color, so a well reads the same wherever it is.
125
+ const rest = clear || depth === "recessed" || surroundings === null ? own : separateColor(own, surroundings, nestingDistance(darkness));
104
126
  const tinted = depth === "none" && base !== "transparent" && base !== colors.default;
105
127
  const fill = clear
106
128
  ? shift > 0 ? colorOpacity(colors.foreground, shift * clearVeil) : "transparent"
@@ -149,6 +171,8 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
149
171
  : shadowStyle(resolveShadowOptions(typeof shadowOptions === "object" ? shadowOptions : {}, shadow));
150
172
  return {
151
173
  fill,
174
+ /** The paint at rest, or `null` for a clear Surface. */
175
+ rest: clear ? null : rest,
152
176
  // The label is chosen once for the resting paint: interaction shades stay
153
177
  // close to it, and re-deciding per state would flip text on near-ties.
154
178
  text: tinted ? base : clear ? "inherit" : readableColor(rest, colors),
package/dist/tooltip.js CHANGED
@@ -11,7 +11,7 @@ import { scale } from "./foundation/scale.js";
11
11
  import { useVisual } from "./foundation/visual.js";
12
12
  import { timing } from "./foundation/timing.js";
13
13
  import { floatingShadow } from "./surface/shadow-options.js";
14
- import { Surface } from "./surface/surface.js";
14
+ import { FloatingLayer, Surface } from "./surface/surface.js";
15
15
  export function TooltipRoot({ children, delay, closeDelay, trigger, disabled, closeOnPress, ...state }) {
16
16
  // A pointer resting on a trigger for four changes in place is asking what it is.
17
17
  const rest = timing("change", { tempo: useVisual().appearance.tempo }).duration * 4;
@@ -25,16 +25,16 @@ export const TooltipContent = forwardRef(function TooltipContent({ children, cla
25
25
  const inset = scale(visual.spacing, "small");
26
26
  const transition = overlayTransition(visual);
27
27
  const direction = resolveDirection(dir, useDirection());
28
- return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaTooltip, { ...attributes, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), UNSTABLE_portalContainer: portalContainer, className: overlayMotionClass, style: transition, children: _jsx(Surface, { dir: direction, className: className, color: color ?? "foreground", material: material, radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
29
- boxSizing: "border-box",
30
- maxWidth: "28em",
31
- paddingBlock: inset,
32
- paddingInline: inset * 1.5,
33
- outline: "none",
34
- fontSize: controlFontSizes.small,
35
- lineHeight: controlLineHeight,
36
- ...style
37
- }, children: children }) })] });
28
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaTooltip, { ...attributes, ref: ref, dir: direction, offset: offset ?? inset, placement: resolveDirectionalPlacement(placement, direction), UNSTABLE_portalContainer: portalContainer, className: overlayMotionClass, style: transition, children: _jsx(FloatingLayer, { children: _jsx(Surface, { dir: direction, className: className, color: color ?? "foreground", material: material, radius: radius, shadow: shadow ?? floatingShadow(visual.shadow), style: {
29
+ boxSizing: "border-box",
30
+ maxWidth: "28em",
31
+ paddingBlock: inset,
32
+ paddingInline: inset * 1.5,
33
+ outline: "none",
34
+ fontSize: controlFontSizes.small,
35
+ lineHeight: controlLineHeight,
36
+ ...style
37
+ }, children: children }) }) })] });
38
38
  });
39
39
  /** A description shown from the focus and hover state of its trigger. */
40
40
  export const Tooltip = Object.assign(TooltipRoot, {
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.2",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,