@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
3
|
+
import { useLocale } from "react-aria-components";
|
|
4
|
+
import { MaterialLayer, useResolvedMaterial } from "./material.js";
|
|
5
|
+
import { SurfaceEdge } from "./surface-edge.js";
|
|
6
|
+
import { controlTransition, visualTransition } from "./motion-style.js";
|
|
7
|
+
/** Shared paint only. Selection, focus, validation, and native input behavior belong to React Aria. */
|
|
8
|
+
export function ToggleIndicator({ kind, selected, indeterminate = false, focused, invalid = false, hovered = false, pressed = false, theme, material: options }) {
|
|
9
|
+
const reduced = useReducedMotion();
|
|
10
|
+
const { direction } = useLocale();
|
|
11
|
+
const diameter = theme.fontSize + 6;
|
|
12
|
+
const active = selected || indeterminate;
|
|
13
|
+
const paints = invalid ? theme.paints.danger : active ? theme.paints.palette : theme.paints.neutral;
|
|
14
|
+
const paint = pressed ? paints.pressed : hovered ? paints.hover : paints.rest;
|
|
15
|
+
const radius = kind === "checkbox" ? Math.min(diameter / 4, typeof theme.radius === "number" ? theme.radius : diameter / 4) : diameter;
|
|
16
|
+
const switching = kind === "switch";
|
|
17
|
+
const thumbWidth = diameter - 6 + (pressed && !reduced ? 3 : 0);
|
|
18
|
+
const travel = (diameter * 2 - thumbWidth) / 2 - 3;
|
|
19
|
+
const material = useResolvedMaterial({ color: paint.background, material: options });
|
|
20
|
+
return _jsxs(motion.span, { "aria-hidden": "true", initial: false, animate: { scale: reduced || switching ? 1 : pressed ? 0.94 : hovered ? 1.03 : 1 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: {
|
|
21
|
+
color: paint.color,
|
|
22
|
+
...visualTransition,
|
|
23
|
+
background: "transparent",
|
|
24
|
+
display: "inline-grid",
|
|
25
|
+
placeItems: "center",
|
|
26
|
+
position: "relative",
|
|
27
|
+
boxSizing: "border-box",
|
|
28
|
+
flexShrink: 0,
|
|
29
|
+
width: switching ? diameter * 2 : diameter,
|
|
30
|
+
height: diameter,
|
|
31
|
+
borderRadius: radius,
|
|
32
|
+
border: "none",
|
|
33
|
+
outline: focused ? `2px solid ${theme.foreground}` : "none",
|
|
34
|
+
outlineOffset: 2,
|
|
35
|
+
}, children: [_jsx(MaterialLayer, { material: material }), _jsx(SurfaceEdge, { material: material }), switching ? _jsx(motion.span, { initial: false, animate: { width: thumbWidth, x: (selected ? travel : -travel) * (direction === "rtl" ? -1 : 1) }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: {
|
|
36
|
+
...visualTransition,
|
|
37
|
+
height: diameter - 6, borderRadius: diameter,
|
|
38
|
+
background: paint.color
|
|
39
|
+
} }) : _jsx(motion.span, { initial: false, animate: { opacity: active ? 1 : 0, scale: active ? 1 : 0.7 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration }, style: { display: "grid", placeItems: "center" }, children: kind === "radio" ? _jsx("span", { style: { ...visualTransition, width: diameter / 2, height: diameter / 2, borderRadius: "50%", background: "currentColor" } })
|
|
40
|
+
: _jsxs("svg", { width: diameter - 4, height: diameter - 4, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx(motion.path, { d: "m3 8 3 3 7-7", initial: false, animate: { pathLength: selected && !indeterminate ? 1 : 0, opacity: selected && !indeterminate ? 1 : 0 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration } }), _jsx(motion.path, { d: "M4 8h8", initial: false, animate: { pathLength: indeterminate ? 1 : 0, opacity: indeterminate ? 1 : 0 }, transition: { ...controlTransition, duration: reduced ? 0 : controlTransition.duration } })] }) })] });
|
|
41
|
+
}
|
|
42
|
+
export function toggleStyle(theme, disabled, readOnly) {
|
|
43
|
+
return {
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
gap: theme.gap,
|
|
47
|
+
minHeight: theme.height,
|
|
48
|
+
width: "fit-content",
|
|
49
|
+
cursor: disabled ? "not-allowed" : readOnly ? "default" : "pointer"
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "React components for coherent PhreshOS Program interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -52,16 +52,17 @@
|
|
|
52
52
|
"prepack": "node --run test && node --run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@phreshos/core": "^0.1.37",
|
|
56
55
|
"react": "^19.2.0",
|
|
57
56
|
"react-dom": "^19.2.0"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
59
|
+
"@phreshos/core": "^0.1.42",
|
|
60
|
+
"colorjs.io": "^0.7.1",
|
|
61
|
+
"motion": "^13.2.0",
|
|
60
62
|
"react-aria-components": "^1.20.0"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
63
65
|
"@base-ui/react": "^1.7.0",
|
|
64
|
-
"@phreshos/core": "^0.1.37",
|
|
65
66
|
"@testing-library/dom": "^10.4.1",
|
|
66
67
|
"@testing-library/react": "^16.3.0",
|
|
67
68
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
interface SurfaceMaterialProps {
|
|
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
|
-
readonly ripples: number;
|
|
9
|
-
readonly waves: number;
|
|
10
|
-
}
|
|
11
|
-
/** The locally owned SVG paint layer inside one Surface. */
|
|
12
|
-
export declare function SurfaceMaterial({ color, distortion, grain, grainAmount, identity, opacity, ripples, waves }: SurfaceMaterialProps): import("react").JSX.Element | null;
|
|
13
|
-
export {};
|
package/dist/surface-material.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useMemo } from "react";
|
|
3
|
-
import { scale } from "./scale.js";
|
|
4
|
-
/** The locally owned SVG paint layer inside one Surface. */
|
|
5
|
-
export function SurfaceMaterial({ color, distortion, grain, grainAmount, identity, opacity, ripples, waves }) {
|
|
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 || waves > 0 || ripples > 0;
|
|
10
|
-
const initial = useMemo(() => hasGrain ? grainPaths(seed, grainAmount) : [], [grainAmount, hasGrain, seed]);
|
|
11
|
-
if (!hasPaint && !hasDistortion)
|
|
12
|
-
return null;
|
|
13
|
-
return _jsxs("svg", { "data-surface-material": "", "data-surface-border": hasPaint ? "" : undefined, "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
|
-
borderColor: edgeColor(color, opacity),
|
|
23
|
-
borderStyle: hasPaint ? "solid" : "none",
|
|
24
|
-
borderWidth: hasPaint ? 1 : 0,
|
|
25
|
-
boxSizing: "border-box",
|
|
26
|
-
pointerEvents: "none"
|
|
27
|
-
}, children: [(hasGrain || hasDistortion) && _jsxs("defs", { children: [hasDistortion && _jsx(DistortionFilter, { distortion: distortion, identity: identity, ripples: ripples, seed: seed, waves: waves }), hasGrain && _jsx("pattern", { id: `${identity}-grain`, width: patternSize, height: patternSize, patternUnits: "userSpaceOnUse", children: initial.map((path, tone) => _jsx("path", { "data-surface-grain-tone": tone, d: path, fill: grainTone(color, tone, grain), shapeRendering: "crispEdges" }, tone)) })] }), hasPaint && _jsxs("g", { "data-surface-paint": "", opacity: opacity, children: [_jsx("rect", { "data-surface-base": "", width: "100%", height: "100%", fill: color }), hasGrain && _jsx("rect", { "data-surface-grain": "", width: "100%", height: "100%", fill: `url(#${identity}-grain)`, shapeRendering: "crispEdges" })] })] });
|
|
28
|
-
}
|
|
29
|
-
function edgeColor(color, opacity) {
|
|
30
|
-
const edge = `color-mix(in oklch, ${color} 94%, black)`;
|
|
31
|
-
const edgeOpacity = Math.min(1, scale(opacity, "large"));
|
|
32
|
-
if (edgeOpacity === 1)
|
|
33
|
-
return edge;
|
|
34
|
-
return `color-mix(in srgb, ${edge} ${Math.round(edgeOpacity * 10_000) / 100}%, transparent)`;
|
|
35
|
-
}
|
|
36
|
-
function DistortionFilter({ distortion, identity, ripples, seed, waves }) {
|
|
37
|
-
const fields = [
|
|
38
|
-
distortion > 0 && { name: "organic", strength: distortion, x: "R" },
|
|
39
|
-
waves > 0 && { name: "waves", strength: waves, x: "R" },
|
|
40
|
-
ripples > 0 && { name: "ripples", strength: ripples, x: "B" }
|
|
41
|
-
].filter((field) => Boolean(field));
|
|
42
|
-
const scale = fields.reduce((total, field) => total + field.strength, 0);
|
|
43
|
-
const weighted = fields.map(field => `${identity}-${field.name}-weighted`);
|
|
44
|
-
const map = `${identity}-distortion-map`;
|
|
45
|
-
return _jsxs("filter", { id: `${identity}-distortion`, "data-surface-distortion": "", x: "-20%", y: "-20%", width: "140%", height: "140%", colorInterpolationFilters: "sRGB", children: [distortion > 0 && _jsxs(Fragment, { children: [_jsx("feTurbulence", { "data-surface-distortion-noise": "", "data-surface-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` })] }), waves > 0 && _jsx(Fragment, { children: _jsx("feTurbulence", { "data-surface-distortion-field": "waves", type: "turbulence", baseFrequency: "0.006 0.045", numOctaves: 1, seed: seed + 17, result: `${identity}-waves-noise` }) }), ripples > 0 && _jsx(Fragment, { children: _jsx("feTurbulence", { "data-surface-distortion-field": "ripples", type: "turbulence", baseFrequency: "0.055", numOctaves: 2, seed: seed + 31, result: `${identity}-ripples-noise` }) }), fields.map(field => _jsx("feColorMatrix", { "data-surface-distortion-weight": field.name, in: `${identity}-${field.name}-noise${field.name === "organic" ? "-blurred" : ""}`, type: "matrix", values: weightMatrix(field.x, field.strength / scale), result: `${identity}-${field.name}-weighted` }, field.name)), weighted.slice(1).map((field, index) => _jsx("feComposite", { "data-surface-distortion-combine": "", in: index === 0 ? weighted[0] : `${identity}-distortion-sum-${index}`, in2: field, operator: "arithmetic", k2: 1, k3: 1, result: index === weighted.length - 2 ? map : `${identity}-distortion-sum-${index + 1}` }, field)), _jsx("feDisplacementMap", { "data-surface-distortion-stage": "combined", in: "SourceGraphic", in2: weighted.length === 1 ? weighted[0] : map, scale: scale, xChannelSelector: "R", yChannelSelector: "G" })] });
|
|
46
|
-
}
|
|
47
|
-
function weightMatrix(x, weight) {
|
|
48
|
-
const red = x === "R" ? `${weight} 0 0 0 0` : `0 0 ${weight} 0 0`;
|
|
49
|
-
return `${red}
|
|
50
|
-
0 ${weight} 0 0 0
|
|
51
|
-
0 0 0 0 0
|
|
52
|
-
0 0 0 1 0`;
|
|
53
|
-
}
|
|
54
|
-
function grainTone(color, tone, intensity) {
|
|
55
|
-
const channel = Math.round(tone / (toneCount - 1) * 255);
|
|
56
|
-
const percentage = Math.round(intensity * 10_000) / 100;
|
|
57
|
-
return `color-mix(in srgb, ${color} ${100 - percentage}%, rgb(${channel} ${channel} ${channel}) ${percentage}%)`;
|
|
58
|
-
}
|
|
59
|
-
function grainPaths(seed, amount) {
|
|
60
|
-
const tones = Array.from({ length: toneCount }, () => []);
|
|
61
|
-
for (let y = 0; y < patternSize; y += 1) {
|
|
62
|
-
for (let x = 0; x < patternSize; x += 1) {
|
|
63
|
-
const pointX = x + seed * 41;
|
|
64
|
-
const pointY = y + seed * 17;
|
|
65
|
-
const presence = shaderHash(pointX + 71.9, pointY + 13.7);
|
|
66
|
-
if (presence > amount)
|
|
67
|
-
continue;
|
|
68
|
-
const fine = shaderHash(Math.floor(pointX * 1.18), Math.floor(pointY * 1.18));
|
|
69
|
-
const clustered = shaderHash(Math.floor(pointX * 0.47) + 31.7, Math.floor(pointY * 0.47) + 31.7);
|
|
70
|
-
const value = clamp(fine * 0.8 + clustered * 0.2, 0, 1);
|
|
71
|
-
const tone = Math.min(toneCount - 1, Math.floor(value * toneCount));
|
|
72
|
-
tones[tone]?.push(`M${x} ${y}h1v1h-1z`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return tones.map(tone => tone.join(""));
|
|
76
|
-
}
|
|
77
|
-
function shaderHash(x, y) {
|
|
78
|
-
let red = fract(x * 0.1031);
|
|
79
|
-
let green = fract(y * 0.1031);
|
|
80
|
-
let blue = fract(x * 0.1031);
|
|
81
|
-
const product = red * (green + 33.33) + green * (blue + 33.33) + blue * (red + 33.33);
|
|
82
|
-
red += product;
|
|
83
|
-
green += product;
|
|
84
|
-
blue += product;
|
|
85
|
-
return fract((red + green) * blue);
|
|
86
|
-
}
|
|
87
|
-
function seedFrom(value) {
|
|
88
|
-
let seed = 2166136261;
|
|
89
|
-
for (const character of value) {
|
|
90
|
-
seed ^= character.codePointAt(0) ?? 0;
|
|
91
|
-
seed = Math.imul(seed, 16777619);
|
|
92
|
-
}
|
|
93
|
-
return (seed >>> 0) % 997 + 1;
|
|
94
|
-
}
|
|
95
|
-
function fract(value) {
|
|
96
|
-
return value - Math.floor(value);
|
|
97
|
-
}
|
|
98
|
-
function clamp(value, minimum, maximum) {
|
|
99
|
-
return Math.min(maximum, Math.max(minimum, value));
|
|
100
|
-
}
|
|
101
|
-
const patternSize = 64;
|
|
102
|
-
const toneCount = 16;
|