@phreshos/react-ui 0.1.27 → 0.1.29

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.
Files changed (47) hide show
  1. package/README.md +40 -21
  2. package/dist/appearance-context.d.ts +47 -16
  3. package/dist/appearance-context.js +70 -20
  4. package/dist/appearance.d.ts +6 -0
  5. package/dist/appearance.js +11 -0
  6. package/dist/button.js +6 -10
  7. package/dist/color.d.ts +7 -5
  8. package/dist/color.js +37 -12
  9. package/dist/context-menu.d.ts +4 -1
  10. package/dist/context-menu.js +30 -3
  11. package/dist/control.d.ts +16 -2
  12. package/dist/control.js +34 -20
  13. package/dist/dialog.js +47 -40
  14. package/dist/direction.d.ts +9 -1
  15. package/dist/direction.js +34 -10
  16. package/dist/field-style.js +2 -1
  17. package/dist/layout.d.ts +1 -1
  18. package/dist/main.d.ts +4 -3
  19. package/dist/main.js +3 -2
  20. package/dist/material-options.d.ts +10 -3
  21. package/dist/material-options.js +3 -6
  22. package/dist/material-paint.d.ts +3 -1
  23. package/dist/material-paint.js +35 -11
  24. package/dist/menu.js +30 -18
  25. package/dist/motion-style.d.ts +6 -8
  26. package/dist/motion-style.js +15 -31
  27. package/dist/popover.js +13 -12
  28. package/dist/preferences.d.ts +4 -0
  29. package/dist/preferences.js +0 -0
  30. package/dist/radio.js +4 -3
  31. package/dist/radius.d.ts +1 -1
  32. package/dist/scroll-area.js +17 -10
  33. package/dist/select.js +24 -36
  34. package/dist/shadow-options.d.ts +9 -3
  35. package/dist/shadow-options.js +3 -6
  36. package/dist/slider.js +7 -6
  37. package/dist/spacing.d.ts +1 -1
  38. package/dist/surface-edge.d.ts +3 -1
  39. package/dist/surface-edge.js +3 -5
  40. package/dist/surface.js +24 -19
  41. package/dist/toggle-indicator.js +2 -5
  42. package/dist/tooltip.js +16 -15
  43. package/dist/ui-provider.d.ts +2 -1
  44. package/dist/ui-provider.js +9 -7
  45. package/dist/window-header.d.ts +42 -0
  46. package/dist/window-header.js +113 -0
  47. package/package.json +2 -2
package/dist/control.js CHANGED
@@ -1,37 +1,50 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { FieldError, Label, Text } from "react-aria-components";
4
- import { useAppearance, useThemedValue } from "./ui-provider.js";
4
+ import { useResolvedAppearance } from "./appearance-context.js";
5
5
  import { resolveRadius } from "./radius.js";
6
6
  import { scale } from "./scale.js";
7
7
  import { colorOpacity, solidColors } from "./color.js";
8
- import { useResolveSolidColor } from "./color.js";
9
- import { useVisualTransition } from "./motion-style.js";
8
+ import { resolveSolidColor } from "./color.js";
9
+ import { controlTransition, visualTransition } from "./motion-style.js";
10
10
  export const controlFontSizes = Object.freeze({
11
- xsmall: 11,
12
- small: 12,
13
- medium: 13,
14
- large: 14,
15
- xlarge: 15
11
+ xsmall: "0.6875em",
12
+ small: "0.75em",
13
+ medium: "0.8125em",
14
+ large: "0.875em",
15
+ xlarge: "0.9375em"
16
16
  });
17
+ /** Shared interaction attenuation, derived consistently across control families. */
18
+ export const controlOpacity = Object.freeze({
19
+ disabled: 0.46,
20
+ pending: 0.68,
21
+ secondary: 0.7,
22
+ placeholder: 0.6,
23
+ separator: 0.14
24
+ });
25
+ /** Shared emphasis used by labels and control-owned headings. */
26
+ export const controlFontWeight = 600;
17
27
  export function useControlTheme({ size = "medium", color, radius = "medium" }) {
18
- const appearance = useAppearance();
19
- const transition = useVisualTransition();
28
+ const resolved = useResolvedAppearance();
29
+ const { appearance, colors } = resolved;
30
+ const transition = useMemo(() => visualTransition(resolved.transaction, resolved.preferences.animations), [resolved.transaction, resolved.preferences.animations]);
31
+ const motionTransition = useMemo(() => controlTransition(resolved.transaction, resolved.preferences.animations), [resolved.transaction, resolved.preferences.animations]);
20
32
  const spacing = scale(appearance.spacing, size);
21
- const colors = useThemedValue(appearance.colors);
22
33
  const foreground = colors.foreground;
23
34
  const background = colors.background;
24
- const tint = useResolveSolidColor(color ?? "default:base");
25
- const neutral = useResolveSolidColor("default:base");
26
- const focus = useResolveSolidColor("warning:base");
27
- const danger = useResolveSolidColor("danger:base");
35
+ const tint = resolveSolidColor(color ?? "default:base", colors);
36
+ const neutral = resolveSolidColor("default:base", colors);
37
+ const focus = resolveSolidColor("warning:base", colors);
38
+ const danger = resolveSolidColor("danger:base", colors);
28
39
  const paints = useMemo(() => ({
29
40
  palette: solidColors(tint, background, foreground),
30
41
  neutral: solidColors(neutral, background, foreground),
31
42
  danger: solidColors(danger, background, foreground)
32
43
  }), [tint, neutral, background, foreground, danger]);
33
- return {
44
+ return useMemo(() => ({
34
45
  transition,
46
+ motionTransition,
47
+ animations: resolved.preferences.animations,
35
48
  spacing,
36
49
  foreground,
37
50
  background,
@@ -41,9 +54,10 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
41
54
  paints,
42
55
  radius: resolveRadius(radius, appearance),
43
56
  fontSize: controlFontSizes[size],
57
+ indicatorSize: Math.max(16, 16 + spacing / 4),
44
58
  height: Math.max(24, 24 + spacing),
45
59
  gap: Math.max(4, spacing / 2)
46
- };
60
+ }), [appearance, colors, transition, motionTransition, resolved.preferences.animations, spacing, foreground, background, tint, focus, danger, paints, radius, size]);
47
61
  }
48
62
  export function fieldStyle(theme, disabled = false, style) {
49
63
  return {
@@ -56,7 +70,7 @@ export function fieldStyle(theme, disabled = false, style) {
56
70
  fontFamily: "inherit",
57
71
  fontSize: theme.fontSize,
58
72
  lineHeight: 1.5,
59
- opacity: disabled ? 0.46 : 1
73
+ opacity: disabled ? controlOpacity.disabled : 1
60
74
  };
61
75
  }
62
76
  export function controlPaint(theme, focused, invalid, hovered = false) {
@@ -85,8 +99,8 @@ export function controlStyle(theme, focused, invalid, hovered = false, focusVisi
85
99
  };
86
100
  }
87
101
  export function FieldLabel({ label }) {
88
- return label == null ? null : _jsx(Label, { style: { fontWeight: 550 }, children: label });
102
+ return label == null ? null : _jsx(Label, { style: { fontWeight: controlFontWeight }, children: label });
89
103
  }
90
104
  export function FieldFeedback({ description, errorMessage, theme }) {
91
- return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: 0.7 }, children: description }), _jsx(FieldError, { style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
105
+ return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: controlOpacity.secondary }, children: description }), _jsx(FieldError, { style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
92
106
  }
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/layout.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties } from "react";
2
- import type { Appearance } from "@phreshos/core";
2
+ import type { Appearance } from "./appearance.js";
3
3
  import { type Spacing } from "./spacing.js";
4
4
  /** Cross-axis alignment shared by layout containers. */
5
5
  export type LayoutAlignment = "start" | "center" | "end" | "stretch" | "baseline";
package/dist/main.d.ts CHANGED
@@ -4,15 +4,16 @@
4
4
  * Components enter this surface only after their implementation-independent
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
- export { defaultAppearance } from "@phreshos/core";
7
+ export { defaultAppearance, type Appearance, type AppearanceColor, type AppearanceColors, type AppearanceMaterial, type AppearanceShadow, type AppearanceTransaction, type Easing, type ThemedValue } from "./appearance.js";
8
8
  export { UIProvider, useAppearance, useBrowserPreferences, useDirection, useDocumentDirection, usePreferences, useThemedValue, type UIProviderProps } from "./ui-provider.js";
9
- export type { DesktopPreferences as Preferences } from "@phreshos/core";
9
+ 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
13
  export { Surface, 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 { WindowHeader, WindowHeaderRoot, WindowHeaderIdentity, WindowHeaderCenter, WindowHeaderActions, WindowHeaderAction, WindowHeaderMinimize, WindowHeaderMaximize, WindowHeaderClose, type WindowHeaderProps, type WindowHeaderRootProps, type WindowHeaderIdentityProps, type WindowHeaderCenterProps, type WindowHeaderActionsProps, type WindowHeaderActionProps, type WindowHeaderControlProps, type WindowHeaderMaximizeProps, type WindowHeaderCloseProps } from "./window-header.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
@@ -4,13 +4,14 @@
4
4
  * Components enter this surface only after their implementation-independent
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
- export { defaultAppearance } from "@phreshos/core";
7
+ export { defaultAppearance } from "./appearance.js";
8
8
  export { UIProvider, useAppearance, useBrowserPreferences, useDirection, useDocumentDirection, usePreferences, useThemedValue } from "./ui-provider.js";
9
9
  export { Flex } from "./flex.js";
10
10
  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 { WindowHeader, WindowHeaderRoot, WindowHeaderIdentity, WindowHeaderCenter, WindowHeaderActions, WindowHeaderAction, WindowHeaderMinimize, WindowHeaderMaximize, WindowHeaderClose } from "./window-header.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";
@@ -13,8 +13,15 @@ export interface MaterialOverrides {
13
13
  /** Omission or true uses Appearance defaults; false uses the resolved color as a normal background. */
14
14
  readonly material?: boolean | MaterialOptions;
15
15
  }
16
- /** Resolves material values from the current Appearance without knowing any host geometry. */
17
- export declare function useMaterialOptions(values?: MaterialOptions): {
16
+ /** Resolves overrides against an already selected material branch. */
17
+ export declare function resolveMaterialOptions(values: MaterialOptions, defaults: Readonly<{
18
+ opacity: number;
19
+ backdrop: number;
20
+ grain: number;
21
+ grainAmount: number;
22
+ distortion: number;
23
+ saturation: number;
24
+ }>): {
18
25
  opacity: number;
19
26
  backdrop: number;
20
27
  grain: number;
@@ -22,4 +29,4 @@ export declare function useMaterialOptions(values?: MaterialOptions): {
22
29
  distortion: number;
23
30
  saturation: number;
24
31
  };
25
- export type ResolvedMaterial = ReturnType<typeof useMaterialOptions>;
32
+ export type ResolvedMaterial = ReturnType<typeof resolveMaterialOptions>;
@@ -1,10 +1,7 @@
1
- import { appearanceLimits } from "@phreshos/core";
2
- import { useAppearance, useThemedValue } from "./ui-provider.js";
1
+ import { appearanceLimits } from "./appearance.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,