@phreshos/react-ui 0.1.28 → 0.1.30

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
@@ -1,9 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Heading as AriaHeading, Modal as AriaModal, ModalOverlay as AriaModalOverlay, Text as AriaText } from "react-aria-components";
4
4
  import { useAppearance } from "./ui-provider.js";
5
+ import { useResolvedAppearance } from "./appearance-context.js";
6
+ import { colorOpacity, orderColors } from "./color.js";
7
+ import { controlFontWeight, controlOpacity } from "./control.js";
5
8
  import { Button } from "./button.js";
6
- import { backdropMotionClass, overlayMotionClass, useOverlayTransition } from "./motion-style.js";
9
+ import MotionStyle, { backdropMotionClass, overlayMotionClass, overlayTransition } from "./motion-style.js";
7
10
  import { scale } from "./scale.js";
8
11
  import { Surface } from "./surface.js";
9
12
  import { resolveDirection, useDirection } from "./direction.js";
@@ -14,57 +17,61 @@ export const DialogTrigger = forwardRef(function DialogTrigger(properties, ref)
14
17
  return _jsx(Button, { ...properties, ref: ref });
15
18
  });
16
19
  export const DialogBackdrop = forwardRef(function DialogBackdrop({ className, style, ...properties }, ref) {
17
- const inset = scale(useAppearance().spacing, "medium");
18
- const transition = useOverlayTransition();
20
+ const resolved = useResolvedAppearance();
21
+ const inset = scale(resolved.appearance.spacing, "medium");
22
+ const colors = resolved.colors;
23
+ const backdrop = colorOpacity(orderColors(colors.background, colors.foreground).darker, 0.32);
24
+ const transition = overlayTransition(resolved.transaction, resolved.preferences.animations);
19
25
  const direction = resolveDirection(properties.dir, useDirection());
20
- return _jsx(AriaModalOverlay, { ...properties, ref: ref, dir: direction, className: [backdropMotionClass, className].filter(Boolean).join(" "), style: {
21
- ...transition,
22
- position: "fixed",
23
- inset: 0,
24
- zIndex: 1_000,
25
- display: "grid",
26
- placeItems: "center",
27
- boxSizing: "border-box",
28
- padding: inset,
29
- overflow: "auto",
30
- background: "rgb(0 0 0 / 0.32)",
31
- ...style
32
- } });
26
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaModalOverlay, { ...properties, ref: ref, dir: direction, className: [backdropMotionClass, className].filter(Boolean).join(" "), style: {
27
+ ...transition,
28
+ position: "fixed",
29
+ inset: 0,
30
+ zIndex: 1_000,
31
+ display: "grid",
32
+ placeItems: "center",
33
+ boxSizing: "border-box",
34
+ padding: inset,
35
+ overflow: "auto",
36
+ background: backdrop,
37
+ ...style
38
+ } })] });
33
39
  });
34
40
  export const DialogContent = forwardRef(function DialogContent({ children, className, color, material, radius, shadow, style, ...properties }, ref) {
35
- const inset = scale(useAppearance().spacing, "medium");
36
- const transition = useOverlayTransition();
41
+ const resolved = useResolvedAppearance();
42
+ const inset = scale(resolved.appearance.spacing, "medium");
43
+ const transition = overlayTransition(resolved.transaction, resolved.preferences.animations);
37
44
  const direction = resolveDirection(properties.dir, useDirection());
38
- return _jsx(AriaModal, { dir: direction, className: overlayMotionClass, style: {
39
- ...transition,
40
- width: `min(32rem, calc(100vw - ${inset * 2}px))`,
41
- maxHeight: `calc(100vh - ${inset * 2}px)`,
42
- outline: "none"
43
- }, children: _jsx(Surface, { className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
44
- boxSizing: "border-box",
45
- width: "100%",
46
- maxHeight: "inherit",
47
- padding: inset,
48
- overflow: "hidden",
49
- ...style
50
- }, children: _jsx(AriaDialog, { ...properties, ref: ref, style: {
51
- display: "grid",
52
- gap: inset,
53
- minWidth: 0,
54
- minHeight: 0,
55
- maxHeight: "inherit",
45
+ return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(AriaModal, { dir: direction, className: overlayMotionClass, style: {
46
+ ...transition,
47
+ width: `min(32rem, calc(100vw - ${inset * 2}px))`,
48
+ maxHeight: `calc(100vh - ${inset * 2}px)`,
56
49
  outline: "none"
57
- }, children: children }) }) });
50
+ }, children: _jsx(Surface, { className: className, color: color, material: material, radius: radius, shadow: shadow, style: {
51
+ boxSizing: "border-box",
52
+ width: "100%",
53
+ maxHeight: "inherit",
54
+ padding: inset,
55
+ overflow: "hidden",
56
+ ...style
57
+ }, children: _jsx(AriaDialog, { ...properties, ref: ref, style: {
58
+ display: "grid",
59
+ gap: inset,
60
+ minWidth: 0,
61
+ minHeight: 0,
62
+ maxHeight: "inherit",
63
+ outline: "none"
64
+ }, children: children }) }) })] });
58
65
  });
59
66
  export const DialogHeader = forwardRef(function DialogHeader({ style, ...properties }, ref) {
60
67
  const gap = scale(useAppearance().spacing, "small");
61
68
  return _jsx("div", { ...properties, ref: ref, style: { display: "grid", gap, minWidth: 0, ...style } });
62
69
  });
63
70
  export const DialogTitle = forwardRef(function DialogTitle({ style, ...properties }, ref) {
64
- return _jsx(AriaHeading, { ...properties, ref: ref, slot: "title", style: { margin: 0, font: "inherit", fontWeight: 650, ...style } });
71
+ return _jsx(AriaHeading, { ...properties, ref: ref, slot: "title", style: { margin: 0, font: "inherit", fontWeight: controlFontWeight, ...style } });
65
72
  });
66
73
  export const DialogDescription = forwardRef(function DialogDescription({ style, ...properties }, ref) {
67
- return _jsx(AriaText, { ...properties, ref: ref, slot: "description", style: { margin: 0, opacity: 0.72, ...style } });
74
+ return _jsx(AriaText, { ...properties, ref: ref, slot: "description", style: { margin: 0, opacity: controlOpacity.secondary, ...style } });
68
75
  });
69
76
  export const DialogBody = forwardRef(function DialogBody({ style, ...properties }, ref) {
70
77
  return _jsx("div", { ...properties, ref: ref, style: { minWidth: 0, minHeight: 0, ...style } });
@@ -1,9 +1,17 @@
1
1
  /** The two concrete directions supported by React UI behavior. */
2
2
  export type Direction = "ltr" | "rtl";
3
- export declare const DirectionContext: import("react").Context<Direction | null>;
3
+ interface DirectionSource {
4
+ readonly getSnapshot: () => Direction;
5
+ readonly getServerSnapshot: () => Direction;
6
+ readonly subscribe: (change: () => void) => () => void;
7
+ }
4
8
  /** Resolves a concrete element override before falling back to its React UI environment. */
5
9
  export declare function resolveDirection(value: string | undefined, fallback: Direction): Direction;
6
10
  /** Returns the nearest guaranteed React UI direction, or the document direction. */
7
11
  export declare function useDirection(): Direction;
8
12
  /** Reactively reads the explicit direction of the document's HTML element. */
9
13
  export declare function useDocumentDirection(): Direction;
14
+ /** Creates a stable source for one explicitly established direction boundary. */
15
+ export declare function fixedDirectionSource(direction: Direction): DirectionSource;
16
+ export declare const DirectionContext: import("react").Context<DirectionSource>;
17
+ export {};
package/dist/direction.js CHANGED
@@ -1,25 +1,43 @@
1
1
  import { createContext, useContext, useSyncExternalStore } from "react";
2
- export const DirectionContext = createContext(null);
3
2
  /** Resolves a concrete element override before falling back to its React UI environment. */
4
3
  export function resolveDirection(value, fallback) {
5
4
  return value === "ltr" || value === "rtl" ? value : fallback;
6
5
  }
7
6
  /** Returns the nearest guaranteed React UI direction, or the document direction. */
8
7
  export function useDirection() {
9
- const direction = useContext(DirectionContext);
10
- const documentDirection = useDocumentDirection();
11
- return direction ?? documentDirection;
8
+ const source = useContext(DirectionContext);
9
+ return useSyncExternalStore(source.subscribe, source.getSnapshot, source.getServerSnapshot);
12
10
  }
13
11
  /** Reactively reads the explicit direction of the document's HTML element. */
14
12
  export function useDocumentDirection() {
15
- return useSyncExternalStore(subscribeDocumentDirection, documentDirection, serverDirection);
13
+ return useSyncExternalStore(documentDirectionSource.subscribe, documentDirectionSource.getSnapshot, documentDirectionSource.getServerSnapshot);
16
14
  }
15
+ /** Creates a stable source for one explicitly established direction boundary. */
16
+ export function fixedDirectionSource(direction) {
17
+ return {
18
+ getSnapshot: () => direction,
19
+ getServerSnapshot: () => direction,
20
+ subscribe: () => () => undefined
21
+ };
22
+ }
23
+ const subscribers = new Set();
24
+ let observer;
17
25
  function subscribeDocumentDirection(change) {
18
- if (typeof document === "undefined" || typeof MutationObserver === "undefined")
19
- return () => undefined;
20
- const observer = new MutationObserver(change);
21
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ["dir"] });
22
- return () => observer.disconnect();
26
+ subscribers.add(change);
27
+ if (subscribers.size === 1 && typeof document !== "undefined" && typeof MutationObserver !== "undefined") {
28
+ observer = new MutationObserver(() => {
29
+ for (const subscriber of subscribers)
30
+ subscriber();
31
+ });
32
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["dir"] });
33
+ }
34
+ return () => {
35
+ subscribers.delete(change);
36
+ if (subscribers.size === 0) {
37
+ observer?.disconnect();
38
+ observer = undefined;
39
+ }
40
+ };
23
41
  }
24
42
  function documentDirection() {
25
43
  if (typeof document === "undefined")
@@ -29,3 +47,9 @@ function documentDirection() {
29
47
  function serverDirection() {
30
48
  return "ltr";
31
49
  }
50
+ const documentDirectionSource = {
51
+ getSnapshot: documentDirection,
52
+ getServerSnapshot: serverDirection,
53
+ subscribe: subscribeDocumentDirection
54
+ };
55
+ export const DirectionContext = createContext(documentDirectionSource);
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { controlOpacity } from "./control.js";
2
3
  export const textControlClass = "phreshos-ui-text-control";
3
4
  /** Placeholder contrast follows the control's chosen text color, not browser gray. */
4
5
  export default function FieldStyle() {
5
6
  return _jsx("style", { href: "phreshos-react-ui-fields", precedence: "phreshos", children: `
6
7
  .phreshos-ui-text-control::placeholder {
7
8
  color: inherit;
8
- opacity: 0.6;
9
+ opacity: ${controlOpacity.placeholder};
9
10
  }
10
11
  ` });
11
12
  }
package/dist/main.d.ts CHANGED
@@ -10,9 +10,10 @@ export type { Preferences, Theme } from "./preferences.js";
10
10
  export type { Direction } from "./direction.js";
11
11
  export { Flex, type FlexProps } from "./flex.js";
12
12
  export { Grid, type GridProps } from "./grid.js";
13
- export { Surface, type MaterialOptions, type ShadowOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
13
+ export { Surface, type MaterialMode, type MaterialOptions, type ShadowOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
14
14
  export { defaultColor, type Color, type ColorLevel, type ColorScale } from "./color.js";
15
15
  export { Panel, PanelRoot, PanelHeader, PanelContent, type PanelProps, type PanelRootProps, type PanelHeaderProps, type PanelContentProps } from "./panel.js";
16
+ export { Window, type WindowProps, type WindowContentProps, type WindowHeaderProps, type WindowIdentityProps, type WindowCenterProps, type WindowActionsProps, type WindowActionProps, type WindowControlProps, type WindowMaximizeProps, type WindowCloseProps } from "./window.js";
16
17
  export { Button, type ButtonColor, type ButtonProps } from "./button.js";
17
18
  export { Input, type InputProps } from "./input.js";
18
19
  export { Textarea, type TextareaProps } from "./textarea.js";
@@ -25,7 +26,7 @@ export { ScrollArea, type ScrollAreaAxis, type ScrollAreaProps } from "./scroll-
25
26
  export { Popover, PopoverRoot, PopoverTrigger, PopoverContent, PopoverDialog, PopoverTitle, PopoverClose, type PopoverProps, type PopoverRootProps, type PopoverTriggerProps, type PopoverContentProps, type PopoverDialogProps, type PopoverTitleProps, type PopoverCloseProps } from "./popover.js";
26
27
  export { Menu, MenuRoot, MenuItem, MenuSection, MenuHeader, MenuSeparator, type MenuProps, type MenuRootProps, type MenuItemProps, type MenuSectionProps, type MenuHeaderProps, type MenuSeparatorProps } from "./menu.js";
27
28
  export { DropdownMenu, DropdownMenuRoot, DropdownMenuTrigger, type DropdownMenuProps, type DropdownMenuRootProps, type DropdownMenuTriggerProps } from "./dropdown-menu.js";
28
- export { ContextMenu, ContextMenuRoot, ContextMenuTrigger, type ContextMenuProps, type ContextMenuRootProps, type ContextMenuTriggerProps } from "./context-menu.js";
29
+ export { ContextMenu, ContextMenuRoot, ContextMenuTrigger, ContextMenuContent, type ContextMenuProps, type ContextMenuRootProps, type ContextMenuTriggerProps, type ContextMenuContentProps } from "./context-menu.js";
29
30
  export { Dialog, DialogRoot, DialogTrigger, DialogBackdrop, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose, type DialogProps, type DialogRootProps, type DialogTriggerProps, type DialogBackdropProps, type DialogContentProps, type DialogHeaderProps, type DialogTitleProps, type DialogDescriptionProps, type DialogBodyProps, type DialogFooterProps, type DialogCloseProps } from "./dialog.js";
30
31
  export { AlertDialog, AlertDialogRoot, AlertDialogBackdrop, AlertDialogContent, type AlertDialogProps, type AlertDialogRootProps, type AlertDialogBackdropProps, type AlertDialogContentProps } from "./alert-dialog.js";
31
32
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, type TooltipProps, type TooltipRootProps, type TooltipTriggerProps, type TooltipContentProps } from "./tooltip.js";
package/dist/main.js CHANGED
@@ -11,6 +11,7 @@ export { Grid } from "./grid.js";
11
11
  export { Surface } from "./surface.js";
12
12
  export { defaultColor } from "./color.js";
13
13
  export { Panel, PanelRoot, PanelHeader, PanelContent } from "./panel.js";
14
+ export { Window } from "./window.js";
14
15
  export { Button } from "./button.js";
15
16
  export { Input } from "./input.js";
16
17
  export { Textarea } from "./textarea.js";
@@ -23,7 +24,7 @@ export { ScrollArea } from "./scroll-area.js";
23
24
  export { Popover, PopoverRoot, PopoverTrigger, PopoverContent, PopoverDialog, PopoverTitle, PopoverClose } from "./popover.js";
24
25
  export { Menu, MenuRoot, MenuItem, MenuSection, MenuHeader, MenuSeparator } from "./menu.js";
25
26
  export { DropdownMenu, DropdownMenuRoot, DropdownMenuTrigger } from "./dropdown-menu.js";
26
- export { ContextMenu, ContextMenuRoot, ContextMenuTrigger } from "./context-menu.js";
27
+ export { ContextMenu, ContextMenuRoot, ContextMenuTrigger, ContextMenuContent } from "./context-menu.js";
27
28
  export { Dialog, DialogRoot, DialogTrigger, DialogBackdrop, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose } from "./dialog.js";
28
29
  export { AlertDialog, AlertDialogRoot, AlertDialogBackdrop, AlertDialogContent } from "./alert-dialog.js";
29
30
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent } from "./tooltip.js";
@@ -8,13 +8,22 @@ export interface MaterialOptions {
8
8
  readonly distortion?: ScaleLevel | number;
9
9
  readonly saturation?: ScaleLevel | number;
10
10
  }
11
+ /** React UI's progressively complete browser rendering strategies for a resolved Material. */
12
+ export type MaterialMode = "none" | "basic" | "extended" | "full";
11
13
  /** Grouped material customization exposed by components built on Surface. */
12
14
  export interface MaterialOverrides {
13
- /** Omission or true uses Appearance defaults; false uses the resolved color as a normal background. */
14
- readonly material?: boolean | MaterialOptions;
15
+ /** Omission uses basic rendering. An options object uses full rendering with those overrides. */
16
+ readonly material?: MaterialMode | MaterialOptions;
15
17
  }
16
- /** Resolves material values from the current Appearance without knowing any host geometry. */
17
- export declare function useMaterialOptions(values?: MaterialOptions): {
18
+ /** Resolves overrides against an already selected material branch. */
19
+ export declare function resolveMaterialOptions(values: MaterialOptions, defaults: Readonly<{
20
+ opacity: number;
21
+ backdrop: number;
22
+ grain: number;
23
+ grainAmount: number;
24
+ distortion: number;
25
+ saturation: number;
26
+ }>): {
18
27
  opacity: number;
19
28
  backdrop: number;
20
29
  grain: number;
@@ -22,4 +31,4 @@ export declare function useMaterialOptions(values?: MaterialOptions): {
22
31
  distortion: number;
23
32
  saturation: number;
24
33
  };
25
- export type ResolvedMaterial = ReturnType<typeof useMaterialOptions>;
34
+ export type ResolvedMaterial = ReturnType<typeof resolveMaterialOptions>;
@@ -1,10 +1,7 @@
1
1
  import { appearanceLimits } from "./appearance.js";
2
- import { useAppearance, useThemedValue } from "./ui-provider.js";
3
2
  import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
4
- /** Resolves material values from the current Appearance without knowing any host geometry. */
5
- export function useMaterialOptions(values = {}) {
6
- const appearance = useAppearance();
7
- const defaults = useThemedValue(appearance.material);
3
+ /** Resolves overrides against an already selected material branch. */
4
+ export function resolveMaterialOptions(values, defaults) {
8
5
  const limits = appearanceLimits.material;
9
6
  return {
10
7
  opacity: resolve(values.opacity, defaults.opacity, limits.opacity),
@@ -1,3 +1,4 @@
1
+ import type { CSSProperties } from "react";
1
2
  interface MaterialPaintProps {
2
3
  readonly color: string;
3
4
  readonly distortion: number;
@@ -5,7 +6,8 @@ interface MaterialPaintProps {
5
6
  readonly grainAmount: number;
6
7
  readonly identity: string;
7
8
  readonly opacity: number;
9
+ readonly transition: CSSProperties;
8
10
  }
9
11
  /** The fill, refraction definition, and grain of one Surface. */
10
- export declare function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }: MaterialPaintProps): import("react").JSX.Element | null;
12
+ export declare function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity, transition }: MaterialPaintProps): import("react").JSX.Element | null;
11
13
  export {};
@@ -1,17 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
- import { usePaintTransition } from "./motion-style.js";
4
3
  /** The fill, refraction definition, and grain of one Surface. */
5
- export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }) {
6
- const paintTransition = usePaintTransition();
7
- const seed = useMemo(() => seedFrom(identity), [identity]);
4
+ export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity, transition }) {
5
+ const grainSeed = useMemo(() => grainSeeds[seedFrom(identity) % grainSeeds.length] ?? grainSeeds[0], [identity]);
8
6
  const hasPaint = opacity > 0;
9
7
  const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
10
8
  const hasDistortion = distortion > 0;
11
- const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
9
+ const grainResource = useMemo(() => hasGrain ? grainImage(grainSeed, grainAmount) : null, [grainSeed, grainAmount, hasGrain]);
12
10
  if (!hasPaint && !hasDistortion)
13
11
  return null;
14
- return _jsxs("svg", { "data-material-paint": "", "aria-hidden": "true", focusable: "false", style: {
12
+ return _jsxs("span", { "data-material-paint": "", "aria-hidden": "true", style: {
15
13
  position: "absolute",
16
14
  zIndex: -1,
17
15
  inset: 0,
@@ -21,16 +19,38 @@ export function MaterialPaint({ color, distortion, grain, grainAmount, identity,
21
19
  overflow: "hidden",
22
20
  borderRadius: "inherit",
23
21
  pointerEvents: "none"
24
- }, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-material-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsxs("g", { "data-material-fill": "", opacity: opacity, style: paintTransition, children: [_jsx("rect", { "data-material-base": "", width: "100%", height: "100%", style: { ...paintTransition, fill: color } }), hasGrain && _jsx("rect", { "data-material-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] });
22
+ }, children: [hasDistortion && _jsx("svg", { width: "0", height: "0", focusable: "false", style: { position: "absolute" }, children: _jsx("defs", { children: _jsx(DistortionFilter, { distortion: distortion, identity: identity }) }) }), hasPaint && _jsx("span", { "data-material-fill": "", "data-material-base": "", style: {
23
+ position: "absolute",
24
+ inset: 0,
25
+ borderRadius: "inherit",
26
+ background: color,
27
+ opacity,
28
+ ...transition,
29
+ transitionProperty: "background-color, opacity"
30
+ }, children: hasGrain && _jsx("span", { "data-material-grain": "", style: {
31
+ position: "absolute",
32
+ inset: 0,
33
+ borderRadius: "inherit",
34
+ backgroundImage: grainResource ?? undefined,
35
+ backgroundRepeat: "repeat",
36
+ backgroundSize: `${patternSize}px ${patternSize}px`,
37
+ opacity: grain,
38
+ ...transition,
39
+ transitionProperty: "opacity"
40
+ } }) })] });
25
41
  }
26
42
  function DistortionFilter({ distortion, identity }) {
27
43
  return _jsxs("filter", { id: `${identity}-distortion`, "data-material-distortion": "", x: "-20%", y: "-20%", width: "140%", height: "140%", colorInterpolationFilters: "sRGB", children: [_jsx("feTurbulence", { "data-material-distortion-noise": "", "data-material-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` }), _jsx("feDisplacementMap", { "data-material-distortion-stage": "organic", in: "SourceGraphic", in2: `${identity}-organic-noise-blurred`, scale: distortion, xChannelSelector: "R", yChannelSelector: "G" })] });
28
44
  }
29
- function grainTone(color, tone, intensity) {
30
- const channel = Math.round(tone / (toneCount - 1) * 255);
31
- const percentage = Math.round(intensity * 10_000) / 100;
32
- return `color-mix(in srgb, ${color} ${100 - percentage}%, rgb(${channel} ${channel} ${channel}) ${percentage}%)`;
45
+ function grainImage(seed, amount) {
46
+ const paths = grainPaths(seed, amount)
47
+ .map((path, tone) => path
48
+ ? `<path d="${path}" fill="rgb(${toneChannel(tone)} ${toneChannel(tone)} ${toneChannel(tone)})"/>`
49
+ : "")
50
+ .join("");
51
+ return `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${patternSize}" height="${patternSize}" viewBox="0 0 ${patternSize} ${patternSize}">${paths}</svg>`)}")`;
33
52
  }
53
+ /** Preserve the original contract: amount is the monotonic density of visible grain pixels. */
34
54
  function grainPaths(seed, amount) {
35
55
  const tones = Array.from({ length: toneCount }, () => []);
36
56
  for (let y = 0; y < patternSize; y += 1) {
@@ -49,6 +69,9 @@ function grainPaths(seed, amount) {
49
69
  }
50
70
  return tones.map(tone => tone.join(""));
51
71
  }
72
+ function toneChannel(tone) {
73
+ return Math.round(tone / (toneCount - 1) * 255);
74
+ }
52
75
  function shaderHash(x, y) {
53
76
  let red = fract(x * 0.1031);
54
77
  let green = fract(y * 0.1031);
@@ -75,3 +98,4 @@ function clamp(value, minimum, maximum) {
75
98
  }
76
99
  const patternSize = 64;
77
100
  const toneCount = 16;
101
+ const grainSeeds = [19, 47, 83, 131];
package/dist/menu.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, forwardRef, useContext } from "react";
2
+ import { createContext, forwardRef, useContext, useMemo } from "react";
3
3
  import { Header as AriaHeader, Menu as AriaMenu, MenuItem as AriaMenuItem, MenuSection as AriaMenuSection, Separator as AriaSeparator } from "react-aria-components";
4
4
  import { colorOpacity } from "./color.js";
5
- import { useControlTheme } from "./control.js";
5
+ import { controlFontWeight, controlOpacity, useControlTheme } from "./control.js";
6
6
  const MenuThemeContext = createContext(null);
7
7
  const MenuRootImplementation = forwardRef(function MenuRoot({ className, color, size = "medium", style, ...properties }, ref) {
8
8
  const theme = useControlTheme({ color, size });
9
- return _jsx(MenuThemeContext.Provider, { value: { color, size, theme }, children: _jsx(AriaMenu, { ...properties, ref: ref, className: className, style: {
9
+ const context = useMemo(() => ({ color, size, theme }), [color, size, theme]);
10
+ return _jsx(MenuThemeContext.Provider, { value: context, children: _jsx(AriaMenu, { ...properties, ref: ref, className: className, style: {
10
11
  display: "grid",
11
12
  gap: theme.gap,
12
- minWidth: 160,
13
+ minWidth: theme.height * 4,
13
14
  padding: theme.gap,
14
15
  boxSizing: "border-box",
15
16
  outline: "none",
@@ -22,11 +23,19 @@ const MenuRootImplementation = forwardRef(function MenuRoot({ className, color,
22
23
  export const MenuRoot = MenuRootImplementation;
23
24
  const MenuItemImplementation = forwardRef(function MenuItem({ className, color, disabled = false, style, ...properties }, ref) {
24
25
  const inherited = useContext(MenuThemeContext);
25
- const theme = useControlTheme({ color: color ?? inherited?.color, size: inherited?.size ?? "medium" });
26
+ const item = { className, disabled, properties, ref, style };
27
+ return inherited != null && color === undefined
28
+ ? _jsx(MenuItemView, { ...item, theme: inherited.theme })
29
+ : _jsx(ResolvedMenuItem, { ...item, color: color ?? inherited?.color, size: inherited?.size });
30
+ });
31
+ function ResolvedMenuItem({ color, size, ...properties }) {
32
+ return _jsx(MenuItemView, { ...properties, theme: useControlTheme({ color, size }) });
33
+ }
34
+ function MenuItemView({ className, disabled, properties, ref, style, theme }) {
26
35
  return _jsx(AriaMenuItem, { ...properties, ref: ref, className: className, isDisabled: disabled, style: state => {
27
36
  const paint = state.isPressed
28
37
  ? theme.paints.palette.pressed
29
- : state.isFocused || state.isHovered || state.isSelected
38
+ : state.isHovered || state.isSelected
30
39
  ? theme.paints.palette.hover
31
40
  : { background: "transparent", color: theme.foreground };
32
41
  return {
@@ -40,41 +49,44 @@ const MenuItemImplementation = forwardRef(function MenuItem({ className, color,
40
49
  paddingInline: Math.max(8, theme.spacing),
41
50
  boxSizing: "border-box",
42
51
  borderRadius: theme.radius,
43
- outline: "none",
52
+ outline: state.isFocusVisible ? `1px solid ${theme.focusColor}` : "none",
53
+ outlineOffset: 1,
44
54
  cursor: disabled ? "not-allowed" : "pointer",
45
- opacity: disabled ? 0.46 : 1,
55
+ opacity: disabled ? controlOpacity.disabled : 1,
46
56
  userSelect: "none",
47
57
  ...paint,
48
58
  ...style
49
59
  };
50
60
  } });
51
- });
61
+ }
52
62
  export const MenuItem = MenuItemImplementation;
53
63
  export const MenuSection = AriaMenuSection;
54
64
  export const MenuHeader = forwardRef(function MenuHeader({ style, ...properties }, ref) {
55
- const inherited = useContext(MenuThemeContext);
56
- const fallback = useControlTheme({});
57
- const theme = inherited?.theme ?? fallback;
65
+ const theme = useMenuTheme();
58
66
  return _jsx(AriaHeader, { ...properties, ref: ref, style: {
59
67
  paddingInline: Math.max(8, theme.spacing),
60
68
  paddingBlock: theme.gap,
61
- fontWeight: 600,
62
- opacity: 0.7,
69
+ fontWeight: controlFontWeight,
70
+ opacity: controlOpacity.secondary,
63
71
  ...style
64
72
  } });
65
73
  });
66
74
  export const MenuSeparator = forwardRef(function MenuSeparator({ style, ...properties }, ref) {
67
- const inherited = useContext(MenuThemeContext);
68
- const fallback = useControlTheme({});
69
- const theme = inherited?.theme ?? fallback;
75
+ const theme = useMenuTheme();
70
76
  return _jsx(AriaSeparator, { ...properties, ref: ref, style: {
71
77
  height: 1,
72
78
  marginBlock: theme.gap,
73
79
  border: 0,
74
- background: colorOpacity(theme.foreground, 0.14),
80
+ background: colorOpacity(theme.foreground, controlOpacity.separator),
75
81
  ...style
76
82
  } });
77
83
  });
84
+ function useMenuTheme() {
85
+ const inherited = useContext(MenuThemeContext);
86
+ if (inherited == null)
87
+ throw new Error("Menu parts must be used inside Menu");
88
+ return inherited.theme;
89
+ }
78
90
  /** A keyboard-navigable collection of commands or selectable options. */
79
91
  export const Menu = Object.assign(MenuRoot, {
80
92
  Root: MenuRoot,
@@ -1,15 +1,13 @@
1
1
  import type { CSSProperties } from "react";
2
2
  import type { Transition } from "motion/react";
3
- /** CSS timing for values painted directly by React UI. */
4
- export declare function useVisualTransition(): CSSProperties;
5
- /** CSS timing for SVG paint owned by a Material. */
6
- export declare function usePaintTransition(): CSSProperties;
7
- /** Motion timing for interactive geometry owned by React UI. */
8
- export declare function useControlTransition(): Transition;
3
+ import type { AppearanceTransaction } from "./appearance.js";
4
+ export declare function controlTransition(transaction: AppearanceTransaction, animations: boolean): Transition;
9
5
  /** Per-overlay variables consumed by the shared entrance keyframes. */
10
- export declare function useOverlayTransition(): CSSProperties;
6
+ export declare function overlayTransition(transaction: AppearanceTransaction, animations: boolean): CSSProperties;
11
7
  export declare const overlayMotionClass = "phreshos-ui-overlay";
12
8
  export declare const backdropMotionClass = "phreshos-ui-backdrop";
13
- export declare function useTransitionTiming(): CSSProperties;
9
+ export declare function transitionTiming(transaction: AppearanceTransaction, animations: boolean): CSSProperties;
10
+ export declare function visualTransition(transaction: AppearanceTransaction, animations: boolean): CSSProperties;
11
+ export declare function paintTransition(transaction: AppearanceTransaction, animations: boolean): CSSProperties;
14
12
  /** React hoists and deduplicates this stylesheet, including portals. */
15
13
  export default function MotionStyle(): import("react").JSX.Element;
@@ -1,45 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { defaultAppearance } from "./appearance.js";
3
- import { useAppearance, usePreferences } from "./appearance-context.js";
4
2
  const visualProperties = "background-color, color, border-color, border-radius, box-shadow";
5
3
  const paintProperties = "fill, stroke, opacity";
6
- /** CSS timing for values painted directly by React UI. */
7
- export function useVisualTransition() {
8
- return { ...useTransitionTiming(), transitionProperty: visualProperties };
9
- }
10
- /** CSS timing for SVG paint owned by a Material. */
11
- export function usePaintTransition() {
12
- return { ...useTransitionTiming(), transitionProperty: paintProperties };
13
- }
14
- /** Motion timing for interactive geometry owned by React UI. */
15
- export function useControlTransition() {
16
- const { duration, easing } = useAppearance().transaction;
17
- const { animations } = usePreferences();
4
+ export function controlTransition(transaction, animations) {
18
5
  return {
19
6
  type: "tween",
20
- duration: animations ? duration / 1_000 : 0,
21
- ease: motionEasing(easing)
7
+ duration: animations ? transaction.duration / 1_000 : 0,
8
+ ease: motionEasing(transaction.easing)
22
9
  };
23
10
  }
24
11
  /** Per-overlay variables consumed by the shared entrance keyframes. */
25
- export function useOverlayTransition() {
26
- const { duration, easing } = useAppearance().transaction;
27
- const { animations } = usePreferences();
12
+ export function overlayTransition(transaction, animations) {
28
13
  return {
29
- "--phreshos-ui-motion-duration": `${animations ? duration : 0}ms`,
30
- "--phreshos-ui-motion-easing": cssEasing(easing)
14
+ "--phreshos-ui-motion-duration": `${animations ? transaction.duration : 0}ms`,
15
+ "--phreshos-ui-motion-easing": cssEasing(transaction.easing)
31
16
  };
32
17
  }
33
18
  export const overlayMotionClass = "phreshos-ui-overlay";
34
19
  export const backdropMotionClass = "phreshos-ui-backdrop";
35
- export function useTransitionTiming() {
36
- const { duration, easing } = useAppearance().transaction;
37
- const { animations } = usePreferences();
20
+ export function transitionTiming(transaction, animations) {
38
21
  return {
39
- transitionDuration: `${animations ? duration : 0}ms`,
40
- transitionTimingFunction: cssEasing(easing)
22
+ transitionDuration: `${animations ? transaction.duration : 0}ms`,
23
+ transitionTimingFunction: cssEasing(transaction.easing)
41
24
  };
42
25
  }
26
+ export function visualTransition(transaction, animations) {
27
+ return { ...transitionTiming(transaction, animations), transitionProperty: visualProperties };
28
+ }
29
+ export function paintTransition(transaction, animations) {
30
+ return { ...transitionTiming(transaction, animations), transitionProperty: paintProperties };
31
+ }
43
32
  function cssEasing(easing) {
44
33
  return typeof easing === "string" ? easing : `cubic-bezier(${easing.join(", ")})`;
45
34
  }
@@ -57,11 +46,6 @@ function motionEasing(easing) {
57
46
  return easing;
58
47
  }
59
48
  const stylesheet = `
60
- :root {
61
- --phreshos-ui-motion-duration: ${defaultAppearance.transaction.duration}ms;
62
- --phreshos-ui-motion-easing: ${cssEasing(defaultAppearance.transaction.easing)};
63
- }
64
-
65
49
  .phreshos-ui-overlay[data-entering] {
66
50
  animation: phreshos-ui-overlay-enter var(--phreshos-ui-motion-duration) var(--phreshos-ui-motion-easing) both;
67
51
  }