@phreshos/react-ui 0.1.15 → 0.1.17

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 (53) hide show
  1. package/README.md +200 -35
  2. package/dist/appearance-provider.d.ts +6 -8
  3. package/dist/appearance-provider.js +34 -19
  4. package/dist/button.d.ts +11 -5
  5. package/dist/button.js +28 -36
  6. package/dist/checkbox.d.ts +11 -0
  7. package/dist/checkbox.js +10 -0
  8. package/dist/color.d.ts +40 -0
  9. package/dist/color.js +99 -4
  10. package/dist/control-material.d.ts +20 -0
  11. package/dist/control-material.js +14 -0
  12. package/dist/control.d.ts +55 -0
  13. package/dist/control.js +87 -0
  14. package/dist/document-scrollbars.js +1 -1
  15. package/dist/field-style.d.ts +3 -0
  16. package/dist/field-style.js +11 -0
  17. package/dist/flex.js +2 -2
  18. package/dist/grid.js +2 -2
  19. package/dist/input.d.ts +4 -0
  20. package/dist/input.js +11 -0
  21. package/dist/main.d.ts +14 -2
  22. package/dist/main.js +11 -0
  23. package/dist/material-options.d.ts +20 -0
  24. package/dist/material-options.js +22 -0
  25. package/dist/material-paint.d.ts +11 -0
  26. package/dist/material-paint.js +76 -0
  27. package/dist/material.d.ts +27 -0
  28. package/dist/material.js +45 -0
  29. package/dist/motion-style.d.ts +21 -0
  30. package/dist/motion-style.js +45 -0
  31. package/dist/panel.d.ts +9 -0
  32. package/dist/panel.js +27 -0
  33. package/dist/radio.d.ts +14 -0
  34. package/dist/radio.js +17 -0
  35. package/dist/select.d.ts +17 -0
  36. package/dist/select.js +28 -0
  37. package/dist/slider.d.ts +7 -0
  38. package/dist/slider.js +36 -0
  39. package/dist/surface-edge.d.ts +7 -0
  40. package/dist/surface-edge.js +59 -0
  41. package/dist/surface.d.ts +7 -44
  42. package/dist/surface.js +18 -98
  43. package/dist/switch.d.ts +10 -0
  44. package/dist/switch.js +10 -0
  45. package/dist/text-control.d.ts +9 -0
  46. package/dist/text-control.js +0 -0
  47. package/dist/textarea.d.ts +6 -0
  48. package/dist/textarea.js +17 -0
  49. package/dist/toggle-indicator.d.ts +16 -0
  50. package/dist/toggle-indicator.js +51 -0
  51. package/package.json +4 -3
  52. package/dist/surface-material.d.ts +0 -13
  53. package/dist/surface-material.js +0 -102
package/README.md CHANGED
@@ -2,8 +2,19 @@
2
2
 
3
3
  Environment-neutral React components and the visual language of PhreshOS.
4
4
 
5
- React UI interprets Core Appearance and Theme contracts. It does not depend on a
6
- Client or Server runtime and does not own authoritative application state.
5
+ [Appearance](https://docs.phreshos.com/system/appearance) ·
6
+ [React SDK](https://docs.phreshos.com/sdks/react) ·
7
+ [Source](https://github.com/PhreshOS/react-ui)
8
+
9
+ ## Role
10
+
11
+ React UI interprets Core Appearance and Theme contracts as reusable visual
12
+ primitives. The Desktop and Programs compose those primitives instead of
13
+ reimplementing material, spacing, color, radius, or interaction behavior.
14
+
15
+ The package does not depend on a Client or Server runtime and does not own
16
+ authoritative application state. React owns runtime-neutral state adaptation;
17
+ applications own composition.
7
18
 
8
19
  ## Installation
9
20
 
@@ -14,45 +25,192 @@ Client or Server runtime and does not own authoritative application state.
14
25
  | Bun | `bun add @phreshos/react-ui` |
15
26
  | Yarn | `yarn add @phreshos/react-ui` |
16
27
 
17
- `@phreshos/core`, React, and React DOM are peer dependencies.
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.
18
31
 
19
- ## Appearance
32
+ ```tsx
33
+ import { Button, Surface } from "@phreshos/react-ui"
34
+
35
+ <Surface>
36
+ <Button>Continue</Button>
37
+ </Surface>
38
+ ```
39
+
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.
43
+ React UI also exports that same canonical `defaultAppearance` value for callers
44
+ that need it explicitly.
45
+ See [Appearance](https://docs.phreshos.com/system/appearance) for the contract
46
+ interpreted by the provider and components.
20
47
 
21
48
  ```tsx
22
- import { standardAppearance } from "@phreshos/core"
23
- import {
24
- AppearanceProvider,
25
- Button,
26
- Surface,
27
- } from "@phreshos/react-ui"
28
-
29
- <AppearanceProvider appearance={standardAppearance} theme="light">
30
- <Surface>
31
- <Button>Continue</Button>
32
- </Surface>
49
+ import { AppearanceProvider, Button } from "@phreshos/react-ui"
50
+
51
+ <AppearanceProvider theme="dark">
52
+ <Button>Dark subtree</Button>
33
53
  </AppearanceProvider>
34
54
  ```
35
55
 
36
- `AppearanceProvider` provides the unresolved Appearance and one effective
37
- `"light" | "dark"` Theme. `useAppearance()` reads the unresolved value,
38
- `useTheme()` reads the effective mode, and `useResolveTheme()` resolves one
39
- themed property where it is consumed.
56
+ `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `Radio`
57
+ host the shared Material while retaining their native behavior. Color and material
58
+ are independent inputs. `color` accepts an Appearance color and resting level such
59
+ as `background:base` or `primary:soft`, or a direct CSS color. Material 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.
62
+ Hover and press derive shades of that fill. The resting fill chooses the
63
+ higher-contrast text from Appearance's background and foreground once; interaction
64
+ shades keep that choice.
65
+ `size` accepts `xsmall`, `small`, `medium` (default), `large`, or `xlarge`;
66
+ spacing and radius follow Appearance. `disabled` prevents activation and focus;
67
+ `pending` prevents activation while retaining focus.
40
68
 
41
- The provider also applies the shared document scrollbar treatment without
42
- adding a rendered container.
69
+ ```tsx
70
+ <Button>Cancel</Button>
71
+ <Button color="primary:base" onPress={save}>Save</Button>
72
+ <Button color="danger:soft" size="small">Delete</Button>
73
+ ```
74
+
75
+ `Material` is the raw visual substance: paint, opacity, frost, refraction, and
76
+ grain. It fills the geometry of its nearest positioned container,
77
+ inherits that container's radius, and adds no content, layout, interaction, or
78
+ shadow. The container must establish its own geometry and isolation.
79
+
80
+ ```tsx
81
+ <div style={{ position: "relative", isolation: "isolate", width: 240, height: 120, borderRadius: 18 }}>
82
+ <Material color="background:soft" material={{ opacity: 0.4 }} />
83
+ </div>
84
+ ```
43
85
 
44
- ## Components
86
+ `Surface` is the standard geometric host for a `Material`. It establishes the
87
+ required positioning and isolation on a `div`, resolves radius, renders Material,
88
+ and then renders its content. It never creates or consumes a shadow. Shadow remains
89
+ an independent visual concern. `radius` accepts a size level, a number in pixels,
90
+ or a CSS radius and defaults to `medium`.
45
91
 
46
- The package owns the reusable visual primitives used across the desktop and
47
- official Programs:
92
+ ```tsx
93
+ <Surface color="background:soft" radius="large">Derived values</Surface>
94
+ <Surface color="#345678" radius={18}>Direct values</Surface>
95
+ ```
48
96
 
49
- - `Surface` and `SurfaceMaterial`
50
- - `Button`
51
- - `Flex` and `Grid`
52
- - spacing, scale, radius, color, and icon utilities
97
+ `MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
98
+ `distortion`, and `saturation`. It contains no color. `MaterialProps` keeps
99
+ `color` beside `material?: MaterialOptions`, so paint selection never becomes a
100
+ physical-material setting. Omitted material values follow `appearance.material`.
101
+ Effect options accept a scale level or a direct number; opacity affects material
102
+ only, never the host's children.
53
103
 
54
- These primitives form one visual language. Desktop and Program Views compose
55
- them rather than reimplementing their material or layout behavior.
104
+ Radius belongs to host geometry, while Surface's border belongs to its geometric
105
+ boundary. Material may determine the border treatment, but it neither owns nor
106
+ renders that border.
107
+
108
+ Material-bearing controls expose the same separate `color` and `material` props.
109
+ For text fields and Select, `material` targets the field or trigger; for Checkbox,
110
+ Switch, and Radio, it targets the indicator. RadioGroup supplies material defaults
111
+ to its options, and a Radio can override them.
112
+
113
+ ```tsx
114
+ <Button color="primary:base" material={{ opacity: 0.6, backdrop: 0 }}>Save</Button>
115
+ <Input label="Name" radius="large" material={{ grain: "small" }} />
116
+ ```
117
+
118
+ `Panel` composes an outer `Surface`, an optional header, and an inset content
119
+ `Surface`. Both materials retain Surface defaults; the content inset follows
120
+ Appearance spacing. Positioning, modality, and lifecycle belong to the caller.
121
+
122
+ ```tsx
123
+ import { Panel } from "@phreshos/react-ui"
124
+
125
+ <Panel header={<h2>Title</h2>} contentProps={{ style: { padding: 16 } }}>
126
+ Content
127
+ </Panel>
128
+ ```
129
+
130
+ Native properties and the forwarded ref target the outer Surface.
131
+ `contentProps` targets the inner Surface; `children` supplies its content.
132
+
133
+ ## Inputs
134
+
135
+ Every input uses Appearance colors and the same five `size` levels as Button.
136
+ `color` selects the fill independently from material. Input, Textarea, Select,
137
+ and Button use `background:base` when it is omitted; selection indicators use
138
+ `primary:base`. Invalid
139
+ fields use danger instead. Text fields share Surface's glass edge. Interaction shades
140
+ derive from the base color's lightness, and text or selection marks use whichever
141
+ Appearance background or foreground has higher contrast against the base fill.
142
+ That text or mark color stays unchanged across interaction shades.
143
+ These are component-owned derivations, not additional Appearance settings.
144
+ Theme names never imply particular colors. No component requires a Client or
145
+ Server SDK.
146
+
147
+ Fields distinguish hover, pointer focus, keyboard focus, and invalid state.
148
+ Shared CSS transitions use a fixed 120ms ease-out timing for colors and corner
149
+ radius. Material fill and opacity transition on the painted layers, not on the
150
+ Surface host. Select menus combine a small placement-aware slide with an overlay
151
+ opacity fade for entry and exit, using React Aria's animation lifecycle. Reduced-motion preferences make
152
+ these changes immediate. No Appearance configuration is added for motion yet.
153
+ Blur, distortion, geometry, and Surface host opacity are not transitioned; gradients
154
+ and structurally removed effects change directly rather than adding extra
155
+ layers or keeping disabled effects alive.
156
+
157
+ Motion animates toggle presses, selection marks, switch travel, the Select
158
+ chevron, and Slider thumb feedback. Reduced-motion preferences remove spatial
159
+ feedback and make state transitions immediate. Slider values and native input
160
+ behavior are never delayed by visual animation.
161
+
162
+ | Component | Value contract | Purpose |
163
+ | --- | --- | --- |
164
+ | `Input` | `value`, `defaultValue`, `onChange(string)` | Single-line text; supports text, email, password, search, URL, and telephone types |
165
+ | `Textarea` | `value`, `defaultValue`, `onChange(string)` | Multiline text; four rows by default, vertically resizable |
166
+ | `Checkbox` | `checked`, `defaultChecked`, `onChange(boolean)` | Independent selection; `indeterminate` represents a mixed state |
167
+ | `Switch` | `checked`, `defaultChecked`, `onChange(boolean)` | An on/off setting |
168
+ | `RadioGroup` / `Radio` | Group `value`, `defaultValue`, `onChange(string)`; Radio `value` | One exclusive choice; vertical by default, optionally horizontal |
169
+ | `Select` | `value: string \| null`, `defaultValue`, `onChange(string \| null)` | One choice from `options: { value, label, disabled? }[]` |
170
+ | `Slider` | `value`, `defaultValue`, `onChange(number)` | One numeric value; `minValue`, `maxValue`, `step`, and `onChangeEnd`; horizontal by default |
171
+
172
+ Use `label` for visible labels, or `aria-label` / `aria-labelledby` for an
173
+ accessible name without visible text. `description` provides associated help.
174
+ `disabled` prevents interaction and removes the control from keyboard focus.
175
+ `name` participates in native form submission. Controlled values remain owned
176
+ by the caller; omit them and use defaults for internal state and form reset.
177
+
178
+ Text fields, Checkbox, Switch, and RadioGroup also support `readOnly`: the
179
+ value cannot change, but the control remains focusable. These fields and
180
+ Select support `required`, `invalid`, `errorMessage`, and React Aria's native
181
+ or ARIA validation behavior. A Radio's selection and validation belong to its
182
+ group. Slider represents a bounded number rather than a required text or
183
+ choice field, and has no read-only or validation-error mode. Select has no
184
+ read-only mode; disable it when selection must be unavailable.
185
+
186
+ `Input`, `Textarea`, and `Select` accept `radius`. Other inputs retain their
187
+ intrinsic indicator shapes. `className` and `style` address the field's root;
188
+ default visual properties remain component-owned. Input and Textarea refs
189
+ target their native text controls; other refs target the root div. Checkbox,
190
+ Switch, and Radio additionally accept `inputRef` for their native input.
191
+
192
+ ```tsx
193
+ import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider } from "@phreshos/react-ui"
194
+
195
+ <Input label="Name" name="name" required />
196
+ <Textarea label="Description" name="description" />
197
+ <Checkbox label="Remember this choice" name="remember" />
198
+ <Switch label="Notifications" name="notifications" defaultChecked />
199
+ <RadioGroup label="Layout" name="layout" defaultValue="grid">
200
+ <Radio label="Grid" value="grid" />
201
+ <Radio label="List" value="list" />
202
+ </RadioGroup>
203
+ <Select label="Sort" name="sort" options={[
204
+ { value: "name", label: "Name" },
205
+ { value: "date", label: "Date" }
206
+ ]} />
207
+ <Slider label="Volume" name="volume" defaultValue={50} minValue={0} maxValue={100} step={1} />
208
+ ```
209
+
210
+ React Aria owns focus, keyboard, form, and selection behavior. Select's popup
211
+ uses Surface defaults. Toggle indicators use Motion internally and respect
212
+ reduced-motion preferences. The preview Program demonstrates each input's
213
+ sizes, colors, state, and interaction without overriding its visual defaults.
56
214
 
57
215
  ## Development
58
216
 
@@ -62,13 +220,20 @@ bun run verify
62
220
  ```
63
221
 
64
222
  `verify` checks the contracts, tests the components, builds the package, and
65
- validates its public artifact.
223
+ validates its published shape.
66
224
 
67
- ## Repository boundary
225
+ ## Related repositories
68
226
 
69
- This repository owns visual interpretation and reusable React components. Core
70
- owns Appearance contracts, React owns runtime-neutral state adaptation, and
71
- applications own composition.
227
+ - [`@phreshos/core`](https://github.com/PhreshOS/core) owns Appearance, Theme,
228
+ and the shared values interpreted here.
229
+ - [`@phreshos/react`](https://github.com/PhreshOS/react) owns runtime-neutral
230
+ React state adaptation.
231
+ - [PhreshOS System](https://github.com/PhreshOS/system) composes the visual
232
+ language into the Desktop.
233
+ - [Settings](https://github.com/PhreshOS/settings-program) presents owner-facing
234
+ Appearance controls.
235
+
236
+ ## Contributing
72
237
 
73
238
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository workflow and
74
239
  [SECURITY.md](SECURITY.md) for private vulnerability reporting.
@@ -1,17 +1,15 @@
1
1
  import type { ReactNode } from "react";
2
- import type { Appearance, Theme, ThemedValue } from "@phreshos/core";
3
- /** Provides unresolved Appearance and one effective Theme to a React subtree. */
2
+ import { type Appearance, type Theme, type ThemedValue } from "@phreshos/core";
3
+ /** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
4
4
  export declare function AppearanceProvider({ appearance, children, theme }: AppearanceProviderProps): import("react").JSX.Element;
5
- /** Returns the complete unresolved Appearance supplied by the nearest provider. */
5
+ /** Returns the nearest unresolved Appearance, or Core's complete default. */
6
6
  export declare function useAppearance(): Appearance;
7
- /** Returns the effective Theme supplied by the nearest provider. */
7
+ /** Returns the nearest explicit Theme, or reactively follows the browser. */
8
8
  export declare function useTheme(): Theme;
9
9
  /** Resolves one themed value only where it is consumed. */
10
10
  export declare function useResolveTheme<Value, DarkValue extends Value = never>(value: ThemedValue<Value, DarkValue>): Exclude<Value, undefined>;
11
- /** Internal optional read for primitives that also accept direct values. */
12
- export declare function useAppearanceIfAvailable(): Appearance | null;
13
11
  export interface AppearanceProviderProps {
14
- readonly appearance: Appearance;
12
+ readonly appearance?: Appearance;
15
13
  readonly children: ReactNode;
16
- readonly theme: Theme;
14
+ readonly theme?: Theme;
17
15
  }
@@ -1,34 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext } from "react";
2
+ import { createContext, useContext, useSyncExternalStore } from "react";
3
+ import { defaultAppearance } from "@phreshos/core";
3
4
  import DocumentScrollbars from "./document-scrollbars.js";
4
- const missing = Symbol("AppearanceProvider");
5
- const AppearanceContext = createContext(missing);
6
- const ThemeContext = createContext(missing);
7
- /** Provides unresolved Appearance and one effective Theme to a React subtree. */
5
+ import MotionStyle from "./motion-style.js";
6
+ const AppearanceContext = createContext(defaultAppearance);
7
+ const ThemeContext = createContext(null);
8
+ /** Optionally overrides unresolved Appearance and effective Theme for a React subtree. */
8
9
  export function AppearanceProvider({ appearance, children, theme }) {
9
- return _jsx(AppearanceContext.Provider, { value: appearance, children: _jsxs(ThemeContext.Provider, { value: theme, children: [_jsx(DocumentScrollbars, { appearance: appearance, theme: theme }), children] }) });
10
+ const inheritedAppearance = useAppearance();
11
+ const inheritedTheme = useTheme();
12
+ const resolvedAppearance = appearance ?? inheritedAppearance;
13
+ const resolvedTheme = theme ?? inheritedTheme;
14
+ return _jsx(AppearanceContext.Provider, { value: resolvedAppearance, children: _jsxs(ThemeContext.Provider, { value: resolvedTheme, children: [_jsx(MotionStyle, {}), _jsx(DocumentScrollbars, { appearance: resolvedAppearance, theme: resolvedTheme }), children] }) });
10
15
  }
11
- /** Returns the complete unresolved Appearance supplied by the nearest provider. */
16
+ /** Returns the nearest unresolved Appearance, or Core's complete default. */
12
17
  export function useAppearance() {
13
- const appearance = useContext(AppearanceContext);
14
- if (appearance === missing)
15
- throw new Error("useAppearance() requires an AppearanceProvider");
16
- return appearance;
18
+ return useContext(AppearanceContext);
17
19
  }
18
- /** Returns the effective Theme supplied by the nearest provider. */
20
+ /** Returns the nearest explicit Theme, or reactively follows the browser. */
19
21
  export function useTheme() {
20
22
  const theme = useContext(ThemeContext);
21
- if (theme === missing)
22
- throw new Error("useTheme() requires an AppearanceProvider");
23
- return theme;
23
+ const browserTheme = useBrowserTheme();
24
+ return theme ?? browserTheme;
24
25
  }
25
26
  /** Resolves one themed value only where it is consumed. */
26
27
  export function useResolveTheme(value) {
27
28
  const theme = useTheme();
28
29
  return (theme === "dark" && "dark" in value ? value.dark : value.light);
29
30
  }
30
- /** Internal optional read for primitives that also accept direct values. */
31
- export function useAppearanceIfAvailable() {
32
- const appearance = useContext(AppearanceContext);
33
- return appearance === missing ? null : appearance;
31
+ function useBrowserTheme() {
32
+ return useSyncExternalStore(subscribeBrowserTheme, browserTheme, serverTheme);
34
33
  }
34
+ function subscribeBrowserTheme(change) {
35
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
36
+ return () => undefined;
37
+ const preference = window.matchMedia(darkThemeQuery);
38
+ preference.addEventListener("change", change);
39
+ return () => preference.removeEventListener("change", change);
40
+ }
41
+ function browserTheme() {
42
+ return typeof window !== "undefined"
43
+ && typeof window.matchMedia === "function"
44
+ && window.matchMedia(darkThemeQuery).matches
45
+ ? "dark"
46
+ : "light";
47
+ }
48
+ function serverTheme() { return "light"; }
49
+ const darkThemeQuery = "(prefers-color-scheme: dark)";
package/dist/button.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
2
  import type { ButtonProps as AriaButtonProps } from "react-aria-components";
3
- import { type ScaleLevel } from "./scale.js";
4
- import { type RadiusProps } from "./radius.js";
5
- type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
3
+ import type { ScaleLevel } from "./scale.js";
4
+ import type { RadiusProps } from "./radius.js";
5
+ import { type ControlColor } from "./control.js";
6
+ import type { MaterialOverrides } from "./material.js";
7
+ type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
8
+ /** A semantic color from Appearance; omission uses background and foreground. */
9
+ export type ButtonColor = ControlColor;
6
10
  /** Properties accepted by the shared interactive button. */
7
- export interface ButtonProps extends NativeButtonProps, RadiusProps {
11
+ export interface ButtonProps extends NativeButtonProps, RadiusProps, MaterialOverrides {
8
12
  /** Visible Button content. */
9
13
  readonly children?: ReactNode;
14
+ /** Base color for the material. Omission keeps the Button neutral. */
15
+ readonly color?: ButtonColor;
10
16
  /** Native class name applied without replacing the component contract. */
11
17
  readonly className?: string;
12
18
  /** Prevents focus and activation. */
@@ -17,7 +23,7 @@ export interface ButtonProps extends NativeButtonProps, RadiusProps {
17
23
  readonly onPress?: () => void;
18
24
  /** Derives the Button's spacing from Appearance's concrete default. */
19
25
  readonly size?: ScaleLevel;
20
- /** Additional native styles that do not replace the Button's identity. */
26
+ /** Native styles applied after Button defaults. */
21
27
  readonly style?: CSSProperties;
22
28
  }
23
29
  /** An Appearance-aware action with normalized pointer and keyboard behavior. */
package/dist/button.js CHANGED
@@ -1,34 +1,33 @@
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 { scale } from "./scale.js";
5
- import { resolveRadius } from "./radius.js";
6
- import { useAppearance, useResolveTheme } from "./appearance-provider.js";
4
+ import { controlFontSizes, useControlTheme } from "./control.js";
5
+ import { MaterialButton } from "./control-material.js";
6
+ import { visualTransition } from "./motion-style.js";
7
7
  /** An Appearance-aware action with normalized pointer and keyboard behavior. */
8
- export const Button = forwardRef(function Button({ children, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, type = "button", ...properties }, ref) {
9
- const appearance = useAppearance();
10
- const spacing = scale(useResolveTheme(appearance.spacing), size);
11
- const borderRadius = resolveRadius(radius, appearance);
12
- const foreground = useResolveTheme(appearance.foreground);
13
- return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
14
- borderRadius,
8
+ export const Button = forwardRef(function Button({ children, color, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, material, type = "button", ...properties }, ref) {
9
+ const theme = useControlTheme({ color, radius, size });
10
+ return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, render: (native, state) => _jsx(MaterialButton, { native: native, material: material, paint: buttonPaint(theme, !disabled && !pending, state.isHovered, state.isPressed) }), style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
11
+ theme,
15
12
  disabled,
16
13
  isFocusVisible,
17
14
  isHovered,
18
15
  isPressed,
19
16
  pending,
20
17
  size,
21
- spacing,
22
- foreground,
23
18
  style
24
19
  }), children: children });
25
20
  });
26
- function buttonStyle({ borderRadius, disabled, isFocusVisible, isHovered, isPressed, pending, size, spacing, foreground, style }) {
27
- const fontSize = buttonFontSizes[size];
28
- const height = Math.max(size === "xsmall" ? 24 : 28, 20 + spacing);
21
+ function buttonStyle({ theme, disabled, isFocusVisible, isHovered, isPressed, pending, size, style }) {
22
+ const fontSize = controlFontSizes[size];
23
+ const { spacing, foreground } = theme;
24
+ const height = Math.max(24, 24 + spacing);
25
+ const interactive = !disabled && !pending;
26
+ const paint = buttonPaint(theme, interactive, isHovered, isPressed);
29
27
  return {
30
- ...style,
28
+ ...visualTransition,
31
29
  appearance: "none",
30
+ boxSizing: "border-box",
32
31
  display: "inline-grid",
33
32
  gridAutoFlow: "column",
34
33
  gridAutoColumns: "max-content",
@@ -37,34 +36,27 @@ function buttonStyle({ borderRadius, disabled, isFocusVisible, isHovered, isPres
37
36
  minWidth: 0,
38
37
  height,
39
38
  paddingBlock: 0,
40
- paddingInline: Math.max(8, spacing * 2 / 3),
39
+ paddingInline: Math.max(8, spacing),
41
40
  gap: Math.max(4, spacing / 2),
42
- border: "1px solid rgba(255, 255, 255, 0.45)",
43
- borderRadius,
44
- outline: "none",
45
- color: foreground,
46
- backgroundColor: `rgba(255, 255, 255, ${isPressed ? 0.42 : isHovered ? 0.5 : 0.3})`,
47
- boxShadow: isFocusVisible
48
- ? "0 0 0 2px rgba(255, 255, 255, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.8)"
49
- : "inset 0 1px 0 rgba(255, 255, 255, 0.8)",
41
+ border: "none",
42
+ borderRadius: theme.radius,
43
+ outline: isFocusVisible ? `2px solid ${foreground}` : "none",
44
+ outlineOffset: 2,
45
+ ...paint,
50
46
  opacity: disabled ? 0.46 : pending ? 0.68 : 1,
51
- transform: isPressed ? "scale(0.95)" : "scale(1)",
52
- transition: "background-color 100ms ease, box-shadow 100ms ease, opacity 100ms ease, transform 100ms ease",
53
47
  cursor: disabled ? "not-allowed" : pending ? "progress" : "pointer",
54
48
  font: "inherit",
55
49
  fontSize,
56
- fontWeight: 650,
50
+ fontWeight: 550,
57
51
  lineHeight: 1,
58
52
  textAlign: "center",
59
53
  textDecoration: "none",
60
54
  userSelect: "none",
61
- WebkitTapHighlightColor: "transparent"
55
+ WebkitTapHighlightColor: "transparent",
56
+ ...style
62
57
  };
63
58
  }
64
- const buttonFontSizes = Object.freeze({
65
- xsmall: 10,
66
- small: 11,
67
- medium: 12,
68
- large: 13,
69
- xlarge: 14
70
- });
59
+ function buttonPaint(theme, interactive, hovered, pressed) {
60
+ const paints = theme.colored ? theme.paints.palette : theme.paints.neutral;
61
+ return interactive && pressed ? paints.pressed : interactive && hovered ? paints.hover : paints.rest;
62
+ }
@@ -0,0 +1,11 @@
1
+ import type { CheckboxFieldProps } from "react-aria-components";
2
+ import type { ControlOverrides, ControlProps, FieldProps } from "./control.js";
3
+ import type { MaterialOverrides } from "./material.js";
4
+ export interface CheckboxProps extends Omit<CheckboxFieldProps, ControlOverrides | "isReadOnly" | "isSelected" | "defaultSelected" | "isIndeterminate">, ControlProps, FieldProps, MaterialOverrides {
5
+ readonly checked?: boolean;
6
+ readonly defaultChecked?: boolean;
7
+ readonly indeterminate?: boolean;
8
+ readonly readOnly?: boolean;
9
+ }
10
+ /** An independent boolean field with optional mixed-state presentation. */
11
+ export declare const Checkbox: import("react").ForwardRefExoticComponent<CheckboxProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { CheckboxField, CheckboxButton } from "react-aria-components";
4
+ import { FieldFeedback, fieldStyle, useControlTheme } from "./control.js";
5
+ import { ToggleIndicator, toggleStyle } from "./toggle-indicator.js";
6
+ /** An independent boolean field with optional mixed-state presentation. */
7
+ export const Checkbox = forwardRef(function Checkbox({ label, description, errorMessage, disabled, required, invalid, readOnly, checked, defaultChecked, indeterminate, size, color, style, material, ...properties }, ref) {
8
+ const theme = useControlTheme({ size, color });
9
+ return _jsxs(CheckboxField, { ...properties, ref: ref, isDisabled: disabled, isRequired: required, isInvalid: invalid, isReadOnly: readOnly, isSelected: checked, defaultSelected: defaultChecked, isIndeterminate: indeterminate, style: state => fieldStyle(theme, state.isDisabled, style), children: [_jsx(CheckboxButton, { style: state => toggleStyle(theme, state.isDisabled, state.isReadOnly), children: state => _jsxs(_Fragment, { children: [_jsx(ToggleIndicator, { kind: "checkbox", material: material, theme: theme, selected: state.isSelected, indeterminate: state.isIndeterminate, focused: state.isFocusVisible, invalid: state.isInvalid, hovered: !state.isDisabled && !state.isReadOnly && state.isHovered, pressed: !state.isDisabled && !state.isReadOnly && state.isPressed }), label] }) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
10
+ });
package/dist/color.d.ts CHANGED
@@ -1,10 +1,50 @@
1
+ import type { AppearanceColor } from "@phreshos/core";
1
2
  /** A visual treatment derived from one concrete CSS color. */
2
3
  export type ColorLevel = "subtle" | "soft" | "base" | "strong" | "intense";
3
4
  /** Every visual treatment derived from one concrete CSS color. */
4
5
  export type ColorScale = Readonly<Record<ColorLevel, string>>;
6
+ /** One Appearance color and resting level, or a direct CSS color. */
7
+ export type Color = `${AppearanceColor}:${ColorLevel}` | (string & {});
8
+ export declare const defaultColor = "background:base";
9
+ /** Distinguishes derived color levels from direct CSS colors. */
10
+ export declare function isColorLevel(value: string | undefined): value is ColorLevel;
5
11
  /** Derives visual treatments while preserving the concrete value at `base`. */
6
12
  export declare function color(value: string): ColorScale;
13
+ /** Resolves the same named treatment for solid paint and contrast calculations. */
14
+ export declare function resolveColorLevel(value: string, level: ColorLevel): string;
7
15
  /** Returns the complete visual treatments derived from one concrete color. */
8
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;
9
21
  /** Applies material opacity without restricting the source CSS color syntax. */
10
22
  export declare function colorOpacity(value: string, opacity: number): string;
23
+ /** Reads perceptual lightness from a resolved CSS color. */
24
+ export declare function colorLightness(value: string): number;
25
+ /** Orders resolved CSS colors by perceptual lightness, never by their role. */
26
+ export declare function orderColors(first: string, second: string): {
27
+ lighter: string;
28
+ darker: string;
29
+ };
30
+ /** Solid controls use opaque, gamut-mapped colors for both paint and contrast. */
31
+ export declare function opaqueColor(value: string): string;
32
+ /** Changes only perceptual lightness; theme names never determine direction. */
33
+ export declare function colorShade(value: string, amount: number): string;
34
+ /** Chooses only from the two Appearance colors, against the actual painted fill. */
35
+ export declare function onColor(fill: string, background: string, foreground: string): string;
36
+ /** Solid controls start at base; interaction shades preserve its text choice. */
37
+ export declare function solidColors(base: string, background: string, foreground: string): {
38
+ rest: {
39
+ background: string;
40
+ color: string;
41
+ };
42
+ hover: {
43
+ background: string;
44
+ color: string;
45
+ };
46
+ pressed: {
47
+ background: string;
48
+ color: string;
49
+ };
50
+ };