@phreshos/react-ui 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,228 +1,69 @@
1
1
  # `@phreshos/react-ui`
2
2
 
3
- A React component library for coherent PhreshOS Program interfaces.
3
+ Environment-neutral React components and visual interpretation for PhreshOS.
4
+ The package depends on React and Core contracts, not on either execution SDK.
4
5
 
5
- ## Installation
6
+ ## Appearance composition
6
7
 
7
- ```bash
8
- bun add @phreshos/react-ui @phreshos/core react react-dom
9
- ```
10
-
11
- React UI accepts an explicit `ThemeProperties` snapshot and does not require a
12
- running PhreshOS environment:
13
-
14
- ```tsx
15
- import { standardTheme } from "@phreshos/core"
16
- import { Button, Flex, ThemeProvider } from "@phreshos/react-ui"
17
-
18
- function Example() {
19
- return <ThemeProvider theme={standardTheme}>
20
- <Flex align="center" gap="small">
21
- <Button onPress={() => console.log("save")}>Save</Button>
22
- </Flex>
23
- </ThemeProvider>
24
- }
25
- ```
26
-
27
- ## Package status
28
-
29
- This package is one component of a larger architecture that remains under
30
- active testing. Its public surface is intentionally small and will grow only
31
- as component contracts are established. It is usable outside a running
32
- system, though its standard Theme contract comes from `@phreshos/core`.
33
-
34
- The library is being built up from behavior contracts rather than from a
35
- primitive dependency's component catalog. `Button` uses React Aria Components
36
- for normalized pointer, keyboard, focus, disabled, and pending behavior,
37
- without exposing that library as the public design language. Components still
38
- under evaluation may compare React Aria Components against Base UI privately,
39
- in tests.
40
-
41
- The library's future icon language has a tree-shakeable public subpath,
42
- `@phreshos/react-ui/icons`. That subpath deliberately exports nothing until an
43
- icon source and its contracts have been selected.
44
-
45
- `Grid` and `Flex` are appearance-neutral layout primitives. They preserve
46
- native element properties, styles, and refs while naming the layout decisions
47
- that recur throughout an interface:
48
-
49
- ```tsx
50
- <Grid columns="repeat(auto-fit, minmax(12rem, 1fr))" gap="1rem">
51
- ...
52
- </Grid>
53
-
54
- <Flex align="center" justify="between" gap={12} wrap>
55
- ...
56
- </Flex>
57
- ```
58
-
59
- Numeric gaps are expressed in pixels. Grid dimensions may be positive integer
60
- counts or native CSS track expressions, leaving responsive behavior to CSS
61
- rather than introducing a second breakpoint system.
62
-
63
- Inside a `ThemeProvider`, React UI derives its own spacing levels from the
64
- Theme's concrete default spacing:
65
-
66
- ```tsx
67
- <Flex gap="small">...</Flex>
68
- <Grid gap="large">...</Grid>
69
- ```
70
-
71
- Structures that own native spacing pass the explicit Theme value to React UI's
72
- general derivation hook:
8
+ React UI receives complete unresolved `Appearance` plus one effective
9
+ `"light" | "dark"` Theme:
73
10
 
74
11
  ```tsx
75
- import { useScale } from "@phreshos/react-ui"
76
-
77
- const spacing = useScale(theme.spacing)
78
-
79
- <section style={{ gap: spacing.large }} />
80
- ```
12
+ import { standardAppearance } from "@phreshos/core"
13
+ import { AppearanceProvider, Button, Surface } from "@phreshos/react-ui"
81
14
 
82
- Explicit values such as `4rem` are used directly — passing them through a
83
- Theme hook would perform no additional work.
84
-
85
- `Surface` keeps a native `<div>` as its public container and gives every
86
- instance one locally owned pure-SVG material plus only the backdrop layers its
87
- settings enable. The material uses a deterministic 64×64 micro-pattern derived
88
- from the former shader grain; it creates no canvas, WebGL context, or shared
89
- texture:
90
-
91
- ```tsx
92
- <Surface className="grid rounded-xl shadow-lg">
93
- ...
94
- </Surface>
95
-
96
- <Surface color="strong" grain="large">...</Surface>
97
- <Surface color="#101114" grain={0.2} backdrop={4} opacity={0.9}>...</Surface>
98
- <Surface distortion={70} waves={8} ripples={4} saturation={1.4} brightness={1.04}>...</Surface>
15
+ export function Example() {
16
+ return <AppearanceProvider appearance={standardAppearance} theme="light">
17
+ <Surface><Button>Continue</Button></Surface>
18
+ </AppearanceProvider>
19
+ }
99
20
  ```
100
21
 
101
- `color` resolves from `Theme.background`; its semantic levels derive from that
102
- same source and a direct color remains an explicit local override. `grain`,
103
- `grainAmount`, `backdrop`, `opacity`, `distortion`, `waves`,
104
- `ripples`, `saturation`, and `brightness` resolve from `Theme.surface`, accept
105
- their semantic levels or direct values, and remain bounded by Core's Theme
106
- limits. Radius and foreground remain ordinary Theme styles. Grain intensity
107
- controls tonal distance while grain amount controls retained cell density.
108
- Each Surface owns one uniform one-pixel inset edge derived from its resolved
109
- material color; consumers supply only their radius and do not redraw the edge.
110
- When either is zero, Surface creates no grain pattern or paths. Refraction and
111
- native frost use independent backdrop layers so blur does not soften the
112
- displaced image. Enabled organic, wave, and ripple fields are combined
113
- mathematically before one displacement pass; each zero-valued field is absent,
114
- and the filter and refraction layer are absent when all three are zero.
115
- Backdrop blur emits no CSS function at zero. Neutral saturation and brightness
116
- at one are also omitted. Animation defaults to zero; only a Surface with
117
- visible grain and a positive rate joins the internal document clock, while
118
- every texture and seed remains local to its own Surface.
119
-
120
- The Theme stores unrestricted CSS background, foreground, and accent sources.
121
- React UI derives the fixed `subtle`, `soft`, `base`, `strong`, and `intense`
122
- treatments from any supplied color, preserving the value exactly at `base`.
123
- `useColor(value)` memoizes that calculation without implicitly choosing a
124
- Theme property. CSS performs the nearby mixing in OKLCH, and the System retains
125
- only the concrete Theme color:
126
-
127
- ```tsx
128
- import { useColor } from "@phreshos/react-ui"
129
-
130
- const colors = useColor(theme.accent)
131
-
132
- <strong style={{ color: colors.strong }} />
133
- ```
22
+ `useAppearance()` returns the unresolved value. `useTheme()` returns only the
23
+ effective mode. `useResolveTheme(themed)` resolves one property at the point
24
+ where it is consumed. Components follow the same rule, so the provider never
25
+ collapses Appearance into a second retained object.
134
26
 
135
- Shape-owning components accept the shared `Radius` value directly. Semantic
136
- levels are derived from the Theme's concrete radius through the same
137
- `scale()` rule used for spacing, while numbers and CSS values remain explicit
138
- overrides:
27
+ An application using the Client SDK composes the packages explicitly:
139
28
 
140
29
  ```tsx
141
- <Button radius="large">...</Button>
142
- <Button radius="2rem">...</Button>
143
- ```
144
-
145
- Structures whose native element owns the shape derive from the explicit
146
- radius value through the same React UI hook:
30
+ const appearance = useSystemAppearance()
31
+ const theme = useSystemTheme()
147
32
 
148
- ```tsx
149
- import { useScale } from "@phreshos/react-ui"
150
-
151
- const radius = useScale(theme.radius)
152
-
153
- <section style={{ borderRadius: radius.large }} />
33
+ return <AppearanceProvider appearance={appearance} theme={theme}>
34
+ {children}
35
+ </AppearanceProvider>
154
36
  ```
155
37
 
156
- `Button` is the library's first interactive primitive. Its translucent
157
- control treatment matches the desktop's Start and sign-out controls, letting
158
- the surrounding Theme material remain visible. It derives spacing and radius
159
- from the Theme while keeping a single activation path across pointer, Enter,
160
- and Space input:
38
+ ## Levels
161
39
 
162
- ```tsx
163
- <Button onPress={save}>Save</Button>
164
- <Button size="large" pending>Saving</Button>
165
- <Button disabled>Unavailable</Button>
166
- ```
40
+ `useScale(value)` and `useColor(value)` derive semantic UI levels from one
41
+ concrete value. They do not select an Appearance property or read an
42
+ environment. Components resolve the property they need first and then derive
43
+ their local level.
167
44
 
168
- Pending Buttons remain focusable but cannot activate. Disabled Buttons leave
169
- the focus order entirely. The native element defaults to `type="button"`, so
170
- placing it inside a form never triggers an accidental submission.
45
+ Layout primitives accept native values without a provider. Semantic gaps and
46
+ radii require Appearance because their concrete source is `spacing` or
47
+ `radius`.
171
48
 
172
- `ThemeProvider` accepts a plain `ThemeProperties` snapshot, such as Core's
173
- `standardTheme`, so the library remains usable without either environment SDK.
174
- A Program can adapt its
175
- observable Host value at the application boundary:
49
+ ## Surface
176
50
 
177
- ```tsx
178
- import { HostProvider, useHostTheme } from "@phreshos/react"
179
- import { ThemeProvider } from "@phreshos/react-ui"
51
+ `Surface` is the shared visual material. It accepts native `div` properties
52
+ plus local overrides for color, grain, grain amount, backdrop blur, opacity,
53
+ distortion, waves, ripples, saturation, and brightness. Omitted controls derive
54
+ from the resolved Appearance. A zero-valued optional effect is omitted from the
55
+ rendered material so disabled work costs nothing.
180
56
 
181
- function ThemedApplication({ children }) {
182
- const theme = useHostTheme()
57
+ Each Surface owns its SVG material and border. Backdrop refraction and frost
58
+ remain separate compositor layers. Radius and foreground resolve from
59
+ Appearance; elevation stays with the surrounding layout.
183
60
 
184
- return <ThemeProvider theme={theme}>{children}</ThemeProvider>
185
- }
186
-
187
- <HostProvider provide={["theme"]} fallback={null}>
188
- <ThemedApplication>{children}</ThemedApplication>
189
- </HostProvider>
190
- ```
191
-
192
- ## Standing requirements
193
-
194
- - Components preserve one recognizable visual identity.
195
- - Props that select a semantic treatment accept only the values documented by
196
- that component. Explicit native styles and supported CSS spacing, radius,
197
- and color values remain available where the component contract allows them.
198
- - `ThemeProvider` requires an explicit `theme` prop; it never discovers an
199
- environment SDK or silently selects a global theme.
200
- - The provider applies a replacement `theme` value immediately to its
201
- descendants.
202
- - Providers are scoped and nestable. The nearest `ThemeProvider` supplies the
203
- complete theme for its descendants without affecting its parent or
204
- siblings.
205
- - Accessibility, keyboard behavior, focus, and form behavior are contractual.
206
- - Components must work inside structurally isolated Program iframes.
207
- - Public types and JSDoc are part of the product.
208
-
209
- ## Development
210
-
211
- ```bash
212
- bun install --frozen-lockfile
213
- bun run verify
214
- ```
61
+ ## Components
215
62
 
216
- `verify` type-checks the source and tests, runs the behavior suite, rebuilds the
217
- package, packs the publication artifact, installs it into a temporary consumer,
218
- and checks its runtime, TypeScript, and public subpath entry points.
219
-
220
- `Button` is the only interactive component currently exported. Private
221
- acceptance suites also compare Field, Select, Dialog, and Context Menu
222
- candidates against the same implementation-independent behaviors; those
223
- candidates are not part of the package's public surface. Field covers labeling,
224
- descriptions, validation, native states, and value changes. Select covers
225
- collections, keyboard input, disabled options, form submission, and cleanup.
226
- Dialog covers modal semantics, focus, dismissal, nesting, state changes, and
227
- cleanup. Context Menu covers invocation, focus, actions, disabled items,
228
- dismissal, and cleanup.
63
+ - `Surface`: shared material container.
64
+ - `Button`: normalized React Aria action with pending and disabled states.
65
+ - `Flex` and `Grid`: small layout primitives that preserve native props.
66
+ - `AppearanceProvider`, `useAppearance`, `useTheme`, `useResolveTheme`:
67
+ environment-neutral appearance composition.
68
+ - `useScale`, `useColor`, `resolveSpacing`, `resolveRadius`: explicit visual
69
+ derivation helpers.
@@ -0,0 +1,17 @@
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. */
4
+ export declare function AppearanceProvider({ appearance, children, theme }: AppearanceProviderProps): import("react").JSX.Element;
5
+ /** Returns the complete unresolved Appearance supplied by the nearest provider. */
6
+ export declare function useAppearance(): Appearance;
7
+ /** Returns the effective Theme supplied by the nearest provider. */
8
+ export declare function useTheme(): Theme;
9
+ /** Resolves one themed value only where it is consumed. */
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
+ export interface AppearanceProviderProps {
14
+ readonly appearance: Appearance;
15
+ readonly children: ReactNode;
16
+ readonly theme: Theme;
17
+ }
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const missing = Symbol("AppearanceProvider");
4
+ const AppearanceContext = createContext(missing);
5
+ const ThemeContext = createContext(missing);
6
+ /** Provides unresolved Appearance and one effective Theme to a React subtree. */
7
+ export function AppearanceProvider({ appearance, children, theme }) {
8
+ return _jsx(AppearanceContext.Provider, { value: appearance, children: _jsx(ThemeContext.Provider, { value: theme, children: children }) });
9
+ }
10
+ /** Returns the complete unresolved Appearance supplied by the nearest provider. */
11
+ export function useAppearance() {
12
+ const appearance = useContext(AppearanceContext);
13
+ if (appearance === missing)
14
+ throw new Error("useAppearance() requires an AppearanceProvider");
15
+ return appearance;
16
+ }
17
+ /** Returns the effective Theme supplied by the nearest provider. */
18
+ export function useTheme() {
19
+ const theme = useContext(ThemeContext);
20
+ if (theme === missing)
21
+ throw new Error("useTheme() requires an AppearanceProvider");
22
+ return theme;
23
+ }
24
+ /** Resolves one themed value only where it is consumed. */
25
+ export function useResolveTheme(value) {
26
+ const theme = useTheme();
27
+ return (theme === "dark" && "dark" in value ? value.dark : value.light);
28
+ }
29
+ /** Internal optional read for primitives that also accept direct values. */
30
+ export function useAppearanceIfAvailable() {
31
+ const appearance = useContext(AppearanceContext);
32
+ return appearance === missing ? null : appearance;
33
+ }
package/dist/button.d.ts CHANGED
@@ -15,11 +15,11 @@ export interface ButtonProps extends NativeButtonProps, RadiusProps {
15
15
  readonly pending?: boolean;
16
16
  /** Runs once for a normalized pointer, Enter, or Space activation. */
17
17
  readonly onPress?: () => void;
18
- /** Derives the Button's spacing from the Theme's concrete default. */
18
+ /** Derives the Button's spacing from Appearance's concrete default. */
19
19
  readonly size?: ScaleLevel;
20
20
  /** Additional native styles that do not replace the Button's identity. */
21
21
  readonly style?: CSSProperties;
22
22
  }
23
- /** A Theme-aware action with normalized pointer and keyboard behavior. */
23
+ /** An Appearance-aware action with normalized pointer and keyboard behavior. */
24
24
  export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
25
25
  export {};
package/dist/button.js CHANGED
@@ -3,12 +3,13 @@ import { forwardRef } from "react";
3
3
  import { Button as AriaButton } from "react-aria-components";
4
4
  import { scale } from "./scale.js";
5
5
  import { resolveRadius } from "./radius.js";
6
- import { useTheme } from "./theme-provider.js";
7
- /** A Theme-aware action with normalized pointer and keyboard behavior. */
6
+ import { useAppearance, useResolveTheme } from "./appearance-provider.js";
7
+ /** An Appearance-aware action with normalized pointer and keyboard behavior. */
8
8
  export const Button = forwardRef(function Button({ children, disabled = false, pending = false, onPress, radius = "medium", size = "medium", style, type = "button", ...properties }, ref) {
9
- const theme = useTheme();
10
- const spacing = scale(theme.spacing, size);
11
- const borderRadius = resolveRadius(radius, theme);
9
+ const appearance = useAppearance();
10
+ const spacing = scale(useResolveTheme(appearance.spacing), size);
11
+ const borderRadius = resolveRadius(radius, appearance);
12
+ const foreground = useResolveTheme(appearance.foreground);
12
13
  return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
13
14
  borderRadius,
14
15
  disabled,
@@ -18,7 +19,7 @@ export const Button = forwardRef(function Button({ children, disabled = false, p
18
19
  pending,
19
20
  size,
20
21
  spacing,
21
- foreground: theme.foreground,
22
+ foreground,
22
23
  style
23
24
  }), children: children });
24
25
  });
package/dist/flex.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { alignment, justification, resolveGap } from "./layout.js";
4
- import { useThemeIfAvailable } from "./theme-provider.js";
4
+ import { useAppearanceIfAvailable } from "./appearance-provider.js";
5
5
  /** A predictable Flexbox container with no visual appearance of its own. */
6
6
  export const Flex = forwardRef(function Flex({ align, direction, gap, inline = false, justify, style, wrap, ...properties }, ref) {
7
- const theme = useThemeIfAvailable();
7
+ const appearance = useAppearanceIfAvailable();
8
8
  return _jsx("div", { ...properties, ref: ref, style: {
9
9
  ...style,
10
10
  display: inline ? "inline-flex" : "flex",
11
11
  alignItems: alignment(align) ?? style?.alignItems,
12
12
  flexDirection: direction ?? style?.flexDirection,
13
13
  flexWrap: wrap === undefined ? style?.flexWrap : wrap === "reverse" ? "wrap-reverse" : wrap ? "wrap" : "nowrap",
14
- gap: gap === undefined ? style?.gap : resolveGap(gap, theme),
14
+ gap: gap === undefined ? style?.gap : resolveGap(gap, appearance),
15
15
  justifyContent: justification(justify) ?? style?.justifyContent
16
16
  } });
17
17
  });
package/dist/grid.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { alignment, justification, resolveGap, tracks } from "./layout.js";
4
- import { useThemeIfAvailable } from "./theme-provider.js";
4
+ import { useAppearanceIfAvailable } from "./appearance-provider.js";
5
5
  /** A predictable CSS Grid container with no visual appearance of its own. */
6
6
  export const Grid = forwardRef(function Grid({ align, columns, flow, gap, inline = false, justify, rows, style, ...properties }, ref) {
7
- const theme = useThemeIfAvailable();
7
+ const appearance = useAppearanceIfAvailable();
8
8
  return _jsx("div", { ...properties, ref: ref, style: {
9
9
  ...style,
10
10
  display: inline ? "inline-grid" : "grid",
11
11
  alignItems: alignment(align) ?? style?.alignItems,
12
- gap: gap === undefined ? style?.gap : resolveGap(gap, theme),
12
+ gap: gap === undefined ? style?.gap : resolveGap(gap, appearance),
13
13
  gridAutoFlow: flow ?? style?.gridAutoFlow,
14
14
  gridTemplateColumns: tracks(columns, "columns") ?? style?.gridTemplateColumns,
15
15
  gridTemplateRows: tracks(rows, "rows") ?? style?.gridTemplateRows,
package/dist/layout.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties } from "react";
2
- import type { ThemeProperties } from "@phreshos/core";
2
+ import type { Appearance } from "@phreshos/core";
3
3
  import { type Spacing } from "./spacing.js";
4
4
  /** Cross-axis alignment shared by layout containers. */
5
5
  export type LayoutAlignment = "start" | "center" | "end" | "stretch" | "baseline";
@@ -10,5 +10,5 @@ export type LayoutGap = Spacing;
10
10
  export declare function alignment(value: LayoutAlignment | undefined): CSSProperties["alignItems"] | undefined;
11
11
  export declare function justification(value: LayoutJustification | undefined): CSSProperties["justifyContent"] | undefined;
12
12
  /** Resolves semantic spacing while preserving explicit CSS gap values. */
13
- export declare function resolveGap(value: LayoutGap | undefined, theme: ThemeProperties | null): CSSProperties["gap"];
13
+ export declare function resolveGap(value: LayoutGap | undefined, appearance: Appearance | null): CSSProperties["gap"];
14
14
  export declare function tracks(value: number | string | undefined, property: "columns" | "rows"): string | undefined;
package/dist/layout.js CHANGED
@@ -21,8 +21,8 @@ export function justification(value) {
21
21
  return value === undefined ? undefined : justifications[value];
22
22
  }
23
23
  /** Resolves semantic spacing while preserving explicit CSS gap values. */
24
- export function resolveGap(value, theme) {
25
- return resolveSpacing(value, theme);
24
+ export function resolveGap(value, appearance) {
25
+ return resolveSpacing(value, appearance);
26
26
  }
27
27
  export function tracks(value, property) {
28
28
  if (value === undefined || typeof value === "string")
package/dist/main.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Components enter this surface only after their implementation-independent
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
- export { ThemeProvider, useTheme, type ThemeProviderProps } from "./theme-provider.js";
7
+ export { AppearanceProvider, useAppearance, useResolveTheme, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
8
8
  export { Flex, type FlexProps } from "./flex.js";
9
9
  export { Grid, type GridProps } from "./grid.js";
10
10
  export { Surface, type SurfaceColor, type SurfaceProps } from "./surface.js";
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Components enter this surface only after their implementation-independent
5
5
  * behavior contract has been established by the package's tests.
6
6
  */
7
- export { ThemeProvider, useTheme } from "./theme-provider.js";
7
+ export { AppearanceProvider, useAppearance, useResolveTheme, useTheme } from "./appearance-provider.js";
8
8
  export { Flex } from "./flex.js";
9
9
  export { Grid } from "./grid.js";
10
10
  export { Surface } from "./surface.js";
package/dist/radius.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { CSSProperties } from "react";
2
- import type { Shapeable, ThemeProperties } from "@phreshos/core";
2
+ import type { Appearance, Shapeable } from "@phreshos/core";
3
3
  import { type ScaleLevel } from "./scale.js";
4
- /** A Theme-derived level, pixel value, or explicit CSS corner radius. */
4
+ /** An Appearance-derived level, pixel value, or explicit CSS corner radius. */
5
5
  export type Radius = ScaleLevel | number | (string & {});
6
6
  /** Shared semantic corner-radius capability for React UI components. */
7
7
  export interface RadiusProps extends Shapeable<Radius> {
8
8
  }
9
9
  /** Resolves a Radius while preserving explicit CSS and pixel values. */
10
- export declare function resolveRadius(value: Radius | undefined, theme: ThemeProperties | null): CSSProperties["borderRadius"];
10
+ export declare function resolveRadius(value: Radius | undefined, appearance: Appearance | null): CSSProperties["borderRadius"];
package/dist/radius.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { isScaleLevel, scale } from "./scale.js";
2
2
  /** Resolves a Radius while preserving explicit CSS and pixel values. */
3
- export function resolveRadius(value, theme) {
3
+ export function resolveRadius(value, appearance) {
4
4
  if (!isScaleLevel(value))
5
5
  return value;
6
- if (!theme)
7
- throw new Error("A semantic radius requires a ThemeProvider");
8
- return scale(theme.radius, value);
6
+ if (!appearance)
7
+ throw new Error("A semantic radius requires an AppearanceProvider");
8
+ return scale(appearance.radius.light, value);
9
9
  }
package/dist/scale.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- /** A visual level derived around one concrete Theme value. */
1
+ /** A visual level derived around one concrete Appearance value. */
2
2
  export type ScaleLevel = "xsmall" | "small" | "medium" | "large" | "xlarge";
3
- /** Every visual level derived around one concrete Theme value. */
3
+ /** Every visual level derived around one concrete Appearance value. */
4
4
  export type NumericScale = Readonly<Record<ScaleLevel, number>>;
5
5
  /** Returns whether a value names a React UI visual level. */
6
6
  export declare function isScaleLevel(value: unknown): value is ScaleLevel;
@@ -10,5 +10,5 @@ export declare function scale(value: number, level: ScaleLevel): number;
10
10
  export declare function numericScale(value: number): NumericScale;
11
11
  /** Derives a multiplier while preserving its neutral value of one. */
12
12
  export declare function scaleMultiplier(value: number, level: ScaleLevel): number;
13
- /** Returns every visual level derived from one concrete Theme value. */
13
+ /** Returns every visual level derived from one concrete Appearance value. */
14
14
  export declare function useScale(value: number): NumericScale;
package/dist/scale.js CHANGED
@@ -28,7 +28,7 @@ export function numericScale(value) {
28
28
  export function scaleMultiplier(value, level) {
29
29
  return 1 + (value - 1) * factors[level];
30
30
  }
31
- /** Returns every visual level derived from one concrete Theme value. */
31
+ /** Returns every visual level derived from one concrete Appearance value. */
32
32
  export function useScale(value) {
33
33
  return useMemo(() => numericScale(value), [value]);
34
34
  }
package/dist/spacing.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { CSSProperties } from "react";
2
- import type { ThemeProperties } from "@phreshos/core";
2
+ import type { Appearance } from "@phreshos/core";
3
3
  import { type ScaleLevel } from "./scale.js";
4
- /** A Theme-derived level, pixel value, or explicit CSS spacing value. */
4
+ /** An Appearance-derived level, pixel value, or explicit CSS spacing value. */
5
5
  export type Spacing = ScaleLevel | number | (string & {});
6
6
  /** Resolves spacing while preserving explicit CSS and pixel values. */
7
- export declare function resolveSpacing(value: ScaleLevel, theme: ThemeProperties | null): number;
8
- export declare function resolveSpacing(value: Spacing | undefined, theme: ThemeProperties | null): CSSProperties["gap"];
7
+ export declare function resolveSpacing(value: ScaleLevel, appearance: Appearance | null): number;
8
+ export declare function resolveSpacing(value: Spacing | undefined, appearance: Appearance | null): CSSProperties["gap"];
package/dist/spacing.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { isScaleLevel, scale } from "./scale.js";
2
- export function resolveSpacing(value, theme) {
2
+ export function resolveSpacing(value, appearance) {
3
3
  if (!isScaleLevel(value))
4
4
  return value;
5
- if (!theme)
6
- throw new Error("Semantic spacing requires a ThemeProvider");
7
- return scale(theme.spacing, value);
5
+ if (!appearance)
6
+ throw new Error("Semantic spacing requires an AppearanceProvider");
7
+ return scale(appearance.spacing.light, value);
8
8
  }
package/dist/surface.d.ts CHANGED
@@ -1,51 +1,51 @@
1
1
  import type { ComponentPropsWithoutRef } from "react";
2
2
  import { type ColorLevel } from "./color.js";
3
3
  import { type ScaleLevel } from "./scale.js";
4
- /** A Theme-derived treatment or direct CSS color. */
4
+ /** An Appearance-derived treatment or direct CSS color. */
5
5
  export type SurfaceColor = ColorLevel | (string & {});
6
6
  /** Native div properties plus controls for the locally owned material. */
7
7
  export type SurfaceProps = Omit<ComponentPropsWithoutRef<"div">, "color" | "opacity"> & Readonly<{
8
- /** Theme-derived treatment or direct CSS material color. */
8
+ /** Appearance-derived treatment or direct CSS material color. */
9
9
  color?: SurfaceColor;
10
- /** Theme-derived level or direct grain intensity from zero to one. */
10
+ /** Appearance-derived level or direct grain intensity from zero to one. */
11
11
  grain?: ScaleLevel | number;
12
- /** Theme-derived level or direct retained grain amount from zero to one. */
12
+ /** Appearance-derived level or direct retained grain amount from zero to one. */
13
13
  grainAmount?: ScaleLevel | number;
14
- /** Theme-derived level or direct backdrop blur from zero to 24 CSS pixels. */
14
+ /** Appearance-derived level or direct backdrop blur from zero to 24 CSS pixels. */
15
15
  backdrop?: ScaleLevel | number;
16
- /** Theme-derived level or direct material opacity from zero to one. */
16
+ /** Appearance-derived level or direct material opacity from zero to one. */
17
17
  opacity?: ScaleLevel | number;
18
- /** Theme-derived level or direct organic displacement from zero to 140 pixels. */
18
+ /** Appearance-derived level or direct organic displacement from zero to 140 pixels. */
19
19
  distortion?: ScaleLevel | number;
20
- /** Theme-derived level or direct directional displacement from zero to 40 pixels. */
20
+ /** Appearance-derived level or direct directional displacement from zero to 40 pixels. */
21
21
  waves?: ScaleLevel | number;
22
- /** Theme-derived level or direct ripple displacement from zero to 40 pixels. */
22
+ /** Appearance-derived level or direct ripple displacement from zero to 40 pixels. */
23
23
  ripples?: ScaleLevel | number;
24
- /** Theme-derived level or direct backdrop saturation multiplier. */
24
+ /** Appearance-derived level or direct backdrop saturation multiplier. */
25
25
  saturation?: ScaleLevel | number;
26
- /** Theme-derived level or direct backdrop brightness multiplier. */
26
+ /** Appearance-derived level or direct backdrop brightness multiplier. */
27
27
  brightness?: ScaleLevel | number;
28
28
  }>;
29
29
  /** Contains content above locally owned Surface material layers. */
30
- export declare const Surface: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "color" | "opacity"> & Readonly<{
31
- /** Theme-derived treatment or direct CSS material color. */
30
+ export declare const Surface: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "opacity" | "color"> & Readonly<{
31
+ /** Appearance-derived treatment or direct CSS material color. */
32
32
  color?: SurfaceColor;
33
- /** Theme-derived level or direct grain intensity from zero to one. */
33
+ /** Appearance-derived level or direct grain intensity from zero to one. */
34
34
  grain?: ScaleLevel | number;
35
- /** Theme-derived level or direct retained grain amount from zero to one. */
35
+ /** Appearance-derived level or direct retained grain amount from zero to one. */
36
36
  grainAmount?: ScaleLevel | number;
37
- /** Theme-derived level or direct backdrop blur from zero to 24 CSS pixels. */
37
+ /** Appearance-derived level or direct backdrop blur from zero to 24 CSS pixels. */
38
38
  backdrop?: ScaleLevel | number;
39
- /** Theme-derived level or direct material opacity from zero to one. */
39
+ /** Appearance-derived level or direct material opacity from zero to one. */
40
40
  opacity?: ScaleLevel | number;
41
- /** Theme-derived level or direct organic displacement from zero to 140 pixels. */
41
+ /** Appearance-derived level or direct organic displacement from zero to 140 pixels. */
42
42
  distortion?: ScaleLevel | number;
43
- /** Theme-derived level or direct directional displacement from zero to 40 pixels. */
43
+ /** Appearance-derived level or direct directional displacement from zero to 40 pixels. */
44
44
  waves?: ScaleLevel | number;
45
- /** Theme-derived level or direct ripple displacement from zero to 40 pixels. */
45
+ /** Appearance-derived level or direct ripple displacement from zero to 40 pixels. */
46
46
  ripples?: ScaleLevel | number;
47
- /** Theme-derived level or direct backdrop saturation multiplier. */
47
+ /** Appearance-derived level or direct backdrop saturation multiplier. */
48
48
  saturation?: ScaleLevel | number;
49
- /** Theme-derived level or direct backdrop brightness multiplier. */
49
+ /** Appearance-derived level or direct backdrop brightness multiplier. */
50
50
  brightness?: ScaleLevel | number;
51
51
  }> & import("react").RefAttributes<HTMLDivElement>>;
package/dist/surface.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useCallback, useId, useLayoutEffect, useRef } from "react";
3
- import { themeLimits } from "@phreshos/core";
3
+ import { appearanceLimits } from "@phreshos/core";
4
4
  import { color as deriveColor } from "./color.js";
5
5
  import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
6
6
  import { SurfaceMaterial } from "./surface-material.js";
7
- import { useTheme } from "./theme-provider.js";
7
+ import { useAppearance, useResolveTheme } from "./appearance-provider.js";
8
8
  const layerStyle = {
9
9
  position: "absolute",
10
10
  inset: 0,
@@ -13,7 +13,11 @@ const layerStyle = {
13
13
  };
14
14
  /** Contains content above locally owned Surface material layers. */
15
15
  export const Surface = forwardRef(function Surface({ backdrop, brightness, children, color, distortion, grain, grainAmount, opacity, ripples, saturation, style, waves, ...properties }, forwardedRef) {
16
- const theme = useTheme();
16
+ const appearance = useAppearance();
17
+ const background = useResolveTheme(appearance.background);
18
+ const foreground = useResolveTheme(appearance.foreground);
19
+ const radius = useResolveTheme(appearance.radius);
20
+ const surface = useResolveTheme(appearance.surface);
17
21
  const identity = `phresh-surface-${useId().replaceAll(":", "")}`;
18
22
  const element = useRef(null);
19
23
  const capture = useCallback((node) => {
@@ -23,17 +27,17 @@ export const Surface = forwardRef(function Surface({ backdrop, brightness, child
23
27
  else if (forwardedRef)
24
28
  forwardedRef.current = node;
25
29
  }, [forwardedRef]);
26
- const resolved = resolveSurface({ backdrop, brightness, color, distortion, grain, grainAmount, opacity, ripples, saturation, waves }, theme);
30
+ const resolved = resolveSurface({ backdrop, brightness, color, distortion, grain, grainAmount, opacity, ripples, saturation, waves }, background, surface);
27
31
  useLayoutEffect(() => {
28
32
  const surface = element.current;
29
33
  if (surface)
30
34
  return prepareSurfaceLayout(surface);
31
35
  });
32
36
  return _jsxs("div", { ...properties, ref: capture, style: {
33
- borderRadius: theme.radius,
34
- color: theme.foreground,
37
+ borderRadius: radius,
38
+ color: foreground,
35
39
  ...style
36
- }, children: [resolved.refracts && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), resolved.frost && _jsx(BackdropLayer, { name: "frost", filter: resolved.frost, zIndex: -2 }), _jsx(SurfaceBorder, { color: resolved.material.color, opacity: resolveScale("large", resolved.material.opacity, themeLimits.surface.opacity) }), _jsx(SurfaceMaterial, { identity: identity, ...resolved.material }), children] });
40
+ }, children: [resolved.refracts && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), resolved.frost && _jsx(BackdropLayer, { name: "frost", filter: resolved.frost, zIndex: -2 }), _jsx(SurfaceBorder, { color: resolved.material.color, opacity: resolveScale("large", resolved.material.opacity, appearanceLimits.surface.opacity) }), _jsx(SurfaceMaterial, { identity: identity, ...resolved.material }), children] });
37
41
  });
38
42
  /** Draws one uniform inset edge from the same color as the Surface material. */
39
43
  function SurfaceBorder({ color, opacity }) {
@@ -55,19 +59,19 @@ function BackdropLayer({ filter, name, zIndex }) {
55
59
  WebkitBackdropFilter: filter
56
60
  } });
57
61
  }
58
- function resolveSurface(values, theme) {
62
+ function resolveSurface(values, background, surface) {
59
63
  const material = {
60
- color: resolveColor(values.color, theme.background),
61
- distortion: resolveScale(values.distortion, theme.surface.distortion, themeLimits.surface.distortion),
62
- grain: resolveScale(values.grain, theme.surface.grain, themeLimits.surface.grain),
63
- grainAmount: resolveScale(values.grainAmount, theme.surface.grainAmount, themeLimits.surface.grainAmount),
64
- opacity: resolveScale(values.opacity, theme.surface.opacity, themeLimits.surface.opacity),
65
- ripples: resolveScale(values.ripples, theme.surface.ripples, themeLimits.surface.ripples),
66
- waves: resolveScale(values.waves, theme.surface.waves, themeLimits.surface.waves)
64
+ color: resolveColor(values.color, background),
65
+ distortion: resolveScale(values.distortion, surface.distortion, appearanceLimits.surface.distortion),
66
+ grain: resolveScale(values.grain, surface.grain, appearanceLimits.surface.grain),
67
+ grainAmount: resolveScale(values.grainAmount, surface.grainAmount, appearanceLimits.surface.grainAmount),
68
+ opacity: resolveScale(values.opacity, surface.opacity, appearanceLimits.surface.opacity),
69
+ ripples: resolveScale(values.ripples, surface.ripples, appearanceLimits.surface.ripples),
70
+ waves: resolveScale(values.waves, surface.waves, appearanceLimits.surface.waves)
67
71
  };
68
- const backdrop = resolveScale(values.backdrop, theme.surface.backdrop, themeLimits.surface.backdrop);
69
- const saturation = resolveMultiplier(values.saturation, theme.surface.saturation, themeLimits.surface.saturation);
70
- const brightness = resolveMultiplier(values.brightness, theme.surface.brightness, themeLimits.surface.brightness);
72
+ const backdrop = resolveScale(values.backdrop, surface.backdrop, appearanceLimits.surface.backdrop);
73
+ const saturation = resolveMultiplier(values.saturation, surface.saturation, appearanceLimits.surface.saturation);
74
+ const brightness = resolveMultiplier(values.brightness, surface.brightness, appearanceLimits.surface.brightness);
71
75
  const frost = [
72
76
  backdrop === 0 ? "" : `blur(${backdrop}px)`,
73
77
  saturation === 1 ? "" : `saturate(${saturation})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -52,7 +52,7 @@
52
52
  "prepack": "node --run test && node --run build"
53
53
  },
54
54
  "peerDependencies": {
55
- "@phreshos/core": "^0.1.16",
55
+ "@phreshos/core": "^0.1.17",
56
56
  "react": "^19.2.0",
57
57
  "react-dom": "^19.2.0"
58
58
  },
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@base-ui/react": "^1.7.0",
64
- "@phreshos/core": "^0.1.16",
64
+ "@phreshos/core": "^0.1.17",
65
65
  "@testing-library/dom": "^10.4.1",
66
66
  "@testing-library/react": "^16.3.0",
67
67
  "@testing-library/user-event": "^14.6.1",
@@ -1,15 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { ThemeProperties } from "@phreshos/core";
3
- /** Provides the nearest Theme value to one React subtree. */
4
- export declare function ThemeProvider({ children, theme }: ThemeProviderProps): import("react").JSX.Element;
5
- /** Returns the complete snapshot supplied by the nearest ThemeProvider. */
6
- export declare function useTheme(): ThemeProperties;
7
- /** Internal optional read used by primitives with both raw and themed values. */
8
- export declare function useThemeIfAvailable(): ThemeProperties | null;
9
- /** Properties accepted by ThemeProvider. */
10
- export interface ThemeProviderProps {
11
- /** Content that receives this Theme instead of any outer Theme. */
12
- readonly children: ReactNode;
13
- /** Complete Theme value for this subtree. */
14
- readonly theme: ThemeProperties;
15
- }
@@ -1,20 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from "react";
3
- const missing = Symbol("ThemeProvider");
4
- const ThemeContext = createContext(missing);
5
- /** Provides the nearest Theme value to one React subtree. */
6
- export function ThemeProvider({ children, theme }) {
7
- return _jsx(ThemeContext.Provider, { value: theme, children: children });
8
- }
9
- /** Returns the complete snapshot supplied by the nearest ThemeProvider. */
10
- export function useTheme() {
11
- const properties = useContext(ThemeContext);
12
- if (properties === missing)
13
- throw new Error("useTheme() requires a ThemeProvider");
14
- return properties;
15
- }
16
- /** Internal optional read used by primitives with both raw and themed values. */
17
- export function useThemeIfAvailable() {
18
- const properties = useContext(ThemeContext);
19
- return properties === missing ? null : properties;
20
- }