@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/README.md CHANGED
@@ -8,8 +8,8 @@ Environment-neutral React components and the visual language of PhreshOS.
8
8
 
9
9
  ## Role
10
10
 
11
- React UI interprets Core Appearance and Theme contracts as reusable visual
12
- primitives. The Desktop and Programs compose those primitives instead of
11
+ React UI owns reusable visual contracts that accept the corresponding PhreshOS
12
+ System values directly. The Desktop and Programs compose those primitives instead of
13
13
  reimplementing material, spacing, color, radius, or interaction behavior.
14
14
 
15
15
  The package does not depend on a Client or Server runtime and does not own
@@ -25,9 +25,8 @@ applications own composition.
25
25
  | Bun | `bun add @phreshos/react-ui` |
26
26
  | Yarn | `yarn add @phreshos/react-ui` |
27
27
 
28
- Core is a built-in runtime dependency. React and React DOM remain peer
29
- dependencies because the application and React UI must share one React
30
- runtime.
28
+ React and React DOM are peer dependencies because the application and React UI
29
+ must share one React runtime.
31
30
 
32
31
  ```tsx
33
32
  import { Button, Surface } from "@phreshos/react-ui"
@@ -37,16 +36,16 @@ import { Button, Surface } from "@phreshos/react-ui"
37
36
  </Surface>
38
37
  ```
39
38
 
40
- Without a provider, components use Core's `defaultAppearance` and reactively
39
+ Without a provider, components use React UI's `defaultAppearance` and reactively
41
40
  follow the browser's complete visual preferences. `UIProvider` supplies the
42
41
  Appearance, Preferences, and direction explicitly established for a React UI
43
42
  subtree. Each omitted property inherits independently from the nearest provider
44
43
  or follows its own default resolution. A provider with no properties introduces
45
44
  no boundary or behavior.
46
- React UI also exports that same canonical `defaultAppearance` value for callers
47
- that need it explicitly.
48
- See [Appearance](https://docs.phreshos.com/system/appearance) for the contract
49
- interpreted by the provider and components.
45
+ React UI also exports `defaultAppearance` for callers that need the value
46
+ explicitly. A complete System Appearance can be passed directly; React UI uses
47
+ the visual fields it owns and ignores additional System fields such as
48
+ wallpapers.
50
49
 
51
50
  React UI is neutral between left-to-right and right-to-left interfaces.
52
51
  An explicit `UIProvider` direction creates a boundary that participates in
@@ -80,11 +79,12 @@ use the shared Surface implementation while retaining their native behavior. Col
80
79
  are independent inputs. `color` accepts an Appearance color and resting level such
81
80
  as `background:base` or `primary:soft`, or a direct CSS color. Surface defaults
82
81
  to `background:base`; solid controls default to `default:base`.
83
- Hover and press derive shades of that fill. The resting fill chooses the
84
- higher-contrast text from Appearance's background and foreground once; interaction
85
- shades keep that choice.
82
+ Hover and press derive shades of that fill. Surface and solid controls choose
83
+ the higher-contrast text from Appearance's background and foreground whenever
84
+ their fill can be evaluated; interaction shades keep that choice.
86
85
  `size` accepts `xsmall`, `small`, `medium` (default), `large`, or `xlarge`;
87
- spacing and radius follow Appearance. `disabled` prevents activation and focus;
86
+ spacing and radius follow Appearance, while typography remains inherited and
87
+ component sizes are relative to the surrounding text. `disabled` prevents activation and focus;
88
88
  `pending` prevents activation while retaining focus.
89
89
 
90
90
  ```tsx
@@ -96,7 +96,7 @@ spacing and radius follow Appearance. `disabled` prevents activation and focus;
96
96
  `Surface` is the material- and shadow-owning element. It renders a `div` by default, while
97
97
  `as` selects another React element and preserves that element's native properties
98
98
  and ref type. Surface owns its paint, opacity, frost, refraction, grain, edge,
99
- outer shadow, radius, and clipping requirements. `radius` accepts a size level, `full`, a
99
+ outer shadow, and radius. `radius` accepts a size level, `full`, a
100
100
  number in pixels, or a CSS radius and defaults to `medium`.
101
101
 
102
102
  ```tsx
@@ -154,6 +154,25 @@ Native properties and the root ref target the outer Surface. `Panel.Header`
154
154
  owns the optional leading region, while `Panel.Content` is the independently
155
155
  configurable inner Surface.
156
156
 
157
+ `WindowHeader` provides the layout shared by Desktop-owned and application-owned
158
+ window headers. `Identity` presents the icon and truncating title, `Center` is
159
+ optional flexible space, and `Actions` aligns compact controls at the end. The
160
+ header uses the enclosing Surface rather than creating another material layer.
161
+ Its controls are React UI Buttons; callers connect them to their own window
162
+ operations. Pointer and double-click handlers on the root can implement a drag
163
+ area, while the center and actions keep their interactions separate.
164
+
165
+ ```tsx
166
+ <WindowHeader active={active} onPointerDown={beginDrag}>
167
+ <WindowHeader.Identity icon={icon} title={title} />
168
+ <WindowHeader.Actions>
169
+ <WindowHeader.Minimize onPress={minimize} />
170
+ <WindowHeader.Maximize maximized={maximized} onPress={toggleMaximize} />
171
+ <WindowHeader.Close onPress={close} />
172
+ </WindowHeader.Actions>
173
+ </WindowHeader>
174
+ ```
175
+
157
176
  ## Overlays
158
177
 
159
178
  Overlay components expose each behavioral role as a named part. Roots own open
@@ -203,8 +222,8 @@ Blur, distortion, geometry, and Surface host opacity are not transitioned; gradi
203
222
  and structurally removed effects change directly rather than adding extra
204
223
  layers or keeping disabled effects alive.
205
224
 
206
- Motion animates toggle presses, selection marks, switch travel, the Select
207
- chevron, and Slider thumb feedback. Preferences with animations disabled remove spatial
225
+ Motion animates toggle presses, selection marks, and switch travel. CSS transitions
226
+ animate paint and the Select chevron. Preferences with animations disabled remove spatial
208
227
  feedback and make state transitions immediate. Slider values and native input
209
228
  behavior are never delayed by visual animation.
210
229
 
@@ -256,8 +275,9 @@ import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider }
256
275
  <Slider label="Volume" name="volume" defaultValue={50} minValue={0} maxValue={100} step={1} />
257
276
  ```
258
277
 
259
- React Aria owns focus, keyboard, form, and selection behavior. Select's popup
260
- uses Surface defaults. Toggle indicators use Motion internally and respect
278
+ Each control delegates focus, keyboard, form, and selection behavior to its
279
+ accessible behavioral primitive rather than recreating those systems. Select's
280
+ popup uses Surface defaults. Toggle indicators use Motion internally and respect
261
281
  animation preferences. The preview Program demonstrates each input's
262
282
  sizes, colors, state, and interaction without overriding its visual defaults.
263
283
 
@@ -279,8 +299,7 @@ the committed dependency graph without local package substitutions.
279
299
 
280
300
  ## Related repositories
281
301
 
282
- - [`@phreshos/core`](https://github.com/PhreshOS/core) owns Appearance, Theme,
283
- and the shared values interpreted here.
302
+ - [`@phreshos/core`](https://github.com/PhreshOS/core) owns the System contracts.
284
303
  - [`@phreshos/react`](https://github.com/PhreshOS/react) owns runtime-neutral
285
304
  React state adaptation.
286
305
  - [PhreshOS System](https://github.com/PhreshOS/system) composes the visual
@@ -1,19 +1,12 @@
1
- import { type Appearance, type DesktopPreferences as Preferences, type ThemedValue } from "@phreshos/core";
2
- export declare const AppearanceContext: import("react").Context<Readonly<{
3
- colors: ThemedValue<import("@phreshos/core").AppearanceColors>;
4
- spacing: number;
5
- radius: number;
6
- shadow: ThemedValue<import("@phreshos/core").AppearanceShadow>;
7
- material: ThemedValue<import("@phreshos/core").AppearanceMaterial>;
8
- transaction: import("@phreshos/core").AppearanceTransaction;
9
- signInWallpaper: ThemedValue<string | null>;
10
- desktopWallpaper: ThemedValue<string | null>;
11
- }>>;
12
- export declare const PreferencesContext: import("react").Context<Readonly<{
13
- theme: import("@phreshos/core").Theme;
14
- animations: boolean;
15
- }> | null>;
16
- /** Returns the nearest unresolved Appearance, or Core's complete default. */
1
+ import { type Appearance, type ThemedValue } from "./appearance.js";
2
+ import type { Preferences } from "./preferences.js";
3
+ export declare const AppearanceContext: import("react").Context<Appearance>;
4
+ export interface PreferencesSource {
5
+ readonly getSnapshot: () => Preferences;
6
+ readonly getServerSnapshot: () => Preferences;
7
+ readonly subscribe: (change: () => void) => () => void;
8
+ }
9
+ /** Returns the nearest unresolved appearance, or React UI's default. */
17
10
  export declare function useAppearance(): Appearance;
18
11
  /** Returns the nearest complete Preferences, or reactively follows the browser. */
19
12
  export declare function usePreferences(): Preferences;
@@ -21,3 +14,41 @@ export declare function usePreferences(): Preferences;
21
14
  export declare function useBrowserPreferences(): Preferences;
22
15
  /** Selects the active branch of one complete ThemedValue. */
23
16
  export declare function useThemedValue<Value>(value: ThemedValue<Value>): Value;
17
+ /** Resolves one coherent Appearance snapshot for internal component derivation. */
18
+ export declare function useResolvedAppearance(): {
19
+ appearance: Appearance;
20
+ preferences: Preferences;
21
+ colors: Readonly<{
22
+ background: string;
23
+ foreground: string;
24
+ default: string;
25
+ primary: string;
26
+ secondary: string;
27
+ success: string;
28
+ warning: string;
29
+ danger: string;
30
+ info: string;
31
+ }>;
32
+ material: Readonly<{
33
+ grain: number;
34
+ grainAmount: number;
35
+ backdrop: number;
36
+ opacity: number;
37
+ distortion: number;
38
+ saturation: number;
39
+ }>;
40
+ shadow: Readonly<{
41
+ x: number;
42
+ y: number;
43
+ blur: number;
44
+ spread: number;
45
+ opacity: number;
46
+ }>;
47
+ transaction: Readonly<{
48
+ duration: number;
49
+ easing: import("@phreshos/core").Easing;
50
+ }>;
51
+ };
52
+ /** Creates a stable source for a complete explicitly supplied Preferences value. */
53
+ export declare function fixedPreferencesSource(preferences: Preferences): PreferencesSource;
54
+ export declare const PreferencesContext: import("react").Context<PreferencesSource>;
@@ -1,41 +1,91 @@
1
1
  import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
2
- import { defaultAppearance } from "@phreshos/core";
2
+ import { defaultAppearance } from "./appearance.js";
3
3
  export const AppearanceContext = createContext(defaultAppearance);
4
- export const PreferencesContext = createContext(null);
5
- /** Returns the nearest unresolved Appearance, or Core's complete default. */
4
+ /** Returns the nearest unresolved appearance, or React UI's default. */
6
5
  export function useAppearance() {
7
6
  return useContext(AppearanceContext);
8
7
  }
9
8
  /** Returns the nearest complete Preferences, or reactively follows the browser. */
10
9
  export function usePreferences() {
11
- const preferences = useContext(PreferencesContext);
12
- const browserPreferences = useBrowserPreferences();
13
- return preferences ?? browserPreferences;
10
+ const source = useContext(PreferencesContext);
11
+ return useSyncExternalStore(source.subscribe, source.getSnapshot, source.getServerSnapshot);
14
12
  }
15
13
  /** Reactively reads the browser's complete visual preferences. */
16
14
  export function useBrowserPreferences() {
17
- const theme = useMediaPreference(darkThemeQuery) ? "dark" : "light";
18
- const animations = !useMediaPreference(reducedMotionQuery);
19
- return useMemo(() => ({ theme, animations }), [animations, theme]);
15
+ return useSyncExternalStore(browserPreferencesSource.subscribe, browserPreferencesSource.getSnapshot, browserPreferencesSource.getServerSnapshot);
20
16
  }
21
17
  /** Selects the active branch of one complete ThemedValue. */
22
18
  export function useThemedValue(value) {
23
19
  return value[usePreferences().theme];
24
20
  }
25
- function useMediaPreference(query) {
26
- return useSyncExternalStore(change => subscribeMediaPreference(query, change), () => browserMediaPreference(query), () => false);
21
+ /** Resolves one coherent Appearance snapshot for internal component derivation. */
22
+ export function useResolvedAppearance() {
23
+ const appearance = useAppearance();
24
+ const preferences = usePreferences();
25
+ return useMemo(() => ({
26
+ appearance,
27
+ preferences,
28
+ colors: appearance.colors[preferences.theme],
29
+ material: appearance.material[preferences.theme],
30
+ shadow: appearance.shadow[preferences.theme],
31
+ transaction: appearance.transaction
32
+ }), [appearance, preferences]);
27
33
  }
28
- function subscribeMediaPreference(query, change) {
34
+ /** Creates a stable source for a complete explicitly supplied Preferences value. */
35
+ export function fixedPreferencesSource(preferences) {
36
+ return {
37
+ getSnapshot: () => preferences,
38
+ getServerSnapshot: () => preferences,
39
+ subscribe: emptySubscription
40
+ };
41
+ }
42
+ const defaultPreferences = Object.freeze({ theme: "light", animations: true });
43
+ const subscribers = new Set();
44
+ let darkPreference;
45
+ let reducedMotionPreference;
46
+ let browserSnapshot = defaultPreferences;
47
+ function readBrowserPreferences() {
29
48
  if (typeof window === "undefined" || typeof window.matchMedia !== "function")
30
- return () => undefined;
31
- const preference = window.matchMedia(query);
32
- preference.addEventListener("change", change);
33
- return () => preference.removeEventListener("change", change);
49
+ return defaultPreferences;
50
+ darkPreference ??= window.matchMedia(darkThemeQuery);
51
+ reducedMotionPreference ??= window.matchMedia(reducedMotionQuery);
52
+ const theme = darkPreference.matches ? "dark" : "light";
53
+ const animations = !reducedMotionPreference.matches;
54
+ if (browserSnapshot.theme !== theme || browserSnapshot.animations !== animations) {
55
+ browserSnapshot = Object.freeze({ theme, animations });
56
+ }
57
+ return browserSnapshot;
58
+ }
59
+ function subscribeBrowserPreferences(change) {
60
+ subscribers.add(change);
61
+ readBrowserPreferences();
62
+ if (subscribers.size === 1) {
63
+ darkPreference?.addEventListener("change", publishBrowserPreferences);
64
+ reducedMotionPreference?.addEventListener("change", publishBrowserPreferences);
65
+ }
66
+ return () => {
67
+ subscribers.delete(change);
68
+ if (subscribers.size !== 0)
69
+ return;
70
+ darkPreference?.removeEventListener("change", publishBrowserPreferences);
71
+ reducedMotionPreference?.removeEventListener("change", publishBrowserPreferences);
72
+ darkPreference = undefined;
73
+ reducedMotionPreference = undefined;
74
+ };
75
+ }
76
+ function publishBrowserPreferences() {
77
+ readBrowserPreferences();
78
+ for (const subscriber of subscribers)
79
+ subscriber();
34
80
  }
35
- function browserMediaPreference(query) {
36
- return Boolean(typeof window !== "undefined"
37
- && typeof window.matchMedia === "function"
38
- && window.matchMedia(query).matches);
81
+ function emptySubscription() {
82
+ return () => undefined;
39
83
  }
84
+ const browserPreferencesSource = {
85
+ getSnapshot: readBrowserPreferences,
86
+ getServerSnapshot: () => defaultPreferences,
87
+ subscribe: subscribeBrowserPreferences
88
+ };
89
+ export const PreferencesContext = createContext(browserPreferencesSource);
40
90
  const darkThemeQuery = "(prefers-color-scheme: dark)";
41
91
  const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
@@ -0,0 +1,6 @@
1
+ import { appearanceLimits, type Appearance as SystemAppearance, type AppearanceColor, type AppearanceColors, type AppearanceMaterial, type AppearanceRange, type AppearanceShadow, type AppearanceTransaction, type Easing, type ThemedValue } from "@phreshos/core";
2
+ /** Complete visual input consumed by React UI. */
3
+ export type Appearance = Pick<SystemAppearance, "colors" | "spacing" | "radius" | "shadow" | "material" | "transaction">;
4
+ export { appearanceLimits, type AppearanceColor, type AppearanceColors, type AppearanceMaterial, type AppearanceRange, type AppearanceShadow, type AppearanceTransaction, type Easing, type ThemedValue };
5
+ /** React UI's default visual input, derived from the System default. */
6
+ export declare const defaultAppearance: Appearance;
@@ -0,0 +1,11 @@
1
+ import { appearanceLimits, defaultAppearance as systemDefaultAppearance } from "@phreshos/core";
2
+ export { appearanceLimits };
3
+ /** React UI's default visual input, derived from the System default. */
4
+ export const defaultAppearance = Object.freeze({
5
+ colors: systemDefaultAppearance.colors,
6
+ spacing: systemDefaultAppearance.spacing,
7
+ radius: systemDefaultAppearance.radius,
8
+ shadow: systemDefaultAppearance.shadow,
9
+ material: systemDefaultAppearance.material,
10
+ transaction: systemDefaultAppearance.transaction
11
+ });
package/dist/button.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { Button as AriaButton } from "react-aria-components";
4
- import { controlFontSizes, useControlTheme } from "./control.js";
4
+ import { controlOpacity, useControlTheme } from "./control.js";
5
5
  import { SurfaceButton } from "./control-surface.js";
6
6
  /** An Appearance-aware action with normalized pointer and keyboard behavior. */
7
7
  export const Button = forwardRef(function Button({ children, color, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, material, shadow, type = "button", ...properties }, ref) {
@@ -13,14 +13,11 @@ export const Button = forwardRef(function Button({ children, color, disabled = f
13
13
  isHovered,
14
14
  isPressed,
15
15
  pending,
16
- size,
17
16
  style
18
17
  }), children: children });
19
18
  });
20
- function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pending, size, style }) {
21
- const fontSize = controlFontSizes[size];
19
+ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pending, style }) {
22
20
  const { spacing, focusColor } = theme;
23
- const height = Math.max(24, 24 + spacing);
24
21
  const interactive = !disabled && !pending;
25
22
  const paint = buttonPaint(theme, interactive, isHovered, isPressed);
26
23
  return {
@@ -34,20 +31,19 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
34
31
  justifyContent: "center",
35
32
  flexShrink: 0,
36
33
  minWidth: 0,
37
- height,
34
+ height: theme.height,
38
35
  paddingBlock: 0,
39
36
  paddingInline: Math.max(8, spacing),
40
- gap: Math.max(4, spacing / 2),
37
+ gap: theme.gap,
41
38
  border: "none",
42
39
  borderRadius: theme.radius,
43
40
  outline: isFocusVisible ? `1px solid ${focusColor}` : "none",
44
41
  outlineOffset: 1,
45
42
  ...paint,
46
- opacity: disabled ? 0.46 : pending ? 0.68 : 1,
43
+ opacity: disabled ? controlOpacity.disabled : pending ? controlOpacity.pending : 1,
47
44
  cursor: disabled ? "not-allowed" : pending ? "progress" : "pointer",
48
45
  font: "inherit",
49
- fontSize,
50
- fontWeight: 550,
46
+ fontSize: theme.fontSize,
51
47
  lineHeight: 1,
52
48
  textAlign: "center",
53
49
  textDecoration: "none",
package/dist/color.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AppearanceColor } from "@phreshos/core";
1
+ import type { AppearanceColor, AppearanceColors } from "./appearance.js";
2
2
  /** A visual treatment derived from one concrete CSS color. */
3
3
  export type ColorLevel = "subtle" | "soft" | "base" | "strong" | "intense";
4
4
  /** Every visual treatment derived from one concrete CSS color. */
@@ -14,10 +14,10 @@ export declare function color(value: string): ColorScale;
14
14
  export declare function resolveColorLevel(value: string, level: ColorLevel): string;
15
15
  /** Returns the complete visual treatments derived from one concrete color. */
16
16
  export declare function useColor(value: string): ColorScale;
17
- /** Resolves a semantic Appearance color or preserves a direct CSS color. */
18
- export declare function useResolveColor(value?: Color): string;
19
- /** Resolves a color to concrete opaque paint for solid interactive states. */
20
- export declare function useResolveSolidColor(value: Color): string;
17
+ /** Resolves a semantic color from an already selected Appearance branch. */
18
+ export declare function resolveColor(value: Color | undefined, colors: AppearanceColors): string;
19
+ /** Resolves opaque control paint from an already selected Appearance branch. */
20
+ export declare function resolveSolidColor(value: Color, colors: AppearanceColors): string;
21
21
  /** Applies material opacity without restricting the source CSS color syntax. */
22
22
  export declare function colorOpacity(value: string, opacity: number): string;
23
23
  /** Reads perceptual lightness from a resolved CSS color. */
@@ -33,6 +33,8 @@ export declare function opaqueColor(value: string): string;
33
33
  export declare function colorShade(value: string, amount: number): string;
34
34
  /** Chooses only from the two Appearance colors, against the actual painted fill. */
35
35
  export declare function onColor(fill: string, background: string, foreground: string): string;
36
+ /** Chooses readable content when the fill can be evaluated, otherwise preserves Appearance foreground. */
37
+ export declare function contrastingColor(value: Color, colors: AppearanceColors): string;
36
38
  /** Solid controls start at base; interaction shades preserve its text choice. */
37
39
  export declare function solidColors(base: string, background: string, foreground: string): {
38
40
  rest: {
package/dist/color.js CHANGED
@@ -1,6 +1,25 @@
1
1
  import { useMemo } from "react";
2
- import { ColorSpace, mix, parse, serialize, to, toGamut, contrastWCAG21, sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65 } from "colorjs.io/fn";
3
- import { useAppearance, useThemedValue } from "./ui-provider.js";
2
+ import ColorSpace from "colorjs.io/src/ColorSpace.js";
3
+ import { mix } from "colorjs.io/src/interpolation.js";
4
+ import parse from "colorjs.io/src/parse.js";
5
+ import serialize from "colorjs.io/src/serialize.js";
6
+ import to from "colorjs.io/src/to.js";
7
+ import toGamut from "colorjs.io/src/toGamut.js";
8
+ import contrastWCAG21 from "colorjs.io/src/contrast/WCAG21.js";
9
+ import sRGB from "colorjs.io/src/spaces/srgb.js";
10
+ import sRGB_Linear from "colorjs.io/src/spaces/srgb-linear.js";
11
+ import HSL from "colorjs.io/src/spaces/hsl.js";
12
+ import HWB from "colorjs.io/src/spaces/hwb.js";
13
+ import Lab from "colorjs.io/src/spaces/lab.js";
14
+ import LCH from "colorjs.io/src/spaces/lch.js";
15
+ import OKLab from "colorjs.io/src/spaces/oklab.js";
16
+ import OKLCH from "colorjs.io/src/spaces/oklch.js";
17
+ import P3 from "colorjs.io/src/spaces/p3.js";
18
+ import A98RGB from "colorjs.io/src/spaces/a98rgb.js";
19
+ import ProPhoto from "colorjs.io/src/spaces/prophoto.js";
20
+ import REC_2020 from "colorjs.io/src/spaces/rec2020.js";
21
+ import XYZ_D50 from "colorjs.io/src/spaces/xyz-d50.js";
22
+ import XYZ_D65 from "colorjs.io/src/spaces/xyz-d65.js";
4
23
  // Register the CSS color spaces, without bundling unrelated color-model APIs.
5
24
  for (const space of [sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65])
6
25
  ColorSpace.register(space);
@@ -40,17 +59,14 @@ export function resolveColorLevel(value, level) {
40
59
  export function useColor(value) {
41
60
  return useMemo(() => color(value), [value]);
42
61
  }
43
- /** Resolves a semantic Appearance color or preserves a direct CSS color. */
44
- export function useResolveColor(value = defaultColor) {
45
- const appearance = useAppearance();
46
- const colors = useThemedValue(appearance.colors);
47
- const semantic = parseSemanticColor(value, colors);
48
- return semantic ? color(semantic.source)[semantic.level] : value;
62
+ /** Resolves a semantic color from an already selected Appearance branch. */
63
+ export function resolveColor(value, colors) {
64
+ const resolved = value ?? defaultColor;
65
+ const semantic = parseSemanticColor(resolved, colors);
66
+ return semantic ? color(semantic.source)[semantic.level] : resolved;
49
67
  }
50
- /** Resolves a color to concrete opaque paint for solid interactive states. */
51
- export function useResolveSolidColor(value) {
52
- const appearance = useAppearance();
53
- const colors = useThemedValue(appearance.colors);
68
+ /** Resolves opaque control paint from an already selected Appearance branch. */
69
+ export function resolveSolidColor(value, colors) {
54
70
  const semantic = parseSemanticColor(value, colors);
55
71
  return semantic ? resolveColorLevel(semantic.source, semantic.level) : opaqueColor(value);
56
72
  }
@@ -88,6 +104,15 @@ export function onColor(fill, background, foreground) {
88
104
  const second = opaqueColor(foreground);
89
105
  return contrastWCAG21(fill, first) > contrastWCAG21(fill, second) ? first : second;
90
106
  }
107
+ /** Chooses readable content when the fill can be evaluated, otherwise preserves Appearance foreground. */
108
+ export function contrastingColor(value, colors) {
109
+ try {
110
+ return onColor(resolveSolidColor(value, colors), colors.background, colors.foreground);
111
+ }
112
+ catch {
113
+ return colors.foreground;
114
+ }
115
+ }
91
116
  /** Solid controls start at base; interaction shades preserve its text choice. */
92
117
  export function solidColors(base, background, foreground) {
93
118
  const fill = resolveColorLevel(base, "base");
@@ -1,15 +1,18 @@
1
1
  import type { ComponentProps } from "react";
2
2
  import { Pressable as AriaPressable } from "react-aria-components";
3
3
  import type { MenuTriggerProps as AriaMenuTriggerProps } from "react-aria-components";
4
+ import { type PopoverContentProps } from "./popover.js";
4
5
  export type ContextMenuRootProps = Omit<AriaMenuTriggerProps, "trigger">;
5
6
  export declare function ContextMenuRoot(properties: ContextMenuRootProps): import("react").JSX.Element;
6
7
  export type ContextMenuTriggerProps = ComponentProps<typeof AriaPressable>;
7
8
  export declare const ContextMenuTrigger: import("react").ForwardRefExoticComponent<Omit<import("react-aria/Pressable").PressableProps & import("react").RefAttributes<import("@react-types/shared").FocusableElement>, "ref"> & import("react").RefAttributes<import("@react-types/shared").FocusableElement>>;
9
+ export type ContextMenuContentProps = Omit<PopoverContentProps, "isNonModal">;
10
+ export declare const ContextMenuContent: import("react").ForwardRefExoticComponent<ContextMenuContentProps & import("react").RefAttributes<HTMLElement>>;
8
11
  /** A context-requested overlay sharing the same Menu contract as DropdownMenu. */
9
12
  export declare const ContextMenu: typeof ContextMenuRoot & {
10
13
  Root: typeof ContextMenuRoot;
11
14
  Trigger: import("react").ForwardRefExoticComponent<Omit<import("react-aria/Pressable").PressableProps & import("react").RefAttributes<import("@react-types/shared").FocusableElement>, "ref"> & import("react").RefAttributes<import("@react-types/shared").FocusableElement>>;
12
- Content: import("react").ForwardRefExoticComponent<import("./popover.js").PopoverContentProps & import("react").RefAttributes<HTMLElement>>;
15
+ Content: import("react").ForwardRefExoticComponent<ContextMenuContentProps & import("react").RefAttributes<HTMLElement>>;
13
16
  Menu: import("./menu.js").MenuRootComponent & {
14
17
  Root: import("./menu.js").MenuRootComponent;
15
18
  Item: import("./menu.js").MenuItemComponent;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
3
- import { MenuTrigger as AriaMenuTrigger, Pressable as AriaPressable } from "react-aria-components";
2
+ import { forwardRef, useCallback, useContext, useEffect, useRef } from "react";
3
+ import { MenuTrigger as AriaMenuTrigger, OverlayTriggerStateContext, Pressable as AriaPressable } from "react-aria-components";
4
4
  import { Menu } from "./menu.js";
5
5
  import { PopoverContent } from "./popover.js";
6
6
  export function ContextMenuRoot(properties) {
@@ -9,10 +9,37 @@ export function ContextMenuRoot(properties) {
9
9
  export const ContextMenuTrigger = forwardRef(function ContextMenuTrigger(properties, ref) {
10
10
  return _jsx(AriaPressable, { ...properties, ref: ref });
11
11
  });
12
+ function setRef(ref, value) {
13
+ if (typeof ref === "function")
14
+ ref(value);
15
+ else if (ref)
16
+ ref.current = value;
17
+ }
18
+ export const ContextMenuContent = forwardRef(function ContextMenuContent(properties, forwardedRef) {
19
+ const state = useContext(OverlayTriggerStateContext);
20
+ const contentRef = useRef(null);
21
+ const ref = useCallback((element) => {
22
+ contentRef.current = element;
23
+ setRef(forwardedRef, element);
24
+ }, [forwardedRef]);
25
+ useEffect(() => {
26
+ if (!state?.isOpen)
27
+ return;
28
+ function dismiss(event) {
29
+ const target = event.target;
30
+ if (target instanceof Node && contentRef.current?.contains(target))
31
+ return;
32
+ state?.close();
33
+ }
34
+ document.addEventListener("pointerdown", dismiss, true);
35
+ return () => document.removeEventListener("pointerdown", dismiss, true);
36
+ }, [state]);
37
+ return _jsx(PopoverContent, { ...properties, ref: ref, isNonModal: true });
38
+ });
12
39
  /** A context-requested overlay sharing the same Menu contract as DropdownMenu. */
13
40
  export const ContextMenu = Object.assign(ContextMenuRoot, {
14
41
  Root: ContextMenuRoot,
15
42
  Trigger: ContextMenuTrigger,
16
- Content: PopoverContent,
43
+ Content: ContextMenuContent,
17
44
  Menu
18
45
  });
package/dist/control.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type RadiusProps } from "./radius.js";
3
3
  import { type ScaleLevel } from "./scale.js";
4
4
  import { solidColors } from "./color.js";
5
5
  import { type Color } from "./color.js";
6
+ import type { Transition } from "motion/react";
6
7
  /** Semantic Appearance colors accepted by interactive controls. */
7
8
  export type ControlColor = Color;
8
9
  export interface ControlProps {
@@ -22,10 +23,22 @@ export interface FieldProps {
22
23
  }
23
24
  /** Names owned by our contract, not a second set of primitive-specific aliases. */
24
25
  export type ControlOverrides = keyof ControlProps | keyof FieldProps | "children" | "isDisabled" | "isRequired" | "isInvalid";
25
- export declare const controlFontSizes: Readonly<Record<ScaleLevel, number>>;
26
+ export declare const controlFontSizes: Readonly<Record<ScaleLevel, CSSProperties["fontSize"]>>;
27
+ /** Shared interaction attenuation, derived consistently across control families. */
28
+ export declare const controlOpacity: Readonly<{
29
+ disabled: 0.46;
30
+ pending: 0.68;
31
+ secondary: 0.7;
32
+ placeholder: 0.6;
33
+ separator: 0.14;
34
+ }>;
35
+ /** Shared emphasis used by labels and control-owned headings. */
36
+ export declare const controlFontWeight = 600;
26
37
  type SolidColors = ReturnType<typeof solidColors>;
27
38
  export interface ControlTheme {
28
39
  readonly transition: CSSProperties;
40
+ readonly motionTransition: Transition;
41
+ readonly animations: boolean;
29
42
  readonly spacing: number;
30
43
  readonly foreground: string;
31
44
  readonly background: string;
@@ -38,7 +51,8 @@ export interface ControlTheme {
38
51
  readonly danger: SolidColors;
39
52
  };
40
53
  readonly radius: CSSProperties["borderRadius"];
41
- readonly fontSize: number;
54
+ readonly fontSize: CSSProperties["fontSize"];
55
+ readonly indicatorSize: number;
42
56
  readonly height: number;
43
57
  readonly gap: number;
44
58
  }