@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/dist/color.js
CHANGED
|
@@ -1,20 +1,115 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { ColorSpace, mix, parse, serialize, to, toGamut, contrastWCAG21, sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65 } from "colorjs.io/fn";
|
|
3
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
4
|
+
// Register the CSS color spaces, without bundling unrelated color-model APIs.
|
|
5
|
+
for (const space of [sRGB, sRGB_Linear, HSL, HWB, Lab, LCH, OKLab, OKLCH, P3, A98RGB, ProPhoto, REC_2020, XYZ_D50, XYZ_D65])
|
|
6
|
+
ColorSpace.register(space);
|
|
7
|
+
export const defaultColor = "background:base";
|
|
8
|
+
const treatments = {
|
|
9
|
+
subtle: { weight: 25, target: "white" },
|
|
10
|
+
soft: { weight: 60, target: "white" },
|
|
11
|
+
strong: { weight: 82, target: "black" },
|
|
12
|
+
intense: { weight: 68, target: "black" }
|
|
13
|
+
};
|
|
14
|
+
/** Distinguishes derived color levels from direct CSS colors. */
|
|
15
|
+
export function isColorLevel(value) {
|
|
16
|
+
return value === "subtle" || value === "soft" || value === "base" || value === "strong" || value === "intense";
|
|
17
|
+
}
|
|
2
18
|
/** Derives visual treatments while preserving the concrete value at `base`. */
|
|
3
19
|
export function color(value) {
|
|
20
|
+
const shade = (level) => {
|
|
21
|
+
const { weight, target } = treatments[level];
|
|
22
|
+
return `color-mix(in oklch, ${value} ${weight}%, ${target})`;
|
|
23
|
+
};
|
|
4
24
|
return Object.freeze({
|
|
5
|
-
subtle:
|
|
6
|
-
soft:
|
|
25
|
+
subtle: shade("subtle"),
|
|
26
|
+
soft: shade("soft"),
|
|
7
27
|
base: value,
|
|
8
|
-
strong:
|
|
9
|
-
intense:
|
|
28
|
+
strong: shade("strong"),
|
|
29
|
+
intense: shade("intense")
|
|
10
30
|
});
|
|
11
31
|
}
|
|
32
|
+
/** Resolves the same named treatment for solid paint and contrast calculations. */
|
|
33
|
+
export function resolveColorLevel(value, level) {
|
|
34
|
+
if (level === "base")
|
|
35
|
+
return opaqueColor(value);
|
|
36
|
+
const { weight, target } = treatments[level];
|
|
37
|
+
return opaqueColor(serialize(mix(opaqueColor(value), target, 1 - weight / 100, { space: "oklch" })));
|
|
38
|
+
}
|
|
12
39
|
/** Returns the complete visual treatments derived from one concrete color. */
|
|
13
40
|
export function useColor(value) {
|
|
14
41
|
return useMemo(() => color(value), [value]);
|
|
15
42
|
}
|
|
43
|
+
/** Resolves a semantic Appearance color or preserves a direct CSS color. */
|
|
44
|
+
export function useResolveColor(value = defaultColor) {
|
|
45
|
+
const appearance = useAppearance();
|
|
46
|
+
const semantic = parseSemanticColor(value);
|
|
47
|
+
const source = useResolveTheme(appearance.colors[semantic?.name ?? "background"]);
|
|
48
|
+
return semantic ? color(source)[semantic.level] : value;
|
|
49
|
+
}
|
|
50
|
+
/** Resolves a color to concrete opaque paint for solid interactive states. */
|
|
51
|
+
export function useResolveSolidColor(value) {
|
|
52
|
+
const appearance = useAppearance();
|
|
53
|
+
const semantic = parseSemanticColor(value);
|
|
54
|
+
const source = useResolveTheme(appearance.colors[semantic?.name ?? "background"]);
|
|
55
|
+
return semantic ? resolveColorLevel(source, semantic.level) : opaqueColor(value);
|
|
56
|
+
}
|
|
16
57
|
/** Applies material opacity without restricting the source CSS color syntax. */
|
|
17
58
|
export function colorOpacity(value, opacity) {
|
|
18
59
|
const percentage = Math.round(opacity * 10_000) / 100;
|
|
19
60
|
return `color-mix(in srgb, ${value} ${percentage}%, transparent)`;
|
|
20
61
|
}
|
|
62
|
+
/** Reads perceptual lightness from a resolved CSS color. */
|
|
63
|
+
export function colorLightness(value) {
|
|
64
|
+
// A missing CSS channel (`none`) contributes zero outside interpolation.
|
|
65
|
+
return OKLCH.from(parse(value))[0] ?? 0;
|
|
66
|
+
}
|
|
67
|
+
/** Orders resolved CSS colors by perceptual lightness, never by their role. */
|
|
68
|
+
export function orderColors(first, second) {
|
|
69
|
+
const difference = colorLightness(first) - colorLightness(second);
|
|
70
|
+
return difference > 0 || (difference === 0 && first >= second)
|
|
71
|
+
? { lighter: first, darker: second }
|
|
72
|
+
: { lighter: second, darker: first };
|
|
73
|
+
}
|
|
74
|
+
/** Solid controls use opaque, gamut-mapped colors for both paint and contrast. */
|
|
75
|
+
export function opaqueColor(value) {
|
|
76
|
+
return serialize(toGamut(to({ ...parse(value), alpha: 1 }, "srgb")), { format: "rgb", precision: 6 });
|
|
77
|
+
}
|
|
78
|
+
/** Changes only perceptual lightness; theme names never determine direction. */
|
|
79
|
+
export function colorShade(value, amount) {
|
|
80
|
+
const result = to(value, "oklch");
|
|
81
|
+
const lightness = result.coords[0] ?? 0;
|
|
82
|
+
result.coords[0] = Math.max(0, Math.min(1, lightness + (lightness > 0.6 ? -amount : amount)));
|
|
83
|
+
return opaqueColor(serialize(result));
|
|
84
|
+
}
|
|
85
|
+
/** Chooses only from the two Appearance colors, against the actual painted fill. */
|
|
86
|
+
export function onColor(fill, background, foreground) {
|
|
87
|
+
const first = opaqueColor(background);
|
|
88
|
+
const second = opaqueColor(foreground);
|
|
89
|
+
return contrastWCAG21(fill, first) > contrastWCAG21(fill, second) ? first : second;
|
|
90
|
+
}
|
|
91
|
+
/** Solid controls start at base; interaction shades preserve its text choice. */
|
|
92
|
+
export function solidColors(base, background, foreground) {
|
|
93
|
+
const fill = resolveColorLevel(base, "base");
|
|
94
|
+
const color = onColor(fill, background, foreground);
|
|
95
|
+
const paint = (background) => ({ background, color });
|
|
96
|
+
return {
|
|
97
|
+
rest: paint(fill),
|
|
98
|
+
hover: paint(colorShade(fill, 0.045)),
|
|
99
|
+
pressed: paint(colorShade(fill, 0.085))
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function parseSemanticColor(value) {
|
|
103
|
+
const separator = value.indexOf(":");
|
|
104
|
+
if (separator < 0 || value.indexOf(":", separator + 1) >= 0)
|
|
105
|
+
return null;
|
|
106
|
+
const name = value.slice(0, separator);
|
|
107
|
+
const level = value.slice(separator + 1);
|
|
108
|
+
if (!isAppearanceColor(name) || !isColorLevel(level))
|
|
109
|
+
return null;
|
|
110
|
+
return { name, level };
|
|
111
|
+
}
|
|
112
|
+
function isAppearanceColor(value) {
|
|
113
|
+
return value === "background" || value === "foreground" || value === "primary" || value === "secondary"
|
|
114
|
+
|| value === "success" || value === "warning" || value === "danger" || value === "info";
|
|
115
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { MaterialOptions } from "./material-options.js";
|
|
3
|
+
type Paint = Readonly<{
|
|
4
|
+
background: string;
|
|
5
|
+
color: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** A native button hosts Material without changing its native contract. */
|
|
8
|
+
export declare function MaterialButton({ native, paint, material: options }: {
|
|
9
|
+
readonly native: ComponentPropsWithRef<"button">;
|
|
10
|
+
readonly paint: Paint;
|
|
11
|
+
readonly material?: MaterialOptions;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Void text controls use a material host while retaining native sizing. */
|
|
14
|
+
export declare function MaterialField({ paint, radius, children, material: options }: Readonly<{
|
|
15
|
+
paint: Paint;
|
|
16
|
+
radius: CSSProperties["borderRadius"];
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
material?: MaterialOptions;
|
|
19
|
+
}>): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MaterialLayer, useResolvedMaterial } from "./material.js";
|
|
3
|
+
import { SurfaceEdge } from "./surface-edge.js";
|
|
4
|
+
/** A native button hosts Material without changing its native contract. */
|
|
5
|
+
export function MaterialButton({ native, paint, material: options }) {
|
|
6
|
+
const { ref, style, children, ...properties } = native;
|
|
7
|
+
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
8
|
+
return _jsxs("button", { ...properties, ref: ref, style: { ...style, background: "transparent", color: paint.color, position: style?.position ?? "relative", isolation: "isolate" }, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
|
|
9
|
+
}
|
|
10
|
+
/** Void text controls use a material host while retaining native sizing. */
|
|
11
|
+
export function MaterialField({ paint, radius, children, material: options }) {
|
|
12
|
+
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
13
|
+
return _jsxs("span", { style: { color: paint.color, display: "grid", minWidth: 0, borderRadius: radius, position: "relative", isolation: "isolate" }, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), children] });
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type RadiusProps } from "./radius.js";
|
|
3
|
+
import { type ScaleLevel } from "./scale.js";
|
|
4
|
+
import { solidColors } from "./color.js";
|
|
5
|
+
import { type Color } from "./color.js";
|
|
6
|
+
/** Semantic Appearance colors accepted by interactive controls. */
|
|
7
|
+
export type ControlColor = Color;
|
|
8
|
+
export interface ControlProps {
|
|
9
|
+
readonly size?: ScaleLevel;
|
|
10
|
+
readonly color?: ControlColor;
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
readonly style?: CSSProperties;
|
|
13
|
+
readonly disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FieldProps {
|
|
16
|
+
/** Visible accessible label. Otherwise supply aria-label or aria-labelledby. */
|
|
17
|
+
readonly label?: ReactNode;
|
|
18
|
+
readonly description?: ReactNode;
|
|
19
|
+
readonly errorMessage?: ReactNode;
|
|
20
|
+
readonly required?: boolean;
|
|
21
|
+
readonly invalid?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Names owned by our contract, not a second set of primitive-specific aliases. */
|
|
24
|
+
export type ControlOverrides = keyof ControlProps | keyof FieldProps | "children" | "isDisabled" | "isRequired" | "isInvalid";
|
|
25
|
+
export declare const controlFontSizes: Readonly<Record<ScaleLevel, number>>;
|
|
26
|
+
type SolidColors = ReturnType<typeof solidColors>;
|
|
27
|
+
export interface ControlTheme {
|
|
28
|
+
readonly spacing: number;
|
|
29
|
+
readonly foreground: string;
|
|
30
|
+
readonly background: string;
|
|
31
|
+
readonly tint: string;
|
|
32
|
+
readonly colored: boolean;
|
|
33
|
+
readonly danger: string;
|
|
34
|
+
readonly paints: {
|
|
35
|
+
readonly palette: SolidColors;
|
|
36
|
+
readonly neutral: SolidColors;
|
|
37
|
+
readonly danger: SolidColors;
|
|
38
|
+
};
|
|
39
|
+
readonly radius: CSSProperties["borderRadius"];
|
|
40
|
+
readonly fontSize: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
readonly gap: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function useControlTheme({ size, color, radius }: ControlProps & RadiusProps): ControlTheme;
|
|
45
|
+
export declare function fieldStyle(theme: ControlTheme, disabled?: boolean, style?: CSSProperties): CSSProperties;
|
|
46
|
+
export declare function controlPaint(theme: ControlTheme, focused: boolean, invalid: boolean, hovered?: boolean): {
|
|
47
|
+
background: string;
|
|
48
|
+
color: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function controlStyle(theme: ControlTheme, focused: boolean, invalid: boolean, hovered?: boolean, focusVisible?: boolean): CSSProperties;
|
|
51
|
+
export declare function FieldLabel({ label }: Pick<FieldProps, "label">): import("react").JSX.Element | null;
|
|
52
|
+
export declare function FieldFeedback({ description, errorMessage, theme }: Pick<FieldProps, "description" | "errorMessage"> & {
|
|
53
|
+
readonly theme: ControlTheme;
|
|
54
|
+
}): import("react").JSX.Element;
|
|
55
|
+
export {};
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { FieldError, Label, Text } from "react-aria-components";
|
|
4
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
5
|
+
import { resolveRadius } from "./radius.js";
|
|
6
|
+
import { scale } from "./scale.js";
|
|
7
|
+
import { solidColors } from "./color.js";
|
|
8
|
+
import { useResolveSolidColor } from "./color.js";
|
|
9
|
+
import { visualTransition } from "./motion-style.js";
|
|
10
|
+
export const controlFontSizes = Object.freeze({
|
|
11
|
+
xsmall: 11,
|
|
12
|
+
small: 12,
|
|
13
|
+
medium: 13,
|
|
14
|
+
large: 14,
|
|
15
|
+
xlarge: 15
|
|
16
|
+
});
|
|
17
|
+
export function useControlTheme({ size = "medium", color, radius = "medium" }) {
|
|
18
|
+
const appearance = useAppearance();
|
|
19
|
+
const spacing = scale(useResolveTheme(appearance.spacing), size);
|
|
20
|
+
const foreground = useResolveTheme(appearance.colors.foreground);
|
|
21
|
+
const background = useResolveTheme(appearance.colors.background);
|
|
22
|
+
const tint = useResolveSolidColor(color ?? "primary:base");
|
|
23
|
+
const danger = useResolveSolidColor("danger:base");
|
|
24
|
+
const paints = useMemo(() => ({
|
|
25
|
+
palette: solidColors(tint, background, foreground),
|
|
26
|
+
neutral: solidColors(background, background, foreground),
|
|
27
|
+
danger: solidColors(danger, background, foreground)
|
|
28
|
+
}), [tint, background, foreground, danger]);
|
|
29
|
+
return {
|
|
30
|
+
spacing,
|
|
31
|
+
foreground,
|
|
32
|
+
background,
|
|
33
|
+
tint,
|
|
34
|
+
colored: color !== undefined,
|
|
35
|
+
danger,
|
|
36
|
+
paints,
|
|
37
|
+
radius: resolveRadius(radius, appearance),
|
|
38
|
+
fontSize: controlFontSizes[size],
|
|
39
|
+
height: Math.max(24, 24 + spacing),
|
|
40
|
+
gap: Math.max(4, spacing / 2)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function fieldStyle(theme, disabled = false, style) {
|
|
44
|
+
return {
|
|
45
|
+
...style,
|
|
46
|
+
...visualTransition,
|
|
47
|
+
display: "grid",
|
|
48
|
+
gap: theme.gap,
|
|
49
|
+
minWidth: 0,
|
|
50
|
+
color: theme.foreground,
|
|
51
|
+
fontFamily: "inherit",
|
|
52
|
+
fontSize: theme.fontSize,
|
|
53
|
+
lineHeight: 1.5,
|
|
54
|
+
opacity: disabled ? 0.46 : 1
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function controlPaint(theme, focused, invalid, hovered = false) {
|
|
58
|
+
const paints = invalid ? theme.paints.danger : theme.colored ? theme.paints.palette : theme.paints.neutral;
|
|
59
|
+
return focused ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
60
|
+
}
|
|
61
|
+
export function controlStyle(theme, focused, invalid, hovered = false, focusVisible = focused) {
|
|
62
|
+
const paint = controlPaint(theme, focused, invalid, hovered);
|
|
63
|
+
return {
|
|
64
|
+
...visualTransition,
|
|
65
|
+
appearance: "none",
|
|
66
|
+
boxSizing: "border-box",
|
|
67
|
+
width: "100%",
|
|
68
|
+
minWidth: 0,
|
|
69
|
+
height: theme.height,
|
|
70
|
+
paddingBlock: 0,
|
|
71
|
+
paddingInline: Math.max(8, theme.spacing),
|
|
72
|
+
border: 0,
|
|
73
|
+
borderRadius: theme.radius,
|
|
74
|
+
outline: focusVisible ? `2px solid ${theme.foreground}` : "none",
|
|
75
|
+
outlineOffset: 2,
|
|
76
|
+
...paint,
|
|
77
|
+
caretColor: paint.color,
|
|
78
|
+
font: "inherit",
|
|
79
|
+
lineHeight: 1.5
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function FieldLabel({ label }) {
|
|
83
|
+
return label == null ? null : _jsx(Label, { style: { fontWeight: 550 }, children: label });
|
|
84
|
+
}
|
|
85
|
+
export function FieldFeedback({ description, errorMessage, theme }) {
|
|
86
|
+
return _jsxs(_Fragment, { children: [description != null && _jsx(Text, { slot: "description", style: { fontSize: "0.92em", opacity: 0.7 }, children: description }), _jsx(FieldError, { style: { fontSize: "0.92em", color: theme.danger }, children: errorMessage })] });
|
|
87
|
+
}
|
|
@@ -86,7 +86,7 @@ const documents = new WeakMap();
|
|
|
86
86
|
/** Applies one Appearance to the complete owning document without rendering. */
|
|
87
87
|
export default function DocumentScrollbars({ appearance, theme }) {
|
|
88
88
|
const identity = useRef(Symbol("AppearanceProvider")).current;
|
|
89
|
-
const foreground = theme === "dark" ? appearance.foreground.dark : appearance.foreground.light;
|
|
89
|
+
const foreground = theme === "dark" ? appearance.colors.foreground.dark : appearance.colors.foreground.light;
|
|
90
90
|
const padding = 5;
|
|
91
91
|
const thumbSize = 6;
|
|
92
92
|
const size = thumbSize + padding * 2;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const textControlClass = "phreshos-ui-text-control";
|
|
3
|
+
/** Placeholder contrast follows the control's chosen text color, not browser gray. */
|
|
4
|
+
export default function FieldStyle() {
|
|
5
|
+
return _jsx("style", { href: "phreshos-react-ui-fields", precedence: "phreshos", children: `
|
|
6
|
+
.phreshos-ui-text-control::placeholder {
|
|
7
|
+
color: inherit;
|
|
8
|
+
opacity: 0.6;
|
|
9
|
+
}
|
|
10
|
+
` });
|
|
11
|
+
}
|
package/dist/flex.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import { useAppearance } 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 appearance =
|
|
7
|
+
const appearance = useAppearance();
|
|
8
8
|
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
9
|
...style,
|
|
10
10
|
display: inline ? "inline-flex" : "flex",
|
package/dist/grid.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import { useAppearance } 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 appearance =
|
|
7
|
+
const appearance = useAppearance();
|
|
8
8
|
return _jsx("div", { ...properties, ref: ref, style: {
|
|
9
9
|
...style,
|
|
10
10
|
display: inline ? "inline-grid" : "grid",
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextControlProps } from "./text-control.js";
|
|
2
|
+
export type InputProps = TextControlProps;
|
|
3
|
+
/** A labeled single-line text control. onChange receives the string value. */
|
|
4
|
+
export declare const Input: import("react").ForwardRefExoticComponent<TextControlProps & import("react").RefAttributes<HTMLInputElement>>;
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Input as AriaInput, TextField } from "react-aria-components";
|
|
4
|
+
import { controlPaint, controlStyle, FieldFeedback, FieldLabel, fieldStyle, useControlTheme } from "./control.js";
|
|
5
|
+
import { MaterialField } from "./control-material.js";
|
|
6
|
+
import FieldStyle, { textControlClass } from "./field-style.js";
|
|
7
|
+
/** A labeled single-line text control. onChange receives the string value. */
|
|
8
|
+
export const Input = forwardRef(function Input({ label, description, errorMessage, disabled, readOnly, required, invalid, size, color, radius, style, className, placeholder, material, ...properties }, ref) {
|
|
9
|
+
const theme = useControlTheme({ size, color, radius });
|
|
10
|
+
return _jsxs(TextField, { ...properties, className: className, isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(theme, disabled, style), children: [_jsx(FieldStyle, {}), _jsx(FieldLabel, { label: label }), _jsx(AriaInput, { ref: ref, placeholder: placeholder, className: textControlClass, render: (native, state) => _jsx(MaterialField, { material: material, radius: theme.radius, paint: controlPaint(theme, state.isFocused, state.isInvalid, state.isHovered), children: _jsx("input", { ...native, style: { ...native.style, borderRadius: "inherit", background: "transparent" } }) }), style: state => controlStyle(theme, state.isFocused, state.isInvalid, state.isHovered, state.isFocusVisible) }), _jsx(FieldFeedback, { theme: theme, description: description, errorMessage: errorMessage })] });
|
|
11
|
+
});
|
package/dist/main.d.ts
CHANGED
|
@@ -4,13 +4,25 @@
|
|
|
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 { defaultAppearance } from "@phreshos/core";
|
|
7
8
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme, type AppearanceProviderProps } from "./appearance-provider.js";
|
|
8
9
|
export { Flex, type FlexProps } from "./flex.js";
|
|
9
10
|
export { Grid, type GridProps } from "./grid.js";
|
|
11
|
+
export { Material, type MaterialOptions, type MaterialOverrides, type MaterialProps } from "./material.js";
|
|
10
12
|
export { Surface, type SurfaceProps } from "./surface.js";
|
|
11
|
-
export {
|
|
13
|
+
export { defaultColor, type Color, type ColorLevel, type ColorScale } from "./color.js";
|
|
14
|
+
export { Panel, type PanelProps } from "./panel.js";
|
|
15
|
+
export { Button, type ButtonColor, type ButtonProps } from "./button.js";
|
|
16
|
+
export { Input, type InputProps } from "./input.js";
|
|
17
|
+
export { Textarea, type TextareaProps } from "./textarea.js";
|
|
18
|
+
export { Checkbox, type CheckboxProps } from "./checkbox.js";
|
|
19
|
+
export { Radio, RadioGroup, type RadioProps, type RadioGroupProps } from "./radio.js";
|
|
20
|
+
export { Switch, type SwitchProps } from "./switch.js";
|
|
21
|
+
export { Select, type SelectProps, type SelectOption } from "./select.js";
|
|
22
|
+
export { Slider, type SliderProps } from "./slider.js";
|
|
23
|
+
export type { ControlColor } from "./control.js";
|
|
12
24
|
export type { LayoutAlignment, LayoutGap, LayoutJustification } from "./layout.js";
|
|
13
25
|
export { resolveRadius, type Radius, type RadiusProps } from "./radius.js";
|
|
14
26
|
export { resolveSpacing, type Spacing } from "./spacing.js";
|
|
15
|
-
export { useColor
|
|
27
|
+
export { useColor } from "./color.js";
|
|
16
28
|
export { useScale, type NumericScale, type ScaleLevel } from "./scale.js";
|
package/dist/main.js
CHANGED
|
@@ -4,11 +4,22 @@
|
|
|
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 { defaultAppearance } from "@phreshos/core";
|
|
7
8
|
export { AppearanceProvider, useAppearance, useResolveTheme, useTheme } from "./appearance-provider.js";
|
|
8
9
|
export { Flex } from "./flex.js";
|
|
9
10
|
export { Grid } from "./grid.js";
|
|
11
|
+
export { Material } from "./material.js";
|
|
10
12
|
export { Surface } from "./surface.js";
|
|
13
|
+
export { defaultColor } from "./color.js";
|
|
14
|
+
export { Panel } from "./panel.js";
|
|
11
15
|
export { Button } from "./button.js";
|
|
16
|
+
export { Input } from "./input.js";
|
|
17
|
+
export { Textarea } from "./textarea.js";
|
|
18
|
+
export { Checkbox } from "./checkbox.js";
|
|
19
|
+
export { Radio, RadioGroup } from "./radio.js";
|
|
20
|
+
export { Switch } from "./switch.js";
|
|
21
|
+
export { Select } from "./select.js";
|
|
22
|
+
export { Slider } from "./slider.js";
|
|
12
23
|
export { resolveRadius } from "./radius.js";
|
|
13
24
|
export { resolveSpacing } from "./spacing.js";
|
|
14
25
|
export { useColor } from "./color.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ScaleLevel } from "./scale.js";
|
|
2
|
+
/** Optional values of visual substance, expressed directly or relative to Appearance. */
|
|
3
|
+
export interface MaterialOptions {
|
|
4
|
+
readonly opacity?: ScaleLevel | number;
|
|
5
|
+
readonly backdrop?: ScaleLevel | number;
|
|
6
|
+
readonly grain?: ScaleLevel | number;
|
|
7
|
+
readonly grainAmount?: ScaleLevel | number;
|
|
8
|
+
readonly distortion?: ScaleLevel | number;
|
|
9
|
+
readonly saturation?: ScaleLevel | number;
|
|
10
|
+
}
|
|
11
|
+
/** Resolves Material values from the current Appearance without knowing any host geometry. */
|
|
12
|
+
export declare function useMaterialOptions(values?: MaterialOptions): {
|
|
13
|
+
opacity: number;
|
|
14
|
+
backdrop: number;
|
|
15
|
+
grain: number;
|
|
16
|
+
grainAmount: number;
|
|
17
|
+
distortion: number;
|
|
18
|
+
saturation: number;
|
|
19
|
+
};
|
|
20
|
+
export type ResolvedMaterial = ReturnType<typeof useMaterialOptions>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appearanceLimits } from "@phreshos/core";
|
|
2
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
3
|
+
import { isScaleLevel, scale, scaleMultiplier } from "./scale.js";
|
|
4
|
+
/** Resolves Material values from the current Appearance without knowing any host geometry. */
|
|
5
|
+
export function useMaterialOptions(values = {}) {
|
|
6
|
+
const appearance = useAppearance();
|
|
7
|
+
const defaults = useResolveTheme(appearance.material);
|
|
8
|
+
const limits = appearanceLimits.material;
|
|
9
|
+
return {
|
|
10
|
+
opacity: resolve(values.opacity, defaults.opacity, limits.opacity),
|
|
11
|
+
backdrop: resolve(values.backdrop, defaults.backdrop, limits.backdrop),
|
|
12
|
+
grain: resolve(values.grain, defaults.grain, limits.grain),
|
|
13
|
+
grainAmount: resolve(values.grainAmount, defaults.grainAmount, limits.grainAmount),
|
|
14
|
+
distortion: resolve(values.distortion, defaults.distortion, limits.distortion),
|
|
15
|
+
saturation: resolve(values.saturation, defaults.saturation, limits.saturation, scaleMultiplier)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolve(value, base, range, derive = scale) {
|
|
19
|
+
const resolved = isScaleLevel(value) ? derive(base, value) : value ?? base;
|
|
20
|
+
const finite = Number.isFinite(resolved) ? resolved : base;
|
|
21
|
+
return Math.min(range.maximum, Math.max(range.minimum, finite));
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface MaterialPaintProps {
|
|
2
|
+
readonly color: string;
|
|
3
|
+
readonly distortion: number;
|
|
4
|
+
readonly grain: number;
|
|
5
|
+
readonly grainAmount: number;
|
|
6
|
+
readonly identity: string;
|
|
7
|
+
readonly opacity: number;
|
|
8
|
+
}
|
|
9
|
+
/** The fill, refraction definition, and grain of one Material. */
|
|
10
|
+
export declare function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }: MaterialPaintProps): import("react").JSX.Element | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { paintTransition } from "./motion-style.js";
|
|
4
|
+
/** The fill, refraction definition, and grain of one Material. */
|
|
5
|
+
export function MaterialPaint({ color, distortion, grain, grainAmount, identity, opacity }) {
|
|
6
|
+
const seed = useMemo(() => seedFrom(identity), [identity]);
|
|
7
|
+
const hasPaint = opacity > 0;
|
|
8
|
+
const hasGrain = hasPaint && grain > 0 && grainAmount > 0;
|
|
9
|
+
const hasDistortion = distortion > 0;
|
|
10
|
+
const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
11
|
+
if (!hasPaint && !hasDistortion)
|
|
12
|
+
return null;
|
|
13
|
+
return _jsxs("svg", { "data-material-paint": "", "aria-hidden": "true", focusable: "false", style: {
|
|
14
|
+
position: "absolute",
|
|
15
|
+
zIndex: -1,
|
|
16
|
+
inset: 0,
|
|
17
|
+
display: "block",
|
|
18
|
+
width: "100%",
|
|
19
|
+
height: "100%",
|
|
20
|
+
overflow: "hidden",
|
|
21
|
+
borderRadius: "inherit",
|
|
22
|
+
pointerEvents: "none"
|
|
23
|
+
}, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-material-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsxs("g", { "data-material-fill": "", opacity: opacity, style: paintTransition, children: [_jsx("rect", { "data-material-base": "", width: "100%", height: "100%", style: { ...paintTransition, fill: color } }), hasGrain && _jsx("rect", { "data-material-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] });
|
|
24
|
+
}
|
|
25
|
+
function DistortionFilter({ distortion, identity }) {
|
|
26
|
+
return _jsxs("filter", { id: `${identity}-distortion`, "data-material-distortion": "", x: "-20%", y: "-20%", width: "140%", height: "140%", colorInterpolationFilters: "sRGB", children: [_jsx("feTurbulence", { "data-material-distortion-noise": "", "data-material-distortion-field": "organic", type: "fractalNoise", baseFrequency: "0.008 0.008", numOctaves: 2, seed: 92, result: `${identity}-organic-noise` }), _jsx("feGaussianBlur", { in: `${identity}-organic-noise`, stdDeviation: 2, result: `${identity}-organic-noise-blurred` }), _jsx("feDisplacementMap", { "data-material-distortion-stage": "organic", in: "SourceGraphic", in2: `${identity}-organic-noise-blurred`, scale: distortion, xChannelSelector: "R", yChannelSelector: "G" })] });
|
|
27
|
+
}
|
|
28
|
+
function grainTone(color, tone, intensity) {
|
|
29
|
+
const channel = Math.round(tone / (toneCount - 1) * 255);
|
|
30
|
+
const percentage = Math.round(intensity * 10_000) / 100;
|
|
31
|
+
return `color-mix(in srgb, ${color} ${100 - percentage}%, rgb(${channel} ${channel} ${channel}) ${percentage}%)`;
|
|
32
|
+
}
|
|
33
|
+
function grainPaths(seed, amount) {
|
|
34
|
+
const tones = Array.from({ length: toneCount }, () => []);
|
|
35
|
+
for (let y = 0; y < patternSize; y += 1) {
|
|
36
|
+
for (let x = 0; x < patternSize; x += 1) {
|
|
37
|
+
const pointX = x + seed * 41;
|
|
38
|
+
const pointY = y + seed * 17;
|
|
39
|
+
const presence = shaderHash(pointX + 71.9, pointY + 13.7);
|
|
40
|
+
if (presence > amount)
|
|
41
|
+
continue;
|
|
42
|
+
const fine = shaderHash(Math.floor(pointX * 1.18), Math.floor(pointY * 1.18));
|
|
43
|
+
const clustered = shaderHash(Math.floor(pointX * 0.47) + 31.7, Math.floor(pointY * 0.47) + 31.7);
|
|
44
|
+
const value = clamp(fine * 0.8 + clustered * 0.2, 0, 1);
|
|
45
|
+
const tone = Math.min(toneCount - 1, Math.floor(value * toneCount));
|
|
46
|
+
tones[tone]?.push(`M${x} ${y}h1v1h-1z`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tones.map(tone => tone.join(""));
|
|
50
|
+
}
|
|
51
|
+
function shaderHash(x, y) {
|
|
52
|
+
let red = fract(x * 0.1031);
|
|
53
|
+
let green = fract(y * 0.1031);
|
|
54
|
+
let blue = fract(x * 0.1031);
|
|
55
|
+
const product = red * (green + 33.33) + green * (blue + 33.33) + blue * (red + 33.33);
|
|
56
|
+
red += product;
|
|
57
|
+
green += product;
|
|
58
|
+
blue += product;
|
|
59
|
+
return fract((red + green) * blue);
|
|
60
|
+
}
|
|
61
|
+
function seedFrom(value) {
|
|
62
|
+
let seed = 2166136261;
|
|
63
|
+
for (const character of value) {
|
|
64
|
+
seed ^= character.codePointAt(0) ?? 0;
|
|
65
|
+
seed = Math.imul(seed, 16777619);
|
|
66
|
+
}
|
|
67
|
+
return (seed >>> 0) % 997 + 1;
|
|
68
|
+
}
|
|
69
|
+
function fract(value) {
|
|
70
|
+
return value - Math.floor(value);
|
|
71
|
+
}
|
|
72
|
+
function clamp(value, minimum, maximum) {
|
|
73
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
74
|
+
}
|
|
75
|
+
const patternSize = 64;
|
|
76
|
+
const toneCount = 16;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type MaterialOptions } from "./material-options.js";
|
|
2
|
+
import { type Color } from "./color.js";
|
|
3
|
+
export type { MaterialOptions } from "./material-options.js";
|
|
4
|
+
/** Visual substance without host layout, content, interaction, or shadow. */
|
|
5
|
+
export interface MaterialProps {
|
|
6
|
+
readonly color?: Color;
|
|
7
|
+
readonly material?: MaterialOptions;
|
|
8
|
+
}
|
|
9
|
+
export interface MaterialOverrides {
|
|
10
|
+
readonly material?: MaterialOptions;
|
|
11
|
+
}
|
|
12
|
+
/** Fill the geometry of the nearest positioned host with the resolved material. */
|
|
13
|
+
export declare function Material(options?: MaterialProps): import("react").JSX.Element;
|
|
14
|
+
export declare function useResolvedMaterial({ color, material: options }?: MaterialProps): {
|
|
15
|
+
color: string;
|
|
16
|
+
foreground: string;
|
|
17
|
+
opacity: number;
|
|
18
|
+
backdrop: number;
|
|
19
|
+
grain: number;
|
|
20
|
+
grainAmount: number;
|
|
21
|
+
distortion: number;
|
|
22
|
+
saturation: number;
|
|
23
|
+
};
|
|
24
|
+
export type ResolvedMaterial = ReturnType<typeof useResolvedMaterial>;
|
|
25
|
+
export declare function MaterialLayer({ material }: Readonly<{
|
|
26
|
+
material: ResolvedMaterial;
|
|
27
|
+
}>): import("react").JSX.Element;
|
package/dist/material.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { useMaterialOptions } from "./material-options.js";
|
|
4
|
+
import { useAppearance, useResolveTheme } from "./appearance-provider.js";
|
|
5
|
+
import { useResolveColor } from "./color.js";
|
|
6
|
+
import { MaterialPaint } from "./material-paint.js";
|
|
7
|
+
import MotionStyle from "./motion-style.js";
|
|
8
|
+
const layerStyle = {
|
|
9
|
+
position: "absolute",
|
|
10
|
+
inset: 0,
|
|
11
|
+
borderRadius: "inherit",
|
|
12
|
+
pointerEvents: "none"
|
|
13
|
+
};
|
|
14
|
+
/** Fill the geometry of the nearest positioned host with the resolved material. */
|
|
15
|
+
export function Material(options = {}) {
|
|
16
|
+
const material = useResolvedMaterial(options);
|
|
17
|
+
return _jsx(MaterialLayer, { material: material });
|
|
18
|
+
}
|
|
19
|
+
export function useResolvedMaterial({ color, material: options } = {}) {
|
|
20
|
+
const appearance = useAppearance();
|
|
21
|
+
const material = useMaterialOptions(options);
|
|
22
|
+
return {
|
|
23
|
+
...material,
|
|
24
|
+
color: useResolveColor(color),
|
|
25
|
+
foreground: useResolveTheme(appearance.colors.foreground)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function MaterialLayer({ material }) {
|
|
29
|
+
const { backdrop, saturation, distortion } = material;
|
|
30
|
+
const frost = [
|
|
31
|
+
backdrop === 0 ? "" : `blur(${backdrop}px)`,
|
|
32
|
+
saturation === 1 ? "" : `saturate(${saturation})`
|
|
33
|
+
].filter(Boolean).join(" ");
|
|
34
|
+
const identity = `phresh-material-${useId().replaceAll(":", "")}`;
|
|
35
|
+
return _jsxs("span", { "data-material": "", "aria-hidden": "true", style: layerStyle, children: [_jsx(MotionStyle, {}), distortion > 0 && _jsx(BackdropLayer, { name: "refraction", filter: `url("#${identity}-distortion")`, zIndex: -3 }), frost && _jsx(BackdropLayer, { name: "frost", filter: frost, zIndex: -2 }), _jsx(MaterialPaint, { identity: identity, distortion: distortion, color: material.color, grain: material.grain, grainAmount: material.grainAmount, opacity: material.opacity })] });
|
|
36
|
+
}
|
|
37
|
+
/** Keep refraction and native frost in independent compositor passes. */
|
|
38
|
+
function BackdropLayer({ filter, name, zIndex }) {
|
|
39
|
+
return _jsx("span", { "data-material-backdrop": name, "aria-hidden": "true", style: {
|
|
40
|
+
...layerStyle,
|
|
41
|
+
zIndex,
|
|
42
|
+
backdropFilter: filter,
|
|
43
|
+
WebkitBackdropFilter: filter
|
|
44
|
+
} });
|
|
45
|
+
}
|