@phreshos/react-ui 0.1.22 → 0.1.24

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/README.md CHANGED
@@ -38,27 +38,30 @@ import { Button, Surface } from "@phreshos/react-ui"
38
38
  ```
39
39
 
40
40
  Without a provider, components use Core's `defaultAppearance` and reactively
41
- follow the browser color scheme. `AppearanceProvider` independently overrides
42
- either value for a subtree; omitted values inherit from the nearest provider.
41
+ follow the browser's complete visual preferences. `AppearanceProvider` can replace
42
+ Appearance or Preferences for a subtree; omitted values inherit from the nearest provider.
43
43
  React UI also exports that same canonical `defaultAppearance` value for callers
44
44
  that need it explicitly.
45
45
  See [Appearance](https://docs.phreshos.com/system/appearance) for the contract
46
46
  interpreted by the provider and components.
47
47
 
48
48
  ```tsx
49
- import { AppearanceProvider, Button } from "@phreshos/react-ui"
49
+ import { AppearanceProvider, Button, useBrowserPreferences } from "@phreshos/react-ui"
50
50
 
51
- <AppearanceProvider theme="dark">
52
- <Button>Dark subtree</Button>
53
- </AppearanceProvider>
51
+ function Example() {
52
+ const browser = useBrowserPreferences()
53
+
54
+ return <AppearanceProvider preferences={{ ...browser, theme: "dark" }}>
55
+ <Button>Dark subtree</Button>
56
+ </AppearanceProvider>
57
+ }
54
58
  ```
55
59
 
56
60
  `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `Radio`
57
61
  use the shared Surface implementation while retaining their native behavior. Color and material
58
62
  are independent inputs. `color` accepts an Appearance color and resting level such
59
- as `background:base` or `primary:soft`, or a direct CSS color. Surface and neutral
60
- surface hosts default to `background:base`; a component may select a semantic
61
- default required by its own behavior, such as `primary:base` for selection.
63
+ as `background:base` or `primary:soft`, or a direct CSS color. Surface defaults
64
+ to `background:base`; solid controls default to `default:base`.
62
65
  Hover and press derive shades of that fill. The resting fill chooses the
63
66
  higher-contrast text from Appearance's background and foreground once; interaction
64
67
  shades keep that choice.
@@ -128,8 +131,7 @@ Native properties and the forwarded ref target the outer Surface.
128
131
 
129
132
  Every input uses Appearance colors and the same five `size` levels as Button.
130
133
  `color` selects the fill independently from material. Input, Textarea, Select,
131
- and Button use `background:base` when it is omitted; selection indicators use
132
- `primary:base`. Invalid
134
+ and Button use `default:base` when it is omitted. Invalid
133
135
  fields use danger instead. Text fields share Surface's glass edge. Interaction shades
134
136
  derive from the base color's lightness, and text or selection marks use whichever
135
137
  Appearance background or foreground has higher contrast against the base fill.
@@ -142,14 +144,14 @@ Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
142
144
  Shared CSS transitions use `appearance.transaction` for colors and corner
143
145
  radius. The material fill and opacity values transition on the painted layers, not on the
144
146
  Surface host. Select menus combine a small placement-aware slide with an overlay
145
- opacity fade for entry and exit, using React Aria's animation lifecycle. Reduced-motion preferences make
147
+ opacity fade for entry and exit, using React Aria's animation lifecycle. Preferences with animations disabled make
146
148
  these changes immediate without changing the Appearance value.
147
149
  Blur, distortion, geometry, and Surface host opacity are not transitioned; gradients
148
150
  and structurally removed effects change directly rather than adding extra
149
151
  layers or keeping disabled effects alive.
150
152
 
151
153
  Motion animates toggle presses, selection marks, switch travel, the Select
152
- chevron, and Slider thumb feedback. Reduced-motion preferences remove spatial
154
+ chevron, and Slider thumb feedback. Preferences with animations disabled remove spatial
153
155
  feedback and make state transitions immediate. Slider values and native input
154
156
  behavior are never delayed by visual animation.
155
157
 
@@ -203,7 +205,7 @@ import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider }
203
205
 
204
206
  React Aria owns focus, keyboard, form, and selection behavior. Select's popup
205
207
  uses Surface defaults. Toggle indicators use Motion internally and respect
206
- reduced-motion preferences. The preview Program demonstrates each input's
208
+ animation preferences. The preview Program demonstrates each input's
207
209
  sizes, colors, state, and interaction without overriding its visual defaults.
208
210
 
209
211
  ## Development
@@ -1,4 +1,4 @@
1
- import { type Appearance, type Theme, type ThemedValue } from "@phreshos/core";
1
+ import { type Appearance, type DesktopPreferences as Preferences, type ThemedValue } from "@phreshos/core";
2
2
  export declare const AppearanceContext: import("react").Context<Readonly<{
3
3
  colors: ThemedValue<import("@phreshos/core").AppearanceColors>;
4
4
  spacing: number;
@@ -9,10 +9,15 @@ export declare const AppearanceContext: import("react").Context<Readonly<{
9
9
  signInWallpaper: ThemedValue<string | null>;
10
10
  desktopWallpaper: ThemedValue<string | null>;
11
11
  }>>;
12
- export declare const ThemeContext: import("react").Context<Theme | null>;
12
+ export declare const PreferencesContext: import("react").Context<Readonly<{
13
+ theme: import("@phreshos/core").Theme;
14
+ animations: boolean;
15
+ }> | null>;
13
16
  /** Returns the nearest unresolved Appearance, or Core's complete default. */
14
17
  export declare function useAppearance(): Appearance;
15
- /** Returns the nearest explicit Theme, or reactively follows the browser. */
16
- export declare function useTheme(): Theme;
18
+ /** Returns the nearest complete Preferences, or reactively follows the browser. */
19
+ export declare function usePreferences(): Preferences;
20
+ /** Reactively reads the browser's complete visual preferences. */
21
+ export declare function useBrowserPreferences(): Preferences;
17
22
  /** Selects the active branch of one complete ThemedValue. */
18
23
  export declare function useThemedValue<Value>(value: ThemedValue<Value>): Value;
@@ -1,37 +1,41 @@
1
- import { createContext, useContext, useSyncExternalStore } from "react";
1
+ import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
2
2
  import { defaultAppearance } from "@phreshos/core";
3
3
  export const AppearanceContext = createContext(defaultAppearance);
4
- export const ThemeContext = createContext(null);
4
+ export const PreferencesContext = createContext(null);
5
5
  /** Returns the nearest unresolved Appearance, or Core's complete default. */
6
6
  export function useAppearance() {
7
7
  return useContext(AppearanceContext);
8
8
  }
9
- /** Returns the nearest explicit Theme, or reactively follows the browser. */
10
- export function useTheme() {
11
- const theme = useContext(ThemeContext);
12
- const browserTheme = useBrowserTheme();
13
- return theme ?? browserTheme;
9
+ /** Returns the nearest complete Preferences, or reactively follows the browser. */
10
+ export function usePreferences() {
11
+ const preferences = useContext(PreferencesContext);
12
+ const browserPreferences = useBrowserPreferences();
13
+ return preferences ?? browserPreferences;
14
+ }
15
+ /** Reactively reads the browser's complete visual preferences. */
16
+ export function useBrowserPreferences() {
17
+ const theme = useMediaPreference(darkThemeQuery) ? "dark" : "light";
18
+ const animations = !useMediaPreference(reducedMotionQuery);
19
+ return useMemo(() => ({ theme, animations }), [animations, theme]);
14
20
  }
15
21
  /** Selects the active branch of one complete ThemedValue. */
16
22
  export function useThemedValue(value) {
17
- return value[useTheme()];
23
+ return value[usePreferences().theme];
18
24
  }
19
- function useBrowserTheme() {
20
- return useSyncExternalStore(subscribeBrowserTheme, browserTheme, serverTheme);
25
+ function useMediaPreference(query) {
26
+ return useSyncExternalStore(change => subscribeMediaPreference(query, change), () => browserMediaPreference(query), () => false);
21
27
  }
22
- function subscribeBrowserTheme(change) {
28
+ function subscribeMediaPreference(query, change) {
23
29
  if (typeof window === "undefined" || typeof window.matchMedia !== "function")
24
30
  return () => undefined;
25
- const preference = window.matchMedia(darkThemeQuery);
31
+ const preference = window.matchMedia(query);
26
32
  preference.addEventListener("change", change);
27
33
  return () => preference.removeEventListener("change", change);
28
34
  }
29
- function browserTheme() {
30
- return typeof window !== "undefined"
35
+ function browserMediaPreference(query) {
36
+ return Boolean(typeof window !== "undefined"
31
37
  && typeof window.matchMedia === "function"
32
- && window.matchMedia(darkThemeQuery).matches
33
- ? "dark"
34
- : "light";
38
+ && window.matchMedia(query).matches);
35
39
  }
36
- function serverTheme() { return "light"; }
37
40
  const darkThemeQuery = "(prefers-color-scheme: dark)";
41
+ const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
@@ -1,10 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
- import { type Appearance, type Theme } from "@phreshos/core";
3
- export { useAppearance, useTheme, useThemedValue } from "./appearance-context.js";
4
- /** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
5
- export declare function AppearanceProvider({ appearance, children, theme }: AppearanceProviderProps): import("react").JSX.Element;
2
+ import { type Appearance, type DesktopPreferences as Preferences } from "@phreshos/core";
3
+ export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
4
+ /** Optionally overrides Appearance and complete visual Preferences for a React subtree. */
5
+ export declare function AppearanceProvider({ appearance, children, preferences }: AppearanceProviderProps): import("react").JSX.Element;
6
6
  export interface AppearanceProviderProps {
7
7
  readonly appearance?: Appearance;
8
8
  readonly children: ReactNode;
9
- readonly theme?: Theme;
9
+ readonly preferences?: Preferences;
10
10
  }
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import {} from "@phreshos/core";
3
3
  import MotionStyle from "./motion-style.js";
4
- import { AppearanceContext, ThemeContext, useAppearance, useTheme } from "./appearance-context.js";
5
- export { useAppearance, useTheme, useThemedValue } from "./appearance-context.js";
6
- /** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
7
- export function AppearanceProvider({ appearance, children, theme }) {
4
+ import { AppearanceContext, PreferencesContext, useAppearance, usePreferences } from "./appearance-context.js";
5
+ export { useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-context.js";
6
+ /** Optionally overrides Appearance and complete visual Preferences for a React subtree. */
7
+ export function AppearanceProvider({ appearance, children, preferences }) {
8
8
  const inheritedAppearance = useAppearance();
9
- const inheritedTheme = useTheme();
9
+ const inheritedPreferences = usePreferences();
10
10
  const resolvedAppearance = appearance ?? inheritedAppearance;
11
- const resolvedTheme = theme ?? inheritedTheme;
12
- return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(ThemeContext.Provider, { value: resolvedTheme, children: [_jsx(MotionStyle, {}), children] }) });
11
+ const resolvedPreferences = preferences ?? inheritedPreferences;
12
+ return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(PreferencesContext.Provider, { value: resolvedPreferences, children: [_jsx(MotionStyle, {}), children] }) });
13
13
  }
package/dist/button.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { RadiusProps } from "./radius.js";
5
5
  import { type ControlColor } from "./control.js";
6
6
  import type { MaterialOverrides } from "./material-options.js";
7
7
  type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
8
- /** A semantic color from Appearance; omission uses background and foreground. */
8
+ /** A semantic color from Appearance; omission uses `default:base`. */
9
9
  export type ButtonColor = ControlColor;
10
10
  /** Properties accepted by the shared interactive button. */
11
11
  export interface ButtonProps extends NativeButtonProps, RadiusProps, MaterialOverrides {
package/dist/button.js CHANGED
@@ -19,7 +19,7 @@ export const Button = forwardRef(function Button({ children, color, disabled = f
19
19
  });
20
20
  function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pending, size, style }) {
21
21
  const fontSize = controlFontSizes[size];
22
- const { spacing, foreground } = theme;
22
+ const { spacing, focusColor } = theme;
23
23
  const height = Math.max(24, 24 + spacing);
24
24
  const interactive = !disabled && !pending;
25
25
  const paint = buttonPaint(theme, interactive, isHovered, isPressed);
@@ -40,8 +40,8 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
40
40
  gap: Math.max(4, spacing / 2),
41
41
  border: "none",
42
42
  borderRadius: theme.radius,
43
- outline: isFocusVisible ? `2px solid ${foreground}` : "none",
44
- outlineOffset: 2,
43
+ outline: isFocusVisible ? `1px solid ${focusColor}` : "none",
44
+ outlineOffset: 1,
45
45
  ...paint,
46
46
  opacity: disabled ? 0.46 : pending ? 0.68 : 1,
47
47
  cursor: disabled ? "not-allowed" : pending ? "progress" : "pointer",
@@ -57,6 +57,6 @@ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pe
57
57
  };
58
58
  }
59
59
  function buttonPaint(theme, interactive, hovered, pressed) {
60
- const paints = theme.colored ? theme.paints.palette : theme.paints.neutral;
60
+ const paints = theme.paints.palette;
61
61
  return interactive && pressed ? paints.pressed : interactive && hovered ? paints.hover : paints.rest;
62
62
  }
package/dist/control.d.ts CHANGED
@@ -30,7 +30,7 @@ export interface ControlTheme {
30
30
  readonly foreground: string;
31
31
  readonly background: string;
32
32
  readonly tint: string;
33
- readonly colored: boolean;
33
+ readonly focusColor: string;
34
34
  readonly danger: string;
35
35
  readonly paints: {
36
36
  readonly palette: SolidColors;
package/dist/control.js CHANGED
@@ -4,7 +4,7 @@ import { FieldError, Label, Text } from "react-aria-components";
4
4
  import { useAppearance, useThemedValue } from "./appearance-provider.js";
5
5
  import { resolveRadius } from "./radius.js";
6
6
  import { scale } from "./scale.js";
7
- import { solidColors } from "./color.js";
7
+ import { colorOpacity, solidColors } from "./color.js";
8
8
  import { useResolveSolidColor } from "./color.js";
9
9
  import { useVisualTransition } from "./motion-style.js";
10
10
  export const controlFontSizes = Object.freeze({
@@ -21,20 +21,22 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
21
21
  const colors = useThemedValue(appearance.colors);
22
22
  const foreground = colors.foreground;
23
23
  const background = colors.background;
24
- const tint = useResolveSolidColor(color ?? "primary:base");
24
+ const tint = useResolveSolidColor(color ?? "default:base");
25
+ const neutral = useResolveSolidColor("default:base");
26
+ const focus = useResolveSolidColor("warning:base");
25
27
  const danger = useResolveSolidColor("danger:base");
26
28
  const paints = useMemo(() => ({
27
29
  palette: solidColors(tint, background, foreground),
28
- neutral: solidColors(background, background, foreground),
30
+ neutral: solidColors(neutral, background, foreground),
29
31
  danger: solidColors(danger, background, foreground)
30
- }), [tint, background, foreground, danger]);
32
+ }), [tint, neutral, background, foreground, danger]);
31
33
  return {
32
34
  transition,
33
35
  spacing,
34
36
  foreground,
35
37
  background,
36
38
  tint,
37
- colored: color !== undefined,
39
+ focusColor: colorOpacity(focus, 0.2),
38
40
  danger,
39
41
  paints,
40
42
  radius: resolveRadius(radius, appearance),
@@ -58,7 +60,7 @@ export function fieldStyle(theme, disabled = false, style) {
58
60
  };
59
61
  }
60
62
  export function controlPaint(theme, focused, invalid, hovered = false) {
61
- const paints = invalid ? theme.paints.danger : theme.colored ? theme.paints.palette : theme.paints.neutral;
63
+ const paints = invalid ? theme.paints.danger : theme.paints.palette;
62
64
  return focused ? paints.pressed : hovered ? paints.hover : paints.rest;
63
65
  }
64
66
  export function controlStyle(theme, focused, invalid, hovered = false, focusVisible = focused) {
@@ -74,8 +76,8 @@ export function controlStyle(theme, focused, invalid, hovered = false, focusVisi
74
76
  paddingInline: Math.max(8, theme.spacing),
75
77
  border: 0,
76
78
  borderRadius: theme.radius,
77
- outline: focusVisible ? `2px solid ${theme.foreground}` : "none",
78
- outlineOffset: 2,
79
+ outline: focusVisible ? `1px solid ${theme.focusColor}` : "none",
80
+ outlineOffset: 1,
79
81
  ...paint,
80
82
  caretColor: paint.color,
81
83
  font: "inherit",
package/dist/main.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
7
  export { defaultAppearance } from "@phreshos/core";
8
- export { AppearanceProvider, useAppearance, useThemedValue, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
8
+ export { AppearanceProvider, useAppearance, useBrowserPreferences, usePreferences, useThemedValue, type AppearanceProviderProps } from "./appearance-provider.js";
9
+ export type { DesktopPreferences as Preferences } from "@phreshos/core";
9
10
  export { Flex, type FlexProps } from "./flex.js";
10
11
  export { Grid, type GridProps } from "./grid.js";
11
12
  export { Surface, type MaterialOptions, type SurfaceComponent, type SurfaceHost, type SurfaceHostProps, type SurfaceOwnProps, type SurfaceProps } from "./surface.js";
package/dist/main.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
7
  export { defaultAppearance } from "@phreshos/core";
8
- export { AppearanceProvider, useAppearance, useThemedValue, useTheme } from "./appearance-provider.js";
8
+ export { AppearanceProvider, useAppearance, useBrowserPreferences, usePreferences, useThemedValue } from "./appearance-provider.js";
9
9
  export { Flex } from "./flex.js";
10
10
  export { Grid } from "./grid.js";
11
11
  export { Surface } from "./surface.js";
@@ -5,7 +5,7 @@ export declare function useVisualTransition(): CSSProperties;
5
5
  /** CSS timing for SVG paint owned by a Material. */
6
6
  export declare function usePaintTransition(): CSSProperties;
7
7
  /** Motion timing for interactive geometry owned by React UI. */
8
- export declare function useControlTransition(reduced?: boolean): Transition;
8
+ export declare function useControlTransition(): Transition;
9
9
  /** Per-overlay variables consumed by the shared entrance keyframes. */
10
10
  export declare function useOverlayTransition(): CSSProperties;
11
11
  export declare const overlayMotionClass = "phreshos-ui-overlay";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { defaultAppearance } from "@phreshos/core";
3
- import { useAppearance } from "./appearance-context.js";
3
+ import { useAppearance, usePreferences } from "./appearance-context.js";
4
4
  const visualProperties = "background-color, color, border-color, border-radius";
5
5
  const paintProperties = "fill, stroke, opacity";
6
6
  /** CSS timing for values painted directly by React UI. */
@@ -12,27 +12,30 @@ export function usePaintTransition() {
12
12
  return { ...useTransitionTiming(), transitionProperty: paintProperties };
13
13
  }
14
14
  /** Motion timing for interactive geometry owned by React UI. */
15
- export function useControlTransition(reduced = false) {
15
+ export function useControlTransition() {
16
16
  const { duration, easing } = useAppearance().transaction;
17
+ const { animations } = usePreferences();
17
18
  return {
18
19
  type: "tween",
19
- duration: reduced ? 0 : duration / 1_000,
20
+ duration: animations ? duration / 1_000 : 0,
20
21
  ease: motionEasing(easing)
21
22
  };
22
23
  }
23
24
  /** Per-overlay variables consumed by the shared entrance keyframes. */
24
25
  export function useOverlayTransition() {
25
26
  const { duration, easing } = useAppearance().transaction;
27
+ const { animations } = usePreferences();
26
28
  return {
27
- "--phreshos-ui-motion-duration": `${duration}ms`,
29
+ "--phreshos-ui-motion-duration": `${animations ? duration : 0}ms`,
28
30
  "--phreshos-ui-motion-easing": cssEasing(easing)
29
31
  };
30
32
  }
31
33
  export const overlayMotionClass = "phreshos-ui-overlay";
32
34
  export function useTransitionTiming() {
33
35
  const { duration, easing } = useAppearance().transaction;
36
+ const { animations } = usePreferences();
34
37
  return {
35
- transitionDuration: `${duration}ms`,
38
+ transitionDuration: `${animations ? duration : 0}ms`,
36
39
  transitionTimingFunction: cssEasing(easing)
37
40
  };
38
41
  }
@@ -73,10 +76,6 @@ const stylesheet = `
73
76
  from { opacity: 0; translate: var(--phreshos-ui-overlay-x) var(--phreshos-ui-overlay-y); }
74
77
  to { opacity: 1; translate: 0 0; }
75
78
  }
76
- @media (prefers-reduced-motion: reduce) {
77
- :root { --phreshos-ui-motion-duration: 0ms; }
78
- .phreshos-ui-overlay[data-entering], .phreshos-ui-overlay[data-exiting] { animation: none; }
79
- }
80
79
  `;
81
80
  /** React hoists and deduplicates this stylesheet, including portals. */
82
81
  export default function MotionStyle() {
package/dist/select.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
- import { motion, useReducedMotion } from "motion/react";
3
+ import { motion } from "motion/react";
4
4
  import { Select as AriaSelect, Button, SelectValue, Popover, ListBox, ListBoxItem } from "react-aria-components";
5
5
  import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
6
6
  import { SurfaceButton } from "./control-surface.js";
@@ -10,8 +10,7 @@ import { overlayMotionClass, useControlTransition, useOverlayTransition } from "
10
10
  /** Single selection from string-valued options; keyboard navigation and typeahead stay native to React Aria. */
11
11
  export const Select = forwardRef(function Select({ label, description, errorMessage, disabled, required, invalid, options, value, defaultValue, onChange, size, color, radius, style, material, ...properties }, ref) {
12
12
  const theme = useControlTheme({ size, color, radius });
13
- const reduced = useReducedMotion();
14
- const transition = useControlTransition(Boolean(reduced));
13
+ const transition = useControlTransition();
15
14
  const overlayTransition = useOverlayTransition();
16
15
  return _jsx(AriaSelect, { ...properties, ref: ref, value: value, defaultValue: defaultValue, onChange: key => onChange?.(key == null ? null : String(key)), disabledKeys: options.filter(option => option.disabled).map(option => option.value), isDisabled: disabled, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: state => _jsxs(_Fragment, { children: [_jsx(FieldLabel, { label: label }), _jsxs(Button, { render: (native, button) => _jsx(SurfaceButton, { native: native, material: material, paint: controlPaint(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered) }), style: button => ({
17
16
  ...controlStyle(theme, state.isOpen || button.isFocused, state.isInvalid, button.isHovered, button.isFocusVisible),
@@ -22,7 +21,7 @@ export const Select = forwardRef(function Select({ label, description, errorMess
22
21
  display: "flex", alignItems: "center", justifyContent: "space-between", gap: theme.gap,
23
22
  minHeight: theme.height, paddingInline: Math.max(8, theme.spacing), boxSizing: "border-box",
24
23
  borderRadius: theme.radius,
25
- outline: item.isFocused && !item.isSelected ? `2px solid ${theme.foreground}` : "none", outlineOffset: -2,
24
+ outline: item.isFocused && !item.isSelected ? `1px solid ${theme.focusColor}` : "none", outlineOffset: 1,
26
25
  cursor: item.isDisabled ? "not-allowed" : "pointer",
27
26
  opacity: item.isDisabled ? 0.46 : 1,
28
27
  ...(item.isSelected ? item.isFocused ? theme.paints.palette.hover : theme.paints.palette.rest
package/dist/slider.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useId } from "react";
3
- import { motion, useReducedMotion } from "motion/react";
3
+ import { motion } from "motion/react";
4
4
  import { Slider as AriaSlider, SliderTrack, SliderThumb, SliderFill, SliderOutput } from "react-aria-components";
5
5
  import { FieldLabel, fieldStyle, useControlTheme } from "./control.js";
6
6
  import { useControlTransition } from "./motion-style.js";
7
+ import { usePreferences } from "./appearance-provider.js";
7
8
  /** One numeric value with pointer, touch, keyboard, and form support. */
8
9
  export const Slider = forwardRef(function Slider({ label, description, name, disabled, size, color, style, orientation = "horizontal", ...properties }, ref) {
9
10
  const theme = useControlTheme({ size, color });
10
11
  const descriptionId = useId();
11
12
  const vertical = orientation === "vertical";
12
13
  const diameter = theme.fontSize + 6;
13
- const reduced = useReducedMotion();
14
- const transition = useControlTransition(Boolean(reduced));
14
+ const { animations } = usePreferences();
15
+ const transition = useControlTransition();
15
16
  const rail = Math.max(4, Math.round(diameter / 3));
16
17
  return _jsxs(AriaSlider, { ...properties, ref: ref, isDisabled: disabled, orientation: orientation, style: fieldStyle(theme, disabled, style), "aria-describedby": [properties["aria-describedby"], description != null ? descriptionId : null].filter(Boolean).join(" ") || undefined, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: theme.gap }, children: [_jsx(FieldLabel, { label: label }), _jsx(SliderOutput, { style: { fontVariantNumeric: "tabular-nums", opacity: 0.7 } })] }), _jsxs(SliderTrack, { style: {
17
18
  position: "relative", touchAction: "none",
@@ -28,9 +29,9 @@ export const Slider = forwardRef(function Slider({ label, description, name, dis
28
29
  } }), _jsx(SliderThumb, { name: name, style: state => ({
29
30
  width: diameter, height: diameter, borderRadius: "50%", boxSizing: "border-box",
30
31
  ...(vertical ? { left: "50%" } : { top: "50%" }),
31
- outline: state.isFocusVisible ? `2px solid ${theme.foreground}` : "none", outlineOffset: 2,
32
+ outline: state.isFocusVisible ? `1px solid ${theme.focusColor}` : "none", outlineOffset: 1,
32
33
  cursor: state.isDisabled ? "not-allowed" : state.isDragging ? "grabbing" : "grab"
33
- }), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: reduced || state.isDisabled ? 1 : state.isDragging ? 0.92 : state.isHovered ? 1.08 : 1 }, transition: transition, style: { ...theme.transition, position: "absolute", inset: 0, borderRadius: "inherit", pointerEvents: "none",
34
+ }), children: state => _jsx(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: !animations || state.isDisabled ? 1 : state.isDragging ? 0.92 : state.isHovered ? 1.08 : 1 }, transition: transition, style: { ...theme.transition, position: "absolute", inset: 0, borderRadius: "inherit", pointerEvents: "none",
34
35
  background: (state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).color,
35
36
  border: `3px solid ${(state.isDragging ? theme.paints.palette.pressed : state.isHovered ? theme.paints.palette.hover : theme.paints.palette.rest).background}`,
36
37
  boxSizing: "border-box" } }) })] }), description != null && _jsx("span", { id: descriptionId, style: { fontSize: "0.92em", opacity: 0.7 }, children: description })] });
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { motion, useReducedMotion } from "motion/react";
2
+ import { motion } from "motion/react";
3
3
  import { useLocale } from "react-aria-components";
4
4
  import { useControlTransition } from "./motion-style.js";
5
+ import { usePreferences } from "./appearance-provider.js";
5
6
  import { Surface } from "./surface.js";
6
7
  /** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
7
8
  export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
8
- const reduced = useReducedMotion();
9
- const transition = useControlTransition(Boolean(reduced));
9
+ const { animations } = usePreferences();
10
+ const transition = useControlTransition();
10
11
  const { direction } = useLocale();
11
12
  const diameter = theme.fontSize + 6;
12
13
  const active = selected || indeterminate;
@@ -14,9 +15,9 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
14
15
  const paint = pressed ? paints.pressed : hovered ? paints.hover : paints.rest;
15
16
  const radius = kind === "checkbox" ? Math.min(diameter / 4, typeof theme.radius === "number" ? theme.radius : diameter / 4) : diameter;
16
17
  const switching = kind === "switch";
17
- const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
18
+ const thumbWidth = diameter - 6 + (pressed && animations ? 3 : 0);
18
19
  const travel = (diameter * 2 - thumbWidth) / 2 - 3;
19
- return _jsx(Surface, { as: motion.span, material: options, color: paint.background, "aria-hidden": "true", initial: false, animate: { scale: reduced || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: transition, style: {
20
+ return _jsx(Surface, { as: motion.span, material: options, color: paint.background, "aria-hidden": "true", initial: false, animate: { scale: !animations || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: transition, style: {
20
21
  color: paint.color,
21
22
  ...theme.transition,
22
23
  display: "inline-grid",
@@ -28,8 +29,8 @@ export function ToggleIndicator({ kind, selected, indeterminate = false, focused
28
29
  height: diameter,
29
30
  borderRadius: radius,
30
31
  border: "none",
31
- outline: focused ? `2px solid ${theme.foreground}` : "none",
32
- outlineOffset: 2,
32
+ outline: focused ? `1px solid ${theme.focusColor}` : "none",
33
+ outlineOffset: 1,
33
34
  }, children: switching ? _jsx(motion.span, { initial: false, animate: { width: thumbWidth, x: (selected ? travel : -travel) * (direction === "rtl" ? -1 : 1) }, transition: transition, style: {
34
35
  ...theme.transition,
35
36
  height: diameter - 6, borderRadius: diameter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@base-ui/react": "^1.7.0",
59
- "@phreshos/core": "^0.1.49",
59
+ "@phreshos/core": "^0.1.51",
60
60
  "colorjs.io": "^0.7.1",
61
61
  "motion": "^13.2.0",
62
62
  "react-aria-components": "^1.20.0"