@phreshos/react-ui 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -15
- package/dist/appearance-context.d.ts +43 -1
- package/dist/appearance-context.js +68 -18
- package/dist/button.js +6 -10
- package/dist/color.d.ts +7 -5
- package/dist/color.js +37 -12
- package/dist/context-menu.d.ts +4 -1
- package/dist/context-menu.js +30 -3
- package/dist/control.d.ts +16 -2
- package/dist/control.js +34 -20
- package/dist/dialog.js +47 -40
- package/dist/direction.d.ts +9 -1
- package/dist/direction.js +34 -10
- package/dist/field-style.js +2 -1
- package/dist/main.d.ts +3 -2
- package/dist/main.js +2 -1
- package/dist/material-options.d.ts +14 -5
- package/dist/material-options.js +2 -5
- package/dist/material-paint.d.ts +3 -1
- package/dist/material-paint.js +35 -11
- package/dist/menu.js +30 -18
- package/dist/motion-style.d.ts +6 -8
- package/dist/motion-style.js +15 -31
- package/dist/popover.js +13 -12
- package/dist/radio.js +4 -3
- package/dist/scroll-area.js +17 -10
- package/dist/select.js +24 -36
- package/dist/shadow-options.d.ts +9 -3
- package/dist/shadow-options.js +2 -5
- package/dist/slider.js +7 -6
- package/dist/surface-edge.d.ts +3 -1
- package/dist/surface-edge.js +3 -5
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +41 -20
- package/dist/toggle-indicator.js +2 -5
- package/dist/tooltip.js +16 -15
- package/dist/ui-provider.js +9 -6
- package/dist/window.d.ts +36 -0
- package/dist/window.js +136 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -79,11 +79,12 @@ use the shared Surface implementation while retaining their native behavior. Col
|
|
|
79
79
|
are independent inputs. `color` accepts an Appearance color and resting level such
|
|
80
80
|
as `background:base` or `primary:soft`, or a direct CSS color. Surface defaults
|
|
81
81
|
to `background:base`; solid controls default to `default:base`.
|
|
82
|
-
Hover and press derive shades of that fill.
|
|
83
|
-
higher-contrast text from Appearance's background and foreground
|
|
84
|
-
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.
|
|
85
85
|
`size` accepts `xsmall`, `small`, `medium` (default), `large`, or `xlarge`;
|
|
86
|
-
spacing and radius follow Appearance
|
|
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;
|
|
87
88
|
`pending` prevents activation while retaining focus.
|
|
88
89
|
|
|
89
90
|
```tsx
|
|
@@ -95,7 +96,7 @@ spacing and radius follow Appearance. `disabled` prevents activation and focus;
|
|
|
95
96
|
`Surface` is the material- and shadow-owning element. It renders a `div` by default, while
|
|
96
97
|
`as` selects another React element and preserves that element's native properties
|
|
97
98
|
and ref type. Surface owns its paint, opacity, frost, refraction, grain, edge,
|
|
98
|
-
outer shadow,
|
|
99
|
+
outer shadow, and radius. `radius` accepts a size level, `full`, a
|
|
99
100
|
number in pixels, or a CSS radius and defaults to `medium`.
|
|
100
101
|
|
|
101
102
|
```tsx
|
|
@@ -114,11 +115,18 @@ required geometry.
|
|
|
114
115
|
`MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
|
|
115
116
|
`distortion`, and `saturation`. Every material-bearing component, including
|
|
116
117
|
Surface, exposes these values through its `material` prop. Color remains a
|
|
117
|
-
separate property. Omission
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
opacity
|
|
121
|
-
|
|
118
|
+
separate property. Omission uses the resource-efficient `basic` rendering mode.
|
|
119
|
+
`none` ignores Material and paints the resolved color as a normal background;
|
|
120
|
+
`basic` adds Material paint, grain, and its edge while remaining solid, with
|
|
121
|
+
Material opacity applied only to the grain; `extended` applies that opacity to
|
|
122
|
+
the complete Material instead; and `full` adds backdrop, distortion, and
|
|
123
|
+
saturation. A `MaterialOptions` object selects full rendering with those
|
|
124
|
+
overrides. Effect options accept a scale level or a direct number; opacity
|
|
125
|
+
affects Surface paint only, never its content. The material edge is part of the
|
|
126
|
+
same Surface rather than a second public entity.
|
|
127
|
+
|
|
128
|
+
The modes are progressive: each level preserves the rendering of the previous
|
|
129
|
+
one and adds only its own Material capability.
|
|
122
130
|
|
|
123
131
|
Surface-based controls expose the same separate `color` and `material` props.
|
|
124
132
|
`ShadowOptions` similarly groups `x`, `y`, `blur`, `spread`, and `opacity` under
|
|
@@ -132,7 +140,8 @@ to its options, and a Radio can override them. Shadow follows the same targets a
|
|
|
132
140
|
inheritance path.
|
|
133
141
|
|
|
134
142
|
```tsx
|
|
135
|
-
<Button color="primary:base" material=
|
|
143
|
+
<Button color="primary:base" material="full">Save</Button>
|
|
144
|
+
<Surface material={{ opacity: 0.6, backdrop: 0 }}>Translucent</Surface>
|
|
136
145
|
<Input label="Name" radius="large" material={{ grain: "small" }} shadow={{ blur: "small" }} />
|
|
137
146
|
```
|
|
138
147
|
|
|
@@ -153,6 +162,31 @@ Native properties and the root ref target the outer Surface. `Panel.Header`
|
|
|
153
162
|
owns the optional leading region, while `Panel.Content` is the independently
|
|
154
163
|
configurable inner Surface.
|
|
155
164
|
|
|
165
|
+
`Window` provides one Surface with a ready header-and-content layout.
|
|
166
|
+
`Window.Header.Identity` presents the icon and truncating title,
|
|
167
|
+
`Window.Header.Center` is optional flexible space, and
|
|
168
|
+
`Window.Header.Actions` aligns compact controls at the end. `Window.Content`
|
|
169
|
+
fills the remaining area without deciding its overflow behavior. The header
|
|
170
|
+
uses the enclosing Surface rather than creating another material layer, and it
|
|
171
|
+
can also be composed independently inside any Surface. Its controls are React
|
|
172
|
+
UI Buttons; callers connect them to their own window operations. Pointer and
|
|
173
|
+
double-click handlers on the header can implement a drag area, while the center
|
|
174
|
+
and actions keep their interactions separate.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<Window>
|
|
178
|
+
<Window.Header active={active} onPointerDown={beginDrag}>
|
|
179
|
+
<Window.Header.Identity icon={icon} title={title} />
|
|
180
|
+
<Window.Header.Actions>
|
|
181
|
+
<Window.Header.Minimize onPress={minimize} />
|
|
182
|
+
<Window.Header.Maximize maximized={maximized} onPress={toggleMaximize} />
|
|
183
|
+
<Window.Header.Close onPress={close} />
|
|
184
|
+
</Window.Header.Actions>
|
|
185
|
+
</Window.Header>
|
|
186
|
+
<Window.Content>{children}</Window.Content>
|
|
187
|
+
</Window>
|
|
188
|
+
```
|
|
189
|
+
|
|
156
190
|
## Overlays
|
|
157
191
|
|
|
158
192
|
Overlay components expose each behavioral role as a named part. Roots own open
|
|
@@ -202,8 +236,8 @@ Blur, distortion, geometry, and Surface host opacity are not transitioned; gradi
|
|
|
202
236
|
and structurally removed effects change directly rather than adding extra
|
|
203
237
|
layers or keeping disabled effects alive.
|
|
204
238
|
|
|
205
|
-
Motion animates toggle presses, selection marks, switch travel
|
|
206
|
-
|
|
239
|
+
Motion animates toggle presses, selection marks, and switch travel. CSS transitions
|
|
240
|
+
animate paint and the Select chevron. Preferences with animations disabled remove spatial
|
|
207
241
|
feedback and make state transitions immediate. Slider values and native input
|
|
208
242
|
behavior are never delayed by visual animation.
|
|
209
243
|
|
|
@@ -255,8 +289,9 @@ import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider }
|
|
|
255
289
|
<Slider label="Volume" name="volume" defaultValue={50} minValue={0} maxValue={100} step={1} />
|
|
256
290
|
```
|
|
257
291
|
|
|
258
|
-
|
|
259
|
-
|
|
292
|
+
Each control delegates focus, keyboard, form, and selection behavior to its
|
|
293
|
+
accessible behavioral primitive rather than recreating those systems. Select's
|
|
294
|
+
popup uses Surface defaults. Toggle indicators use Motion internally and respect
|
|
260
295
|
animation preferences. The preview Program demonstrates each input's
|
|
261
296
|
sizes, colors, state, and interaction without overriding its visual defaults.
|
|
262
297
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type Appearance, type ThemedValue } from "./appearance.js";
|
|
2
2
|
import type { Preferences } from "./preferences.js";
|
|
3
3
|
export declare const AppearanceContext: import("react").Context<Appearance>;
|
|
4
|
-
export
|
|
4
|
+
export interface PreferencesSource {
|
|
5
|
+
readonly getSnapshot: () => Preferences;
|
|
6
|
+
readonly getServerSnapshot: () => Preferences;
|
|
7
|
+
readonly subscribe: (change: () => void) => () => void;
|
|
8
|
+
}
|
|
5
9
|
/** Returns the nearest unresolved appearance, or React UI's default. */
|
|
6
10
|
export declare function useAppearance(): Appearance;
|
|
7
11
|
/** Returns the nearest complete Preferences, or reactively follows the browser. */
|
|
@@ -10,3 +14,41 @@ export declare function usePreferences(): Preferences;
|
|
|
10
14
|
export declare function useBrowserPreferences(): Preferences;
|
|
11
15
|
/** Selects the active branch of one complete ThemedValue. */
|
|
12
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
2
|
import { defaultAppearance } from "./appearance.js";
|
|
3
3
|
export const AppearanceContext = createContext(defaultAppearance);
|
|
4
|
-
export const PreferencesContext = createContext(null);
|
|
5
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
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
return
|
|
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)";
|
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 {
|
|
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,
|
|
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:
|
|
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 ?
|
|
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 "./appearance.js";
|
|
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
|
|
18
|
-
export declare function
|
|
19
|
-
/** Resolves
|
|
20
|
-
export declare function
|
|
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
|
|
3
|
-
import {
|
|
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
|
|
44
|
-
export function
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
|
51
|
-
export function
|
|
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");
|
package/dist/context-menu.d.ts
CHANGED
|
@@ -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<
|
|
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;
|
package/dist/context-menu.js
CHANGED
|
@@ -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:
|
|
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,
|
|
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:
|
|
54
|
+
readonly fontSize: CSSProperties["fontSize"];
|
|
55
|
+
readonly indicatorSize: number;
|
|
42
56
|
readonly height: number;
|
|
43
57
|
readonly gap: number;
|
|
44
58
|
}
|
package/dist/control.js
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { FieldError, Label, Text } from "react-aria-components";
|
|
4
|
-
import {
|
|
4
|
+
import { useResolvedAppearance } from "./appearance-context.js";
|
|
5
5
|
import { resolveRadius } from "./radius.js";
|
|
6
6
|
import { scale } from "./scale.js";
|
|
7
7
|
import { colorOpacity, solidColors } from "./color.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { resolveSolidColor } from "./color.js";
|
|
9
|
+
import { controlTransition, visualTransition } from "./motion-style.js";
|
|
10
10
|
export const controlFontSizes = Object.freeze({
|
|
11
|
-
xsmall:
|
|
12
|
-
small:
|
|
13
|
-
medium:
|
|
14
|
-
large:
|
|
15
|
-
xlarge:
|
|
11
|
+
xsmall: "0.6875em",
|
|
12
|
+
small: "0.75em",
|
|
13
|
+
medium: "0.8125em",
|
|
14
|
+
large: "0.875em",
|
|
15
|
+
xlarge: "0.9375em"
|
|
16
16
|
});
|
|
17
|
+
/** Shared interaction attenuation, derived consistently across control families. */
|
|
18
|
+
export const controlOpacity = Object.freeze({
|
|
19
|
+
disabled: 0.46,
|
|
20
|
+
pending: 0.68,
|
|
21
|
+
secondary: 0.7,
|
|
22
|
+
placeholder: 0.6,
|
|
23
|
+
separator: 0.14
|
|
24
|
+
});
|
|
25
|
+
/** Shared emphasis used by labels and control-owned headings. */
|
|
26
|
+
export const controlFontWeight = 600;
|
|
17
27
|
export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
28
|
+
const resolved = useResolvedAppearance();
|
|
29
|
+
const { appearance, colors } = resolved;
|
|
30
|
+
const transition = useMemo(() => visualTransition(resolved.transaction, resolved.preferences.animations), [resolved.transaction, resolved.preferences.animations]);
|
|
31
|
+
const motionTransition = useMemo(() => controlTransition(resolved.transaction, resolved.preferences.animations), [resolved.transaction, resolved.preferences.animations]);
|
|
20
32
|
const spacing = scale(appearance.spacing, size);
|
|
21
|
-
const colors = useThemedValue(appearance.colors);
|
|
22
33
|
const foreground = colors.foreground;
|
|
23
34
|
const background = colors.background;
|
|
24
|
-
const tint =
|
|
25
|
-
const neutral =
|
|
26
|
-
const focus =
|
|
27
|
-
const danger =
|
|
35
|
+
const tint = resolveSolidColor(color ?? "default:base", colors);
|
|
36
|
+
const neutral = resolveSolidColor("default:base", colors);
|
|
37
|
+
const focus = resolveSolidColor("warning:base", colors);
|
|
38
|
+
const danger = resolveSolidColor("danger:base", colors);
|
|
28
39
|
const paints = useMemo(() => ({
|
|
29
40
|
palette: solidColors(tint, background, foreground),
|
|
30
41
|
neutral: solidColors(neutral, background, foreground),
|
|
31
42
|
danger: solidColors(danger, background, foreground)
|
|
32
43
|
}), [tint, neutral, background, foreground, danger]);
|
|
33
|
-
return {
|
|
44
|
+
return useMemo(() => ({
|
|
34
45
|
transition,
|
|
46
|
+
motionTransition,
|
|
47
|
+
animations: resolved.preferences.animations,
|
|
35
48
|
spacing,
|
|
36
49
|
foreground,
|
|
37
50
|
background,
|
|
@@ -41,9 +54,10 @@ export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
|
41
54
|
paints,
|
|
42
55
|
radius: resolveRadius(radius, appearance),
|
|
43
56
|
fontSize: controlFontSizes[size],
|
|
57
|
+
indicatorSize: Math.max(16, 16 + spacing / 4),
|
|
44
58
|
height: Math.max(24, 24 + spacing),
|
|
45
59
|
gap: Math.max(4, spacing / 2)
|
|
46
|
-
};
|
|
60
|
+
}), [appearance, colors, transition, motionTransition, resolved.preferences.animations, spacing, foreground, background, tint, focus, danger, paints, radius, size]);
|
|
47
61
|
}
|
|
48
62
|
export function fieldStyle(theme, disabled = false, style) {
|
|
49
63
|
return {
|
|
@@ -56,7 +70,7 @@ export function fieldStyle(theme, disabled = false, style) {
|
|
|
56
70
|
fontFamily: "inherit",
|
|
57
71
|
fontSize: theme.fontSize,
|
|
58
72
|
lineHeight: 1.5,
|
|
59
|
-
opacity: disabled ?
|
|
73
|
+
opacity: disabled ? controlOpacity.disabled : 1
|
|
60
74
|
};
|
|
61
75
|
}
|
|
62
76
|
export function controlPaint(theme, focused, invalid, hovered = false) {
|
|
@@ -85,8 +99,8 @@ export function controlStyle(theme, focused, invalid, hovered = false, focusVisi
|
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
export function FieldLabel({ label }) {
|
|
88
|
-
return label == null ? null : _jsx(Label, { style: { fontWeight:
|
|
102
|
+
return label == null ? null : _jsx(Label, { style: { fontWeight: controlFontWeight }, children: label });
|
|
89
103
|
}
|
|
90
104
|
export function FieldFeedback({ description, errorMessage, theme }) {
|
|
91
|
-
return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity:
|
|
105
|
+
return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: controlOpacity.secondary }, children: description }), _jsx(FieldError, { style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
|
|
92
106
|
}
|