@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.
- package/README.md +200 -35
- package/dist/appearance-provider.d.ts +6 -8
- package/dist/appearance-provider.js +34 -19
- package/dist/button.d.ts +11 -5
- package/dist/button.js +28 -36
- package/dist/checkbox.d.ts +11 -0
- package/dist/checkbox.js +10 -0
- package/dist/color.d.ts +40 -0
- package/dist/color.js +99 -4
- package/dist/control-material.d.ts +20 -0
- package/dist/control-material.js +14 -0
- package/dist/control.d.ts +55 -0
- package/dist/control.js +87 -0
- package/dist/document-scrollbars.js +1 -1
- package/dist/field-style.d.ts +3 -0
- package/dist/field-style.js +11 -0
- package/dist/flex.js +2 -2
- package/dist/grid.js +2 -2
- package/dist/input.d.ts +4 -0
- package/dist/input.js +11 -0
- package/dist/main.d.ts +14 -2
- package/dist/main.js +11 -0
- package/dist/material-options.d.ts +20 -0
- package/dist/material-options.js +22 -0
- package/dist/material-paint.d.ts +11 -0
- package/dist/material-paint.js +76 -0
- package/dist/material.d.ts +27 -0
- package/dist/material.js +45 -0
- package/dist/motion-style.d.ts +21 -0
- package/dist/motion-style.js +45 -0
- package/dist/panel.d.ts +9 -0
- package/dist/panel.js +27 -0
- package/dist/radio.d.ts +14 -0
- package/dist/radio.js +17 -0
- package/dist/select.d.ts +17 -0
- package/dist/select.js +28 -0
- package/dist/slider.d.ts +7 -0
- package/dist/slider.js +36 -0
- package/dist/surface-edge.d.ts +7 -0
- package/dist/surface-edge.js +59 -0
- package/dist/surface.d.ts +7 -44
- package/dist/surface.js +18 -98
- package/dist/switch.d.ts +10 -0
- package/dist/switch.js +10 -0
- package/dist/text-control.d.ts +9 -0
- package/dist/text-control.js +0 -0
- package/dist/textarea.d.ts +6 -0
- package/dist/textarea.js +17 -0
- package/dist/toggle-indicator.d.ts +16 -0
- package/dist/toggle-indicator.js +51 -0
- package/package.json +4 -3
- package/dist/surface-material.d.ts +0 -13
- 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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
`
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
223
|
+
validates its published shape.
|
|
66
224
|
|
|
67
|
-
##
|
|
225
|
+
## Related repositories
|
|
68
226
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
3
|
-
/**
|
|
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
|
|
5
|
+
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
6
6
|
export declare function useAppearance(): Appearance;
|
|
7
|
-
/** Returns the
|
|
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
|
|
12
|
+
readonly appearance?: Appearance;
|
|
15
13
|
readonly children: ReactNode;
|
|
16
|
-
readonly 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
|
-
|
|
5
|
-
const AppearanceContext = createContext(
|
|
6
|
-
const ThemeContext = createContext(
|
|
7
|
-
/**
|
|
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
|
-
|
|
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
|
|
16
|
+
/** Returns the nearest unresolved Appearance, or Core's complete default. */
|
|
12
17
|
export function useAppearance() {
|
|
13
|
-
|
|
14
|
-
if (appearance === missing)
|
|
15
|
-
throw new Error("useAppearance() requires an AppearanceProvider");
|
|
16
|
-
return appearance;
|
|
18
|
+
return useContext(AppearanceContext);
|
|
17
19
|
}
|
|
18
|
-
/** Returns the
|
|
20
|
+
/** Returns the nearest explicit Theme, or reactively follows the browser. */
|
|
19
21
|
export function useTheme() {
|
|
20
22
|
const theme = useContext(ThemeContext);
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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({
|
|
27
|
-
const fontSize =
|
|
28
|
-
const
|
|
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
|
-
...
|
|
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
|
|
39
|
+
paddingInline: Math.max(8, spacing),
|
|
41
40
|
gap: Math.max(4, spacing / 2),
|
|
42
|
-
border: "
|
|
43
|
-
borderRadius,
|
|
44
|
-
outline: "none",
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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>>;
|
package/dist/checkbox.js
ADDED
|
@@ -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
|
+
};
|