@phreshos/react-ui 0.1.0
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/LICENSE +19 -0
- package/README.md +214 -0
- package/dist/button.d.ts +25 -0
- package/dist/button.js +69 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +5 -0
- package/dist/flex.d.ts +19 -0
- package/dist/flex.js +17 -0
- package/dist/glass-surface.d.ts +34 -0
- package/dist/glass-surface.js +40 -0
- package/dist/grid.d.ts +21 -0
- package/dist/grid.js +18 -0
- package/dist/icons/main.d.ts +7 -0
- package/dist/icons/main.js +7 -0
- package/dist/layout.d.ts +14 -0
- package/dist/layout.js +34 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +13 -0
- package/dist/radius.d.ts +10 -0
- package/dist/radius.js +9 -0
- package/dist/spacing.d.ts +8 -0
- package/dist/spacing.js +8 -0
- package/dist/theme-provider.d.ts +29 -0
- package/dist/theme-provider.js +20 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Zohayr SLILEH
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# `@phreshos/react-ui`
|
|
2
|
+
|
|
3
|
+
A React component library for coherent PhreshOS Program interfaces.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
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 the
|
|
72
|
+
general React SDK hook instead:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { useScale } from "@phreshos/react"
|
|
76
|
+
|
|
77
|
+
const spacing = useScale(theme.spacing)
|
|
78
|
+
|
|
79
|
+
<section style={{ gap: spacing.large }} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Explicit values such as `4rem` are used directly — passing them through a
|
|
83
|
+
Theme hook would perform no additional work.
|
|
84
|
+
|
|
85
|
+
The Theme stores unrestricted CSS background, foreground, and accent sources.
|
|
86
|
+
Core derives the fixed `subtle`, `soft`, `base`, `strong`, and `intense`
|
|
87
|
+
treatments from any supplied color, preserving the value exactly at `base`.
|
|
88
|
+
`useColor(value)` in the React SDK memoizes that calculation without
|
|
89
|
+
implicitly choosing a Theme property. CSS performs the nearby mixing in
|
|
90
|
+
OKLCH, so React UI does not own or persist a parallel palette of its own:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { useColor } from "@phreshos/react"
|
|
94
|
+
|
|
95
|
+
const colors = useColor(theme.accent)
|
|
96
|
+
|
|
97
|
+
<strong style={{ color: colors.strong }} />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`GlassSurface` is the shared translucent material. The Theme supplies its
|
|
101
|
+
background, foreground, and concrete default values for distortion, blur,
|
|
102
|
+
saturation, brightness, and material opacity. The component derives its tint
|
|
103
|
+
from the background and applies the foreground to its content, while accent
|
|
104
|
+
remains independent for emphasis and interaction. The surface may also derive
|
|
105
|
+
a small or large treatment from each numeric default without turning those
|
|
106
|
+
levels into system state.
|
|
107
|
+
|
|
108
|
+
Derived opacity is capped at thirty percent and never fades the surface's
|
|
109
|
+
content. Layout, spacing, radius, and external elevation remain ordinary
|
|
110
|
+
container concerns:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<GlassSurface className="rounded-xl p-3">
|
|
114
|
+
...
|
|
115
|
+
</GlassSurface>
|
|
116
|
+
|
|
117
|
+
<GlassSurface color="soft" distortion="large" blur="small" opacity="medium">
|
|
118
|
+
...
|
|
119
|
+
</GlassSurface>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Shape-owning components accept the shared `Radius` value directly. Semantic
|
|
123
|
+
levels are derived from the Theme's concrete radius through the same
|
|
124
|
+
`scale()` rule used for spacing, while numbers and CSS values remain explicit
|
|
125
|
+
overrides:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<GlassSurface radius="large">...</GlassSurface>
|
|
129
|
+
<GlassSurface radius="2rem">...</GlassSurface>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Structures whose native element owns the shape derive from the explicit
|
|
133
|
+
radius value through the same general React SDK hook:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useScale } from "@phreshos/react"
|
|
137
|
+
|
|
138
|
+
const radius = useScale(theme.radius)
|
|
139
|
+
|
|
140
|
+
<section style={{ borderRadius: radius.large }} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`Button` is the library's first interactive primitive. Its translucent
|
|
144
|
+
control treatment matches the desktop's Start and sign-out controls, letting
|
|
145
|
+
the surrounding Theme material remain visible. It derives spacing and radius
|
|
146
|
+
from the Theme while keeping a single activation path across pointer, Enter,
|
|
147
|
+
and Space input:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<Button onPress={save}>Save</Button>
|
|
151
|
+
<Button size="large" pending>Saving</Button>
|
|
152
|
+
<Button disabled>Unavailable</Button>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Pending Buttons remain focusable but cannot activate. Disabled Buttons leave
|
|
156
|
+
the focus order entirely. The native element defaults to `type="button"`, so
|
|
157
|
+
placing it inside a form never triggers an accidental submission.
|
|
158
|
+
|
|
159
|
+
`ThemeProvider` accepts a plain `ThemeProperties` snapshot, so the library
|
|
160
|
+
remains usable without either environment SDK. A Program can adapt its
|
|
161
|
+
observable Host value at the application boundary:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { HostProvider, useHostTheme } from "@phreshos/react"
|
|
165
|
+
import { ThemeProvider } from "@phreshos/react-ui"
|
|
166
|
+
|
|
167
|
+
function ThemedApplication({ children }) {
|
|
168
|
+
const theme = useHostTheme()
|
|
169
|
+
|
|
170
|
+
return <ThemeProvider theme={theme}>{children}</ThemeProvider>
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
<HostProvider provide={["theme"]} fallback={null}>
|
|
174
|
+
<ThemedApplication>{children}</ThemedApplication>
|
|
175
|
+
</HostProvider>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Standing requirements
|
|
179
|
+
|
|
180
|
+
- Components preserve one recognizable visual identity.
|
|
181
|
+
- Props that select a semantic treatment accept only the values documented by
|
|
182
|
+
that component. Explicit native styles and supported CSS spacing, radius,
|
|
183
|
+
and color values remain available where the component contract allows them.
|
|
184
|
+
- `ThemeProvider` requires an explicit `theme` prop; it never discovers an
|
|
185
|
+
environment SDK or silently selects a global theme.
|
|
186
|
+
- The provider applies a replacement `theme` value immediately to its
|
|
187
|
+
descendants.
|
|
188
|
+
- Providers are scoped and nestable. The nearest `ThemeProvider` supplies the
|
|
189
|
+
complete theme for its descendants without affecting its parent or
|
|
190
|
+
siblings.
|
|
191
|
+
- Accessibility, keyboard behavior, focus, and form behavior are contractual.
|
|
192
|
+
- Components must work inside structurally isolated Program iframes.
|
|
193
|
+
- Public types and JSDoc are part of the product.
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bun install --frozen-lockfile
|
|
199
|
+
bun run verify
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`verify` type-checks the source and tests, runs the behavior suite, rebuilds the
|
|
203
|
+
package, packs the publication artifact, installs it into a temporary consumer,
|
|
204
|
+
and checks its runtime, TypeScript, and public subpath entry points.
|
|
205
|
+
|
|
206
|
+
`Button` is the only interactive component currently exported. Private
|
|
207
|
+
acceptance suites also compare Field, Select, Dialog, and Context Menu
|
|
208
|
+
candidates against the same implementation-independent behaviors; those
|
|
209
|
+
candidates are not part of the package's public surface. Field covers labeling,
|
|
210
|
+
descriptions, validation, native states, and value changes. Select covers
|
|
211
|
+
collections, keyboard input, disabled options, form submission, and cleanup.
|
|
212
|
+
Dialog covers modal semantics, focus, dismissal, nesting, state changes, and
|
|
213
|
+
cleanup. Context Menu covers invocation, focus, actions, disabled items,
|
|
214
|
+
dismissal, and cleanup.
|
package/dist/button.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { ButtonProps as AriaButtonProps } from "react-aria-components";
|
|
3
|
+
import { type ScaleLevel } from "@phreshos/core";
|
|
4
|
+
import { type RadiusProps } from "./radius.js";
|
|
5
|
+
type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "isDisabled" | "isPending" | "onClick" | "onPress" | "style">;
|
|
6
|
+
/** Properties accepted by the shared interactive button. */
|
|
7
|
+
export interface ButtonProps extends NativeButtonProps, RadiusProps {
|
|
8
|
+
/** Visible Button content. */
|
|
9
|
+
readonly children?: ReactNode;
|
|
10
|
+
/** Native class name applied without replacing the component contract. */
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
/** Prevents focus and activation. */
|
|
13
|
+
readonly disabled?: boolean;
|
|
14
|
+
/** Prevents activation while keeping the Button focusable. */
|
|
15
|
+
readonly pending?: boolean;
|
|
16
|
+
/** Runs once for a normalized pointer, Enter, or Space activation. */
|
|
17
|
+
readonly onPress?: () => void;
|
|
18
|
+
/** Derives the Button's spacing from the Theme's concrete default. */
|
|
19
|
+
readonly size?: ScaleLevel;
|
|
20
|
+
/** Additional native styles that do not replace the Button's identity. */
|
|
21
|
+
readonly style?: CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
/** A Theme-aware action with normalized pointer and keyboard behavior. */
|
|
24
|
+
export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
25
|
+
export {};
|
package/dist/button.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Button as AriaButton } from "react-aria-components";
|
|
4
|
+
import { scale } from "@phreshos/core";
|
|
5
|
+
import { resolveRadius } from "./radius.js";
|
|
6
|
+
import { useTheme } from "./theme-provider.js";
|
|
7
|
+
/** A Theme-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 theme = useTheme();
|
|
10
|
+
const spacing = scale(theme.spacing, size);
|
|
11
|
+
const borderRadius = resolveRadius(radius, theme);
|
|
12
|
+
return _jsx(AriaButton, { ...properties, ref: ref, type: type, isDisabled: disabled, isPending: pending, onPress: onPress, style: ({ isFocusVisible, isHovered, isPressed }) => buttonStyle({
|
|
13
|
+
borderRadius,
|
|
14
|
+
disabled,
|
|
15
|
+
isFocusVisible,
|
|
16
|
+
isHovered,
|
|
17
|
+
isPressed,
|
|
18
|
+
pending,
|
|
19
|
+
size,
|
|
20
|
+
spacing,
|
|
21
|
+
foreground: theme.foreground,
|
|
22
|
+
style
|
|
23
|
+
}), children: children });
|
|
24
|
+
});
|
|
25
|
+
function buttonStyle({ borderRadius, disabled, isFocusVisible, isHovered, isPressed, pending, size, spacing, foreground, style }) {
|
|
26
|
+
const fontSize = buttonFontSizes[size];
|
|
27
|
+
const height = Math.max(size === "xsmall" ? 24 : 28, 20 + spacing);
|
|
28
|
+
return {
|
|
29
|
+
...style,
|
|
30
|
+
appearance: "none",
|
|
31
|
+
display: "inline-grid",
|
|
32
|
+
gridAutoFlow: "column",
|
|
33
|
+
gridAutoColumns: "max-content",
|
|
34
|
+
placeItems: "center",
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
minWidth: 0,
|
|
37
|
+
height,
|
|
38
|
+
paddingBlock: 0,
|
|
39
|
+
paddingInline: Math.max(8, spacing * 2 / 3),
|
|
40
|
+
gap: Math.max(4, spacing / 2),
|
|
41
|
+
border: "1px solid rgba(255, 255, 255, 0.45)",
|
|
42
|
+
borderRadius,
|
|
43
|
+
outline: "none",
|
|
44
|
+
color: foreground,
|
|
45
|
+
backgroundColor: `rgba(255, 255, 255, ${isPressed ? 0.42 : isHovered ? 0.5 : 0.3})`,
|
|
46
|
+
boxShadow: isFocusVisible
|
|
47
|
+
? "0 0 0 2px rgba(255, 255, 255, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.8)"
|
|
48
|
+
: "inset 0 1px 0 rgba(255, 255, 255, 0.8)",
|
|
49
|
+
opacity: disabled ? 0.46 : pending ? 0.68 : 1,
|
|
50
|
+
transform: isPressed ? "scale(0.95)" : "scale(1)",
|
|
51
|
+
transition: "background-color 100ms ease, box-shadow 100ms ease, opacity 100ms ease, transform 100ms ease",
|
|
52
|
+
cursor: disabled ? "not-allowed" : pending ? "progress" : "pointer",
|
|
53
|
+
font: "inherit",
|
|
54
|
+
fontSize,
|
|
55
|
+
fontWeight: 650,
|
|
56
|
+
lineHeight: 1,
|
|
57
|
+
textAlign: "center",
|
|
58
|
+
textDecoration: "none",
|
|
59
|
+
userSelect: "none",
|
|
60
|
+
WebkitTapHighlightColor: "transparent"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const buttonFontSizes = Object.freeze({
|
|
64
|
+
xsmall: 10,
|
|
65
|
+
small: 11,
|
|
66
|
+
medium: 12,
|
|
67
|
+
large: 13,
|
|
68
|
+
xlarge: 14
|
|
69
|
+
});
|
package/dist/color.d.ts
ADDED
package/dist/color.js
ADDED
package/dist/flex.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
3
|
+
/** Properties accepted by the Flex layout primitive. */
|
|
4
|
+
export interface FlexProps extends ComponentPropsWithoutRef<"div"> {
|
|
5
|
+
/** Main-axis direction. The browser default is `row`. */
|
|
6
|
+
readonly direction?: "row" | "row-reverse" | "column" | "column-reverse";
|
|
7
|
+
/** Cross-axis alignment of the children. */
|
|
8
|
+
readonly align?: LayoutAlignment;
|
|
9
|
+
/** Distribution of children along the main axis. */
|
|
10
|
+
readonly justify?: LayoutJustification;
|
|
11
|
+
/** Space between children. Numbers are pixels. */
|
|
12
|
+
readonly gap?: LayoutGap;
|
|
13
|
+
/** Enables wrapping, or reverses the wrapped cross-axis order. */
|
|
14
|
+
readonly wrap?: boolean | "reverse";
|
|
15
|
+
/** Uses `inline-flex` instead of `flex`. */
|
|
16
|
+
readonly inline?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A predictable Flexbox container with no visual appearance of its own. */
|
|
19
|
+
export declare const Flex: import("react").ForwardRefExoticComponent<FlexProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/flex.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { alignment, justification, resolveGap } from "./layout.js";
|
|
4
|
+
import { useThemeIfAvailable } from "./theme-provider.js";
|
|
5
|
+
/** A predictable Flexbox container with no visual appearance of its own. */
|
|
6
|
+
export const Flex = forwardRef(function Flex({ align, direction, gap, inline = false, justify, style, wrap, ...properties }, ref) {
|
|
7
|
+
const theme = useThemeIfAvailable();
|
|
8
|
+
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
|
+
...style,
|
|
10
|
+
display: inline ? "inline-flex" : "flex",
|
|
11
|
+
alignItems: alignment(align) ?? style?.alignItems,
|
|
12
|
+
flexDirection: direction ?? style?.flexDirection,
|
|
13
|
+
flexWrap: wrap === undefined ? style?.flexWrap : wrap === "reverse" ? "wrap-reverse" : wrap ? "wrap" : "nowrap",
|
|
14
|
+
gap: gap === undefined ? style?.gap : resolveGap(gap, theme),
|
|
15
|
+
justifyContent: justification(justify) ?? style?.justifyContent
|
|
16
|
+
} });
|
|
17
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { type Colorable, type ColorLevel, type ScaleLevel } from "@phreshos/core";
|
|
3
|
+
import { type RadiusProps } from "./radius.js";
|
|
4
|
+
/** Native properties accepted by the shared glass container. */
|
|
5
|
+
export type GlassSurfaceProps = ComponentPropsWithoutRef<"div"> & Colorable<ColorLevel> & RadiusProps & Readonly<{
|
|
6
|
+
/** Distortion derived from the Theme's concrete default. */
|
|
7
|
+
distortion?: ScaleLevel;
|
|
8
|
+
/** Blur derived from the Theme's concrete default. */
|
|
9
|
+
blur?: ScaleLevel;
|
|
10
|
+
/** Saturation derived from the Theme's concrete default. */
|
|
11
|
+
saturation?: ScaleLevel;
|
|
12
|
+
/** Brightness derived from the Theme's concrete default. */
|
|
13
|
+
brightness?: ScaleLevel;
|
|
14
|
+
/** Material opacity derived from the Theme's concrete default. */
|
|
15
|
+
opacity?: ScaleLevel;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Contains content within the shared refracted glass material.
|
|
19
|
+
*
|
|
20
|
+
* Layout, spacing, radius, and external elevation remain the responsibility of
|
|
21
|
+
* the containing interface. Only the material itself is owned here.
|
|
22
|
+
*/
|
|
23
|
+
export declare const GlassSurface: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & Colorable<ColorLevel> & RadiusProps & Readonly<{
|
|
24
|
+
/** Distortion derived from the Theme's concrete default. */
|
|
25
|
+
distortion?: ScaleLevel;
|
|
26
|
+
/** Blur derived from the Theme's concrete default. */
|
|
27
|
+
blur?: ScaleLevel;
|
|
28
|
+
/** Saturation derived from the Theme's concrete default. */
|
|
29
|
+
saturation?: ScaleLevel;
|
|
30
|
+
/** Brightness derived from the Theme's concrete default. */
|
|
31
|
+
brightness?: ScaleLevel;
|
|
32
|
+
/** Material opacity derived from the Theme's concrete default. */
|
|
33
|
+
opacity?: ScaleLevel;
|
|
34
|
+
}> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import { color as deriveColor, scale, scaleMultiplier, themeLimits } from "@phreshos/core";
|
|
4
|
+
import { colorOpacity } from "./color.js";
|
|
5
|
+
import { resolveRadius } from "./radius.js";
|
|
6
|
+
import { useTheme } from "./theme-provider.js";
|
|
7
|
+
const shell = "inset 1px 1px 0 rgba(255, 255, 255, 0.7), inset -1px -1px 0 rgba(255, 255, 255, 0.28)";
|
|
8
|
+
/**
|
|
9
|
+
* Contains content within the shared refracted glass material.
|
|
10
|
+
*
|
|
11
|
+
* Layout, spacing, radius, and external elevation remain the responsibility of
|
|
12
|
+
* the containing interface. Only the material itself is owned here.
|
|
13
|
+
*/
|
|
14
|
+
export const GlassSurface = forwardRef(function GlassSurface({ blur = "medium", brightness = "medium", children, color = "base", distortion = "medium", opacity = "medium", radius, saturation = "medium", style, ...properties }, ref) {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const filter = `phresh-glass-${useId().replaceAll(":", "")}`;
|
|
17
|
+
const backdrop = glassBackdrop(theme.glass, { blur, brightness, saturation });
|
|
18
|
+
const alpha = boundedOpacity(scale(theme.glass.opacity, opacity));
|
|
19
|
+
const tint = deriveColor(theme.background)[color];
|
|
20
|
+
const borderRadius = resolveRadius(radius, theme);
|
|
21
|
+
return _jsxs("div", { ...properties, ref: ref, style: {
|
|
22
|
+
...style,
|
|
23
|
+
...(borderRadius === undefined ? {} : { borderRadius }),
|
|
24
|
+
color: theme.foreground,
|
|
25
|
+
backgroundColor: colorOpacity(tint, alpha),
|
|
26
|
+
backgroundImage: `linear-gradient(to bottom, ${colorOpacity(tint, boundedOpacity(alpha * 5 / 3))}, ${colorOpacity(tint, boundedOpacity(alpha / 2))})`,
|
|
27
|
+
boxShadow: shell,
|
|
28
|
+
backdropFilter: `url(#${filter}) ${backdrop}`,
|
|
29
|
+
WebkitBackdropFilter: backdrop,
|
|
30
|
+
isolation: "isolate",
|
|
31
|
+
overflow: "hidden"
|
|
32
|
+
}, children: [_jsx("svg", { "aria-hidden": "true", width: "0", height: "0", style: { position: "absolute" }, children: _jsxs("filter", { id: filter, x: "0%", y: "0%", width: "100%", height: "100%", children: [_jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "0.008 0.008", numOctaves: "2", seed: "92", result: "noise" }), _jsx("feGaussianBlur", { in: "noise", stdDeviation: "2", result: "blurred" }), _jsx("feDisplacementMap", { in: "SourceGraphic", in2: "blurred", scale: scale(theme.glass.distortion, distortion), xChannelSelector: "R", yChannelSelector: "G" })] }) }), children] });
|
|
33
|
+
});
|
|
34
|
+
function glassBackdrop(glass, levels) {
|
|
35
|
+
return `blur(${scale(glass.blur, levels.blur)}px) saturate(${scaleMultiplier(glass.saturation, levels.saturation)}) brightness(${scaleMultiplier(glass.brightness, levels.brightness)})`;
|
|
36
|
+
}
|
|
37
|
+
/** Keeps every derived tint within the system's maximum glass opacity. */
|
|
38
|
+
function boundedOpacity(value) {
|
|
39
|
+
return Math.round(Math.min(themeLimits.glass.opacity.maximum, Math.max(0, value)) * 10_000) / 10_000;
|
|
40
|
+
}
|
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, CSSProperties } from "react";
|
|
2
|
+
import type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
3
|
+
/** Properties accepted by the Grid layout primitive. */
|
|
4
|
+
export interface GridProps extends ComponentPropsWithoutRef<"div"> {
|
|
5
|
+
/** Equal-width column count or native CSS column-track expression. */
|
|
6
|
+
readonly columns?: number | string;
|
|
7
|
+
/** Equal-height row count or native CSS row-track expression. */
|
|
8
|
+
readonly rows?: number | string;
|
|
9
|
+
/** Automatic placement direction. */
|
|
10
|
+
readonly flow?: CSSProperties["gridAutoFlow"];
|
|
11
|
+
/** Cross-axis alignment of items inside their grid areas. */
|
|
12
|
+
readonly align?: LayoutAlignment;
|
|
13
|
+
/** Distribution of the grid tracks along the inline axis. */
|
|
14
|
+
readonly justify?: LayoutJustification;
|
|
15
|
+
/** Space between rows and columns. Numbers are pixels. */
|
|
16
|
+
readonly gap?: LayoutGap;
|
|
17
|
+
/** Uses `inline-grid` instead of `grid`. */
|
|
18
|
+
readonly inline?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A predictable CSS Grid container with no visual appearance of its own. */
|
|
21
|
+
export declare const Grid: import("react").ForwardRefExoticComponent<GridProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { alignment, justification, resolveGap, tracks } from "./layout.js";
|
|
4
|
+
import { useThemeIfAvailable } from "./theme-provider.js";
|
|
5
|
+
/** A predictable CSS Grid container with no visual appearance of its own. */
|
|
6
|
+
export const Grid = forwardRef(function Grid({ align, columns, flow, gap, inline = false, justify, rows, style, ...properties }, ref) {
|
|
7
|
+
const theme = useThemeIfAvailable();
|
|
8
|
+
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
|
+
...style,
|
|
10
|
+
display: inline ? "inline-grid" : "grid",
|
|
11
|
+
alignItems: alignment(align) ?? style?.alignItems,
|
|
12
|
+
gap: gap === undefined ? style?.gap : resolveGap(gap, theme),
|
|
13
|
+
gridAutoFlow: flow ?? style?.gridAutoFlow,
|
|
14
|
+
gridTemplateColumns: tracks(columns, "columns") ?? style?.gridTemplateColumns,
|
|
15
|
+
gridTemplateRows: tracks(rows, "rows") ?? style?.gridTemplateRows,
|
|
16
|
+
justifyContent: justification(justify) ?? style?.justifyContent
|
|
17
|
+
} });
|
|
18
|
+
});
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { ThemeProperties } from "@phreshos/core";
|
|
3
|
+
import { type Spacing } from "./spacing.js";
|
|
4
|
+
/** Cross-axis alignment shared by layout containers. */
|
|
5
|
+
export type LayoutAlignment = "start" | "center" | "end" | "stretch" | "baseline";
|
|
6
|
+
/** Main-axis distribution shared by layout containers. */
|
|
7
|
+
export type LayoutJustification = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
8
|
+
/** A spacing value applied between layout children. */
|
|
9
|
+
export type LayoutGap = Spacing;
|
|
10
|
+
export declare function alignment(value: LayoutAlignment | undefined): CSSProperties["alignItems"] | undefined;
|
|
11
|
+
export declare function justification(value: LayoutJustification | undefined): CSSProperties["justifyContent"] | undefined;
|
|
12
|
+
/** Resolves semantic spacing while preserving explicit CSS gap values. */
|
|
13
|
+
export declare function resolveGap(value: LayoutGap | undefined, theme: ThemeProperties | null): CSSProperties["gap"];
|
|
14
|
+
export declare function tracks(value: number | string | undefined, property: "columns" | "rows"): string | undefined;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveSpacing } from "./spacing.js";
|
|
2
|
+
const alignments = {
|
|
3
|
+
start: "flex-start",
|
|
4
|
+
center: "center",
|
|
5
|
+
end: "flex-end",
|
|
6
|
+
stretch: "stretch",
|
|
7
|
+
baseline: "baseline"
|
|
8
|
+
};
|
|
9
|
+
const justifications = {
|
|
10
|
+
start: "flex-start",
|
|
11
|
+
center: "center",
|
|
12
|
+
end: "flex-end",
|
|
13
|
+
between: "space-between",
|
|
14
|
+
around: "space-around",
|
|
15
|
+
evenly: "space-evenly"
|
|
16
|
+
};
|
|
17
|
+
export function alignment(value) {
|
|
18
|
+
return value === undefined ? undefined : alignments[value];
|
|
19
|
+
}
|
|
20
|
+
export function justification(value) {
|
|
21
|
+
return value === undefined ? undefined : justifications[value];
|
|
22
|
+
}
|
|
23
|
+
/** Resolves semantic spacing while preserving explicit CSS gap values. */
|
|
24
|
+
export function resolveGap(value, theme) {
|
|
25
|
+
return resolveSpacing(value, theme);
|
|
26
|
+
}
|
|
27
|
+
export function tracks(value, property) {
|
|
28
|
+
if (value === undefined || typeof value === "string")
|
|
29
|
+
return value;
|
|
30
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
31
|
+
throw new TypeError(`Grid ${property} must be a positive integer or CSS track expression`);
|
|
32
|
+
}
|
|
33
|
+
return `repeat(${value}, minmax(0, 1fr))`;
|
|
34
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the React UI library.
|
|
3
|
+
*
|
|
4
|
+
* Components enter this surface only after their implementation-independent
|
|
5
|
+
* behavior contract has been established by the package's tests.
|
|
6
|
+
*/
|
|
7
|
+
export { ThemeProvider, useTheme, type ThemeProviderProps } from "./theme-provider.js";
|
|
8
|
+
export { Flex, type FlexProps } from "./flex.js";
|
|
9
|
+
export { Grid, type GridProps } from "./grid.js";
|
|
10
|
+
export { GlassSurface, type GlassSurfaceProps } from "./glass-surface.js";
|
|
11
|
+
export { Button, type ButtonProps } from "./button.js";
|
|
12
|
+
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
13
|
+
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
|
14
|
+
export { resolveSpacing, type Spacing } from "./spacing.js";
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the React UI library.
|
|
3
|
+
*
|
|
4
|
+
* Components enter this surface only after their implementation-independent
|
|
5
|
+
* behavior contract has been established by the package's tests.
|
|
6
|
+
*/
|
|
7
|
+
export { ThemeProvider, useTheme } from "./theme-provider.js";
|
|
8
|
+
export { Flex } from "./flex.js";
|
|
9
|
+
export { Grid } from "./grid.js";
|
|
10
|
+
export { GlassSurface } from "./glass-surface.js";
|
|
11
|
+
export { Button } from "./button.js";
|
|
12
|
+
export { resolveRadius } from "./radius.js";
|
|
13
|
+
export { resolveSpacing } from "./spacing.js";
|
package/dist/radius.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { Shapeable, ThemeProperties } from "@phreshos/core";
|
|
3
|
+
import { type ScaleLevel } from "@phreshos/core";
|
|
4
|
+
/** A Theme-derived level, pixel value, or explicit CSS corner radius. */
|
|
5
|
+
export type Radius = ScaleLevel | number | (string & {});
|
|
6
|
+
/** Shared semantic corner-radius capability for React UI components. */
|
|
7
|
+
export interface RadiusProps extends Shapeable<Radius> {
|
|
8
|
+
}
|
|
9
|
+
/** Resolves a Radius while preserving explicit CSS and pixel values. */
|
|
10
|
+
export declare function resolveRadius(value: Radius | undefined, theme: ThemeProperties | null): CSSProperties["borderRadius"];
|
package/dist/radius.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isScaleLevel, scale } from "@phreshos/core";
|
|
2
|
+
/** Resolves a Radius while preserving explicit CSS and pixel values. */
|
|
3
|
+
export function resolveRadius(value, theme) {
|
|
4
|
+
if (!isScaleLevel(value))
|
|
5
|
+
return value;
|
|
6
|
+
if (!theme)
|
|
7
|
+
throw new Error("A semantic radius requires a ThemeProvider");
|
|
8
|
+
return scale(theme.radius, value);
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { ThemeProperties } from "@phreshos/core";
|
|
3
|
+
import { type ScaleLevel } from "@phreshos/core";
|
|
4
|
+
/** A Theme-derived level, pixel value, or explicit CSS spacing value. */
|
|
5
|
+
export type Spacing = ScaleLevel | number | (string & {});
|
|
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"];
|
package/dist/spacing.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isScaleLevel, scale } from "@phreshos/core";
|
|
2
|
+
export function resolveSpacing(value, theme) {
|
|
3
|
+
if (!isScaleLevel(value))
|
|
4
|
+
return value;
|
|
5
|
+
if (!theme)
|
|
6
|
+
throw new Error("Semantic spacing requires a ThemeProvider");
|
|
7
|
+
return scale(theme.spacing, value);
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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(): Readonly<{
|
|
7
|
+
background: string;
|
|
8
|
+
foreground: string;
|
|
9
|
+
accent: string;
|
|
10
|
+
spacing: number;
|
|
11
|
+
radius: number;
|
|
12
|
+
glass: import("@phreshos/core").ThemeGlass;
|
|
13
|
+
}>;
|
|
14
|
+
/** Internal optional read used by primitives with both raw and themed values. */
|
|
15
|
+
export declare function useThemeIfAvailable(): Readonly<{
|
|
16
|
+
background: string;
|
|
17
|
+
foreground: string;
|
|
18
|
+
accent: string;
|
|
19
|
+
spacing: number;
|
|
20
|
+
radius: number;
|
|
21
|
+
glass: import("@phreshos/core").ThemeGlass;
|
|
22
|
+
}> | null;
|
|
23
|
+
/** Properties accepted by ThemeProvider. */
|
|
24
|
+
export interface ThemeProviderProps {
|
|
25
|
+
/** Content that receives this Theme instead of any outer Theme. */
|
|
26
|
+
readonly children: ReactNode;
|
|
27
|
+
/** Complete Theme value for this subtree. */
|
|
28
|
+
readonly theme: ThemeProperties;
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phreshos/react-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "dist/main.js",
|
|
8
|
+
"types": "dist/main.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/main.d.ts",
|
|
12
|
+
"default": "./dist/main.js"
|
|
13
|
+
},
|
|
14
|
+
"./icons": {
|
|
15
|
+
"types": "./dist/icons/main.d.ts",
|
|
16
|
+
"default": "./dist/icons/main.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"author": "Zohayr SLILEH",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/PhreshOS/react-ui.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/PhreshOS/react-ui/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/PhreshOS/react-ui#readme",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"phreshos",
|
|
36
|
+
"react",
|
|
37
|
+
"components",
|
|
38
|
+
"ui"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "bun@1.3.14",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
47
|
+
"check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
48
|
+
"test": "vitest run --environment jsdom",
|
|
49
|
+
"build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
|
|
50
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
51
|
+
"verify": "node --run check && node --run test && node --run build && node --run verify:package",
|
|
52
|
+
"prepack": "node --run test && node --run build"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@phreshos/core": "^0.1.1",
|
|
56
|
+
"react": "^19.2.0",
|
|
57
|
+
"react-dom": "^19.2.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"react-aria-components": "^1.20.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@base-ui/react": "^1.7.0",
|
|
64
|
+
"@phreshos/core": "^0.1.1",
|
|
65
|
+
"@testing-library/dom": "^10.4.1",
|
|
66
|
+
"@testing-library/react": "^16.3.0",
|
|
67
|
+
"@testing-library/user-event": "^14.6.1",
|
|
68
|
+
"@types/react": "^19.2.18",
|
|
69
|
+
"@types/react-dom": "^19.2.4",
|
|
70
|
+
"jsdom": "^30.0.1",
|
|
71
|
+
"react": "^19.2.8",
|
|
72
|
+
"react-dom": "^19.2.8",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"vitest": "^4.1.1"
|
|
75
|
+
}
|
|
76
|
+
}
|