@phreshos/react-ui 0.1.39 → 0.1.41
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/dist/alert.js +3 -4
- package/dist/app-layout.d.ts +2 -0
- package/dist/app-layout.js +54 -27
- package/dist/avatar.d.ts +23 -0
- package/dist/avatar.js +42 -0
- package/dist/badge.d.ts +20 -0
- package/dist/badge.js +31 -0
- package/dist/breadcrumbs.d.ts +20 -0
- package/dist/breadcrumbs.js +43 -0
- package/dist/control/control.d.ts +2 -0
- package/dist/control/control.js +4 -0
- package/dist/control/meaning.d.ts +5 -0
- package/dist/control/meaning.js +10 -0
- package/dist/dialog.js +2 -2
- package/dist/foundation/motion-style.d.ts +8 -0
- package/dist/foundation/motion-style.js +13 -0
- package/dist/link.d.ts +18 -0
- package/dist/link.js +32 -0
- package/dist/loading.d.ts +20 -0
- package/dist/loading.js +66 -0
- package/dist/main.d.ts +13 -1
- package/dist/main.js +12 -0
- package/dist/meter.d.ts +26 -0
- package/dist/meter.js +29 -0
- package/dist/progress-bar.d.ts +3 -0
- package/dist/progress-bar.js +7 -3
- package/dist/readiness.d.ts +46 -0
- package/dist/readiness.js +78 -0
- package/dist/search-field.d.ts +13 -0
- package/dist/search-field.js +27 -0
- package/dist/skeleton.d.ts +15 -0
- package/dist/skeleton.js +29 -0
- package/dist/spinner.d.ts +2 -0
- package/dist/spinner.js +5 -2
- package/dist/surface/grain.d.ts +9 -0
- package/dist/surface/grain.js +9 -0
- package/dist/surface/surface.d.ts +4 -0
- package/dist/surface/surface.js +12 -6
- package/dist/tag-group.d.ts +21 -0
- package/dist/tag-group.js +52 -0
- package/dist/toast.d.ts +32 -0
- package/dist/toast.js +62 -0
- package/dist/typography.d.ts +49 -0
- package/dist/typography.js +71 -0
- package/dist/window.d.ts +4 -1
- package/dist/window.js +2 -0
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -40,6 +40,18 @@ export { Spinner } from "./spinner.js";
|
|
|
40
40
|
export { NumberField } from "./number-field.js";
|
|
41
41
|
export { SegmentedControl } from "./segmented-control.js";
|
|
42
42
|
export { Alert } from "./alert.js";
|
|
43
|
+
export { Link } from "./link.js";
|
|
44
|
+
export { Heading, Text, Kbd, Code } from "./typography.js";
|
|
45
|
+
export { Badge } from "./badge.js";
|
|
46
|
+
export { Meter } from "./meter.js";
|
|
47
|
+
export { SearchField } from "./search-field.js";
|
|
48
|
+
export { Breadcrumbs } from "./breadcrumbs.js";
|
|
49
|
+
export { Avatar } from "./avatar.js";
|
|
50
|
+
export { TagGroup } from "./tag-group.js";
|
|
51
|
+
export { Skeleton } from "./skeleton.js";
|
|
52
|
+
export { toast, ToastRegion } from "./toast.js";
|
|
53
|
+
export { Readiness, useReadiness, useRequirement } from "./readiness.js";
|
|
54
|
+
export { Loading } from "./loading.js";
|
|
43
55
|
export { FileTrigger } from "./file-trigger.js";
|
|
44
56
|
export { DropZone } from "./drop-zone.js";
|
|
45
57
|
export { Fieldset } from "./fieldset.js";
|
package/dist/meter.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type MeterProps as AriaMeterProps } from "react-aria-components";
|
|
3
|
+
import { type Color } from "./foundation/color.js";
|
|
4
|
+
import type { ScaleLevel } from "./foundation/scale.js";
|
|
5
|
+
import type { MaterialOverrides } from "./surface/material-options.js";
|
|
6
|
+
type NativeMeterProps = Omit<AriaMeterProps, "children" | "className" | "style">;
|
|
7
|
+
export interface MeterProps extends NativeMeterProps, MaterialOverrides {
|
|
8
|
+
readonly className?: string;
|
|
9
|
+
/** Color of the measured portion while no threshold is reached. */
|
|
10
|
+
readonly color?: Color;
|
|
11
|
+
/** From this share of the range, between 0 and 1, the measure turns `warning`. */
|
|
12
|
+
readonly warning?: number;
|
|
13
|
+
/** From this share of the range, between 0 and 1, the measure turns `danger`. */
|
|
14
|
+
readonly danger?: number;
|
|
15
|
+
/** Visible label associated with the measured value. */
|
|
16
|
+
readonly label?: ReactNode;
|
|
17
|
+
readonly size?: ScaleLevel;
|
|
18
|
+
readonly style?: CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A quantity within a known range, such as disk usage. Unlike a ProgressBar it
|
|
22
|
+
* does not move toward an end: it reads a level, and turns `warning` or
|
|
23
|
+
* `danger` once it crosses the thresholds it is given.
|
|
24
|
+
*/
|
|
25
|
+
export declare const Meter: import("react").ForwardRefExoticComponent<MeterProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
26
|
+
export {};
|
package/dist/meter.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Label, Meter as AriaMeter } from "react-aria-components";
|
|
4
|
+
import { controlFontWeight, controlOpacity, transition, useControlMetrics } from "./control/control.js";
|
|
5
|
+
import { fieldStyle } from "./control/field.js";
|
|
6
|
+
import { resolveColor } from "./foundation/color.js";
|
|
7
|
+
import { railThickness } from "./progress-bar.js";
|
|
8
|
+
import { SurfaceView } from "./surface/surface.js";
|
|
9
|
+
/**
|
|
10
|
+
* A quantity within a known range, such as disk usage. Unlike a ProgressBar it
|
|
11
|
+
* does not move toward an end: it reads a level, and turns `warning` or
|
|
12
|
+
* `danger` once it crosses the thresholds it is given.
|
|
13
|
+
*/
|
|
14
|
+
export const Meter = forwardRef(function Meter({ className, color = "info", warning, danger, label, material, size, style, value = 0, minValue = 0, maxValue = 100, ...properties }, ref) {
|
|
15
|
+
const metrics = useControlMetrics(size);
|
|
16
|
+
const share = maxValue === minValue ? 0 : (value - minValue) / (maxValue - minValue);
|
|
17
|
+
const role = danger !== undefined && share >= danger ? "danger" : warning !== undefined && share >= warning ? "warning" : color;
|
|
18
|
+
const fill = resolveColor(role, metrics.visual.colors);
|
|
19
|
+
return _jsx(AriaMeter, { ...properties, ref: ref, className: className, value: value, minValue: minValue, maxValue: maxValue, style: fieldStyle(metrics, { width: "100%", ...style }), children: state => _jsxs(_Fragment, { children: [(label != null || state.valueText != null) && _jsxs("span", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: metrics.gap }, children: [label != null && _jsx(Label, { style: { fontWeight: controlFontWeight }, children: label }), state.valueText != null && _jsx("span", { style: { fontVariantNumeric: "tabular-nums", opacity: controlOpacity.secondary }, children: state.valueText })] }), _jsx(SurfaceView, { as: "span", "aria-hidden": "true", "data-meter-track": "", color: "background", depth: "recessed", material: material, radius: "full", style: { display: "block", height: railThickness(metrics), overflow: "hidden" }, children: _jsx("span", { "data-meter-fill": "", style: {
|
|
20
|
+
...transition(metrics.visual, "width, background-color", 1.5),
|
|
21
|
+
position: "absolute",
|
|
22
|
+
insetBlock: 0,
|
|
23
|
+
insetInlineStart: 0,
|
|
24
|
+
display: "block",
|
|
25
|
+
width: `${state.percentage}%`,
|
|
26
|
+
borderRadius: "inherit",
|
|
27
|
+
background: fill
|
|
28
|
+
} }) })] }) });
|
|
29
|
+
});
|
package/dist/progress-bar.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
2
|
import type { ProgressBarProps as AriaProgressBarProps } from "react-aria-components";
|
|
3
|
+
import { useControlMetrics } from "./control/control.js";
|
|
3
4
|
import { type Color } from "./foundation/color.js";
|
|
4
5
|
import type { ScaleLevel } from "./foundation/scale.js";
|
|
5
6
|
import type { MaterialOverrides } from "./surface/material-options.js";
|
|
@@ -15,6 +16,8 @@ export interface ProgressBarProps extends NativeProgressBarProps, MaterialOverri
|
|
|
15
16
|
readonly size?: ScaleLevel;
|
|
16
17
|
readonly style?: CSSProperties;
|
|
17
18
|
}
|
|
19
|
+
/** The thickness of a recessed value rail, shared by ProgressBar and Meter. */
|
|
20
|
+
export declare function railThickness(metrics: ReturnType<typeof useControlMetrics>): number;
|
|
18
21
|
/** Progress over time: a recessed rail filled in the owner's color. */
|
|
19
22
|
export declare const ProgressBar: import("react").ForwardRefExoticComponent<ProgressBarProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
20
23
|
export {};
|
package/dist/progress-bar.js
CHANGED
|
@@ -4,12 +4,16 @@ import { Label, ProgressBar as AriaProgressBar } from "react-aria-components";
|
|
|
4
4
|
import { controlFontWeight, controlOpacity, transition, useControlMetrics } from "./control/control.js";
|
|
5
5
|
import { fieldStyle } from "./control/field.js";
|
|
6
6
|
import { resolveColor } from "./foundation/color.js";
|
|
7
|
-
import MotionStyle from "./foundation/motion-style.js";
|
|
7
|
+
import MotionStyle, { loopDuration } from "./foundation/motion-style.js";
|
|
8
8
|
import { SurfaceView } from "./surface/surface.js";
|
|
9
|
+
/** The thickness of a recessed value rail, shared by ProgressBar and Meter. */
|
|
10
|
+
export function railThickness(metrics) {
|
|
11
|
+
return Math.max(4, Math.round(metrics.spacing / 2));
|
|
12
|
+
}
|
|
9
13
|
/** Progress over time: a recessed rail filled in the owner's color. */
|
|
10
14
|
export const ProgressBar = forwardRef(function ProgressBar({ className, color = "primary", indeterminate = false, label, material, size, style, value, minValue = 0, maxValue = 100, ...properties }, ref) {
|
|
11
15
|
const metrics = useControlMetrics(size);
|
|
12
|
-
const thickness =
|
|
16
|
+
const thickness = railThickness(metrics);
|
|
13
17
|
const fill = resolveColor(color, metrics.visual.colors);
|
|
14
18
|
const moving = indeterminate && metrics.visual.duration > 0;
|
|
15
19
|
return _jsx(AriaProgressBar, { ...properties, ref: ref, className: className, value: value, minValue: minValue, maxValue: maxValue, isIndeterminate: indeterminate, style: fieldStyle(metrics, { width: "100%", ...style }), children: state => {
|
|
@@ -24,7 +28,7 @@ export const ProgressBar = forwardRef(function ProgressBar({ className, color =
|
|
|
24
28
|
width: state.isIndeterminate ? "40%" : `${state.percentage ?? 0}%`,
|
|
25
29
|
borderRadius: "inherit",
|
|
26
30
|
background: fill,
|
|
27
|
-
animation: moving ? `phreshos-ui-sweep ${
|
|
31
|
+
animation: moving ? `phreshos-ui-sweep ${loopDuration(metrics.visual)}ms ${metrics.visual.easing} infinite` : undefined
|
|
28
32
|
} }) })] });
|
|
29
33
|
} });
|
|
30
34
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** One requirement of a boundary, as it is now. */
|
|
3
|
+
export interface Requirement<Detail = unknown> {
|
|
4
|
+
readonly ready: boolean;
|
|
5
|
+
readonly detail: Detail;
|
|
6
|
+
}
|
|
7
|
+
/** What a boundary knows: whether it is ready, and every requirement declared in it. */
|
|
8
|
+
export interface ReadinessState<Detail = unknown> {
|
|
9
|
+
readonly ready: boolean;
|
|
10
|
+
readonly requirements: readonly Requirement<Detail>[];
|
|
11
|
+
}
|
|
12
|
+
export interface ReadinessProps<Detail = unknown> {
|
|
13
|
+
/** The interface that waits. Held while the boundary is not ready. */
|
|
14
|
+
readonly children?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Interface that shows the waiting. Rendered next to the children, outside
|
|
17
|
+
* the held area, so it stays interactive. A function receives the same
|
|
18
|
+
* value `useReadiness()` returns.
|
|
19
|
+
*/
|
|
20
|
+
readonly status?: ReactNode | ((readiness: ReadinessState<Detail>) => ReactNode);
|
|
21
|
+
/** Milliseconds the boundary stays held after its requirements are ready, for work they do not see. */
|
|
22
|
+
readonly delay?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Holds interaction with its children until every requirement declared in it
|
|
26
|
+
* is ready. It draws nothing: the children are wrapped in one
|
|
27
|
+
* `display: contents` element that carries `inert` while waiting.
|
|
28
|
+
*
|
|
29
|
+
* A requirement belongs to its nearest boundary only, so a boundary that
|
|
30
|
+
* starts waiting inside a ready one holds just its own part.
|
|
31
|
+
*
|
|
32
|
+
* The boundary starts held and is released only after the first commit, once
|
|
33
|
+
* every requirement mounted with it has registered. Server HTML is held too.
|
|
34
|
+
*/
|
|
35
|
+
export declare function Readiness<Detail = unknown>({ children, status, delay }: ReadinessProps<Detail>): import("react").JSX.Element;
|
|
36
|
+
/**
|
|
37
|
+
* Declares one requirement of the nearest boundary for as long as the calling
|
|
38
|
+
* component is mounted. It follows `ready` both ways. `detail` is carried as-is
|
|
39
|
+
* and compared by identity, so pass a stable value such as a string.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useRequirement(ready: boolean, detail?: unknown): void;
|
|
42
|
+
/**
|
|
43
|
+
* Reads the nearest boundary. Inside `status`, that is the boundary the status
|
|
44
|
+
* belongs to. `Detail` states what the boundary's requirements pass as detail.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useReadiness<Detail = unknown>(): ReadinessState<Detail>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
3
|
+
/** Where `useRequirement` declares. Status content sees the parent's, not its own boundary's. */
|
|
4
|
+
const RegistryContext = createContext(null);
|
|
5
|
+
/** What `useReadiness` reads. */
|
|
6
|
+
const StateContext = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* Holds interaction with its children until every requirement declared in it
|
|
9
|
+
* is ready. It draws nothing: the children are wrapped in one
|
|
10
|
+
* `display: contents` element that carries `inert` while waiting.
|
|
11
|
+
*
|
|
12
|
+
* A requirement belongs to its nearest boundary only, so a boundary that
|
|
13
|
+
* starts waiting inside a ready one holds just its own part.
|
|
14
|
+
*
|
|
15
|
+
* The boundary starts held and is released only after the first commit, once
|
|
16
|
+
* every requirement mounted with it has registered. Server HTML is held too.
|
|
17
|
+
*/
|
|
18
|
+
export function Readiness({ children, status, delay = 0 }) {
|
|
19
|
+
const entries = useRef(new Map());
|
|
20
|
+
const [revision, invalidate] = useReducer((value) => value + 1, 0);
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
|
+
const [delayed, setDelayed] = useState(false);
|
|
23
|
+
const registry = useMemo(() => ({
|
|
24
|
+
set(identity, requirement) {
|
|
25
|
+
const current = entries.current.get(identity);
|
|
26
|
+
if (current !== undefined && current.ready === requirement.ready && Object.is(current.detail, requirement.detail))
|
|
27
|
+
return;
|
|
28
|
+
entries.current.set(identity, requirement);
|
|
29
|
+
invalidate();
|
|
30
|
+
},
|
|
31
|
+
delete(identity) {
|
|
32
|
+
if (entries.current.delete(identity))
|
|
33
|
+
invalidate();
|
|
34
|
+
}
|
|
35
|
+
}), []);
|
|
36
|
+
// Descendant layout effects run first, so every requirement mounted with the boundary is known here.
|
|
37
|
+
useLayoutEffect(() => setMounted(true), []);
|
|
38
|
+
const requirements = useMemo(() => [...entries.current.values()], [revision]);
|
|
39
|
+
const met = mounted && requirements.every(requirement => requirement.ready);
|
|
40
|
+
// The delay starts again whenever a requirement stops being ready.
|
|
41
|
+
if (!met && delayed)
|
|
42
|
+
setDelayed(false);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!met || delay === 0)
|
|
45
|
+
return;
|
|
46
|
+
const timer = setTimeout(() => setDelayed(true), delay);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}, [met, delay]);
|
|
49
|
+
const ready = met && (delay === 0 || delayed);
|
|
50
|
+
const state = useMemo(() => ({ ready, requirements }), [ready, requirements]);
|
|
51
|
+
return _jsxs(StateContext.Provider, { value: state, children: [_jsx(RegistryContext.Provider, { value: registry, children: _jsx("div", { "data-readiness": "", inert: !ready, style: { display: "contents" }, children: children }) }), typeof status === "function" ? status(state) : status] });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Declares one requirement of the nearest boundary for as long as the calling
|
|
55
|
+
* component is mounted. It follows `ready` both ways. `detail` is carried as-is
|
|
56
|
+
* and compared by identity, so pass a stable value such as a string.
|
|
57
|
+
*/
|
|
58
|
+
export function useRequirement(ready, detail) {
|
|
59
|
+
const registry = useContext(RegistryContext);
|
|
60
|
+
if (registry === null)
|
|
61
|
+
throw new Error("useRequirement must be called inside a Readiness boundary.");
|
|
62
|
+
// The call site is the identity: one object for this mounted component.
|
|
63
|
+
const [identity] = useState(() => ({}));
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
registry.set(identity, { ready, detail });
|
|
66
|
+
}, [registry, identity, ready, detail]);
|
|
67
|
+
useLayoutEffect(() => () => registry.delete(identity), [registry, identity]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reads the nearest boundary. Inside `status`, that is the boundary the status
|
|
71
|
+
* belongs to. `Detail` states what the boundary's requirements pass as detail.
|
|
72
|
+
*/
|
|
73
|
+
export function useReadiness() {
|
|
74
|
+
const state = useContext(StateContext);
|
|
75
|
+
if (state === null)
|
|
76
|
+
throw new Error("useReadiness must be called inside a Readiness boundary.");
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SearchFieldProps as AriaSearchFieldProps } from "react-aria-components";
|
|
2
|
+
import { type ControlOverrides, type ControlProps, type FieldProps } from "./control/control.js";
|
|
3
|
+
import type { RadiusProps } from "./foundation/radius.js";
|
|
4
|
+
import type { MaterialOverrides } from "./surface/material-options.js";
|
|
5
|
+
export interface SearchFieldProps extends Omit<AriaSearchFieldProps, ControlOverrides | "className" | "style">, ControlProps, FieldProps, RadiusProps, MaterialOverrides {
|
|
6
|
+
readonly placeholder?: string;
|
|
7
|
+
readonly readOnly?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A text field for searching. It leads with a search mark, and a clear button
|
|
11
|
+
* appears once it holds text; Escape clears it too, and Enter calls `onSubmit`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SearchField: import("react").ForwardRefExoticComponent<SearchFieldProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Group, Input as AriaInput, SearchField as AriaSearchField } from "react-aria-components";
|
|
4
|
+
import { Search, X } from "lucide-react";
|
|
5
|
+
import { controlOpacity, useControlMetrics } from "./control/control.js";
|
|
6
|
+
import { FieldButton } from "./control/field-button.js";
|
|
7
|
+
import { FieldFeedback, FieldLabel, fieldStyle } from "./control/field.js";
|
|
8
|
+
import { iconProps } from "./control/icon.js";
|
|
9
|
+
import { surfaceRender } from "./control/surface-render.js";
|
|
10
|
+
import { nativeTextStyle } from "./control/text-control.js";
|
|
11
|
+
import MotionStyle, { textControlClass } from "./foundation/motion-style.js";
|
|
12
|
+
import { dimmedClass } from "./surface/surface.js";
|
|
13
|
+
/**
|
|
14
|
+
* A text field for searching. It leads with a search mark, and a clear button
|
|
15
|
+
* appears once it holds text; Escape clears it too, and Enter calls `onSubmit`.
|
|
16
|
+
*/
|
|
17
|
+
export const SearchField = forwardRef(function SearchField({ label, description, errorMessage, disabled, readOnly, required, invalid, size, color = "background", radius, style, className, placeholder, material, ...properties }, ref) {
|
|
18
|
+
const metrics = useControlMetrics(size, radius);
|
|
19
|
+
const icon = Math.round(metrics.height / 2);
|
|
20
|
+
return _jsx(AriaSearchField, { ...properties, className: dimmedClass(disabled ?? false, className), isDisabled: disabled, isReadOnly: readOnly, isRequired: required, isInvalid: invalid, style: fieldStyle(metrics, style), children: field => _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx(FieldLabel, { label: label }), _jsxs(Group, { render: surfaceRender("div", group => ({
|
|
21
|
+
color,
|
|
22
|
+
depth: "recessed",
|
|
23
|
+
material,
|
|
24
|
+
radius: metrics.radius,
|
|
25
|
+
interaction: { hovered: group.isHovered, focusVisible: group.isFocusWithin, invalid: group.isInvalid, disabled: group.isDisabled }
|
|
26
|
+
})), style: { display: "grid", gridTemplateColumns: "auto minmax(0, 1fr) auto", alignItems: "center", minWidth: 0, height: metrics.height, paddingInlineEnd: 4 }, children: [_jsx(Search, { ...iconProps(icon), style: { marginInlineStart: metrics.inset, opacity: controlOpacity.placeholder } }), _jsx(AriaInput, { ref: ref, placeholder: placeholder, className: textControlClass, style: { ...nativeTextStyle(metrics, false), paddingInlineStart: metrics.gap, WebkitAppearance: "none" } }), _jsx(FieldButton, { metrics: metrics, "aria-label": "Clear search", style: { visibility: field.isEmpty ? "hidden" : "visible" }, children: _jsx(X, { ...iconProps(icon) }) })] }), _jsx(FieldFeedback, { metrics: metrics, description: description, errorMessage: errorMessage })] }) });
|
|
27
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type Radius } from "./foundation/radius.js";
|
|
3
|
+
export interface SkeletonProps {
|
|
4
|
+
/** Lines of text to hold the place of. Omitted, the Skeleton is one block sized by `style`. */
|
|
5
|
+
readonly lines?: number;
|
|
6
|
+
readonly radius?: Radius;
|
|
7
|
+
readonly className?: string;
|
|
8
|
+
readonly style?: CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The place of content that is still loading, in the separator paint so it
|
|
12
|
+
* sits quietly on any Surface. It pulses at the pace of every repeating motion
|
|
13
|
+
* and stands still when animations are off.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Skeleton({ lines, radius, className, style }: SkeletonProps): import("react").JSX.Element;
|
package/dist/skeleton.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useControlMetrics } from "./control/control.js";
|
|
3
|
+
import { separatorColor } from "./control/field.js";
|
|
4
|
+
import MotionStyle, { loopDuration } from "./foundation/motion-style.js";
|
|
5
|
+
import { resolveRadius } from "./foundation/radius.js";
|
|
6
|
+
/** The last line of a paragraph ends short, at the golden ratio of the others. */
|
|
7
|
+
const lastLine = `${(1 / 1.618) * 100}%`;
|
|
8
|
+
/**
|
|
9
|
+
* The place of content that is still loading, in the separator paint so it
|
|
10
|
+
* sits quietly on any Surface. It pulses at the pace of every repeating motion
|
|
11
|
+
* and stands still when animations are off.
|
|
12
|
+
*/
|
|
13
|
+
export function Skeleton({ lines, radius, className, style }) {
|
|
14
|
+
const metrics = useControlMetrics();
|
|
15
|
+
const visual = metrics.visual;
|
|
16
|
+
const paint = {
|
|
17
|
+
display: "block",
|
|
18
|
+
background: separatorColor(metrics),
|
|
19
|
+
borderRadius: resolveRadius(radius ?? "small", visual.radius),
|
|
20
|
+
animation: visual.duration > 0 ? `phreshos-ui-pulse ${loopDuration(visual)}ms ${visual.easing} infinite` : undefined
|
|
21
|
+
};
|
|
22
|
+
if (lines === undefined)
|
|
23
|
+
return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx("span", { "aria-hidden": "true", className: className, style: { ...paint, height: metrics.height, ...style } })] });
|
|
24
|
+
return _jsxs(_Fragment, { children: [_jsx(MotionStyle, {}), _jsx("span", { "aria-hidden": "true", className: className, style: { display: "grid", gap: "0.5lh", ...style }, children: Array.from({ length: lines }, (_, index) => _jsx("span", { style: {
|
|
25
|
+
...paint,
|
|
26
|
+
height: "1em",
|
|
27
|
+
width: index === lines - 1 && lines > 1 ? lastLine : "100%"
|
|
28
|
+
} }, index)) })] });
|
|
29
|
+
}
|
package/dist/spinner.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ type DecorativeSpinnerProps = Readonly<{
|
|
|
21
21
|
label?: never;
|
|
22
22
|
}>;
|
|
23
23
|
export type SpinnerProps = SpinnerVisualProps & (NamedSpinnerProps | DecorativeSpinnerProps);
|
|
24
|
+
/** A standalone status stays visibly distinct from the compact control indicator. */
|
|
25
|
+
export declare function spinnerDiameter(spacing: number, size: ScaleLevel): number;
|
|
24
26
|
/** Compact indeterminate progress, either named or explicitly decorative. */
|
|
25
27
|
export declare const Spinner: import("react").ForwardRefExoticComponent<SpinnerProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
26
28
|
export {};
|
package/dist/spinner.js
CHANGED
|
@@ -5,12 +5,15 @@ import { useControlMetrics } from "./control/control.js";
|
|
|
5
5
|
import { colorOpacity, resolveColor } from "./foundation/color.js";
|
|
6
6
|
import MotionStyle from "./foundation/motion-style.js";
|
|
7
7
|
import { scale } from "./foundation/scale.js";
|
|
8
|
+
/** A standalone status stays visibly distinct from the compact control indicator. */
|
|
9
|
+
export function spinnerDiameter(spacing, size) {
|
|
10
|
+
return spacing + scale(spacing, size);
|
|
11
|
+
}
|
|
8
12
|
/** Compact indeterminate progress, either named or explicitly decorative. */
|
|
9
13
|
export const Spinner = forwardRef(function Spinner({ className, color = "primary", decorative = false, label, size = "medium", style, ...properties }, ref) {
|
|
10
14
|
const metrics = useControlMetrics(size);
|
|
11
15
|
const { colors } = metrics.visual;
|
|
12
|
-
|
|
13
|
-
const diameter = metrics.visual.spacing + scale(metrics.visual.spacing, size);
|
|
16
|
+
const diameter = spinnerDiameter(metrics.visual.spacing, size);
|
|
14
17
|
const arc = color === "currentColor" ? "currentColor" : resolveColor(color, colors);
|
|
15
18
|
const rootStyle = {
|
|
16
19
|
display: "inline-grid",
|
package/dist/surface/grain.d.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* without also fading that layer's backdrop.
|
|
5
5
|
*/
|
|
6
6
|
export declare function grainImage(seed: number, amount: number, opacity: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* One grain texture as a shared class. Its name comes from the texture's own
|
|
9
|
+
* parameters, so server and browser rendering agree on it, and the rule is
|
|
10
|
+
* written once however many Surfaces use it instead of inline on each.
|
|
11
|
+
*/
|
|
12
|
+
export declare function grainTexture(seed: number, amount: number, opacity: number): Readonly<{
|
|
13
|
+
name: string;
|
|
14
|
+
rule: string;
|
|
15
|
+
}>;
|
|
7
16
|
/**
|
|
8
17
|
* One texture serves every Surface. Each Surface tiles it from its own origin,
|
|
9
18
|
* so neighbours never align, and equal paints can share one generated class.
|
package/dist/surface/grain.js
CHANGED
|
@@ -20,6 +20,15 @@ export function grainImage(seed, amount, opacity) {
|
|
|
20
20
|
resources.set(key, resource);
|
|
21
21
|
return resource;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* One grain texture as a shared class. Its name comes from the texture's own
|
|
25
|
+
* parameters, so server and browser rendering agree on it, and the rule is
|
|
26
|
+
* written once however many Surfaces use it instead of inline on each.
|
|
27
|
+
*/
|
|
28
|
+
export function grainTexture(seed, amount, opacity) {
|
|
29
|
+
const name = `phreshos-grain-${seed}-${amount}-${Math.round(opacity * 1_000)}`.replace(/[^a-z0-9-]/gi, "_");
|
|
30
|
+
return { name, rule: `:where(.${name}) { --phreshos-surface-grain: ${grainImage(seed, amount, opacity)} }` };
|
|
31
|
+
}
|
|
23
32
|
/**
|
|
24
33
|
* One texture serves every Surface. Each Surface tiles it from its own origin,
|
|
25
34
|
* so neighbours never align, and equal paints can share one generated class.
|
package/dist/surface/surface.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createElement, forwardRef, useId, useInsertionEffect } from "react";
|
|
|
3
3
|
import { canvasDarkness, colorOpacity, contrast, mixColor, readableColor, recessColor, resolveColor } from "../foundation/color.js";
|
|
4
4
|
import { resolveRadius } from "../foundation/radius.js";
|
|
5
5
|
import { useVisual } from "../foundation/visual.js";
|
|
6
|
-
import {
|
|
6
|
+
import { grainSeed, grainSize, grainTexture } from "./grain.js";
|
|
7
7
|
import { flushPaintRules, paintClass, usePaintClasses } from "./paint-class.js";
|
|
8
8
|
import { resolveMaterialOptions } from "./material-options.js";
|
|
9
9
|
import { resolveShadowOptions, shadowStyle } from "./shadow-options.js";
|
|
@@ -35,10 +35,10 @@ export const SurfaceView = forwardRef(function Surface({ as: Element = "div", co
|
|
|
35
35
|
return createElement(Element, {
|
|
36
36
|
...properties,
|
|
37
37
|
ref,
|
|
38
|
-
className: [surfaceClass, painted, dimmedClass(interaction?.disabled ?? false), className].filter(Boolean).join(" "),
|
|
38
|
+
className: [surfaceClass, paint.grain?.name, painted, dimmedClass(interaction?.disabled ?? false), className].filter(Boolean).join(" "),
|
|
39
39
|
// Without a document (server rendering), the same declarations travel inline.
|
|
40
40
|
style: painted === undefined ? { ...inlineDeclarations(declarations), ...style } : style
|
|
41
|
-
}, _jsx(SurfaceStyle, {}), paint.distortion > 0 && _jsx(Refraction, { identity: identity, distortion: paint.distortion }), children);
|
|
41
|
+
}, _jsx(SurfaceStyle, {}), paint.grain && _jsx(GrainStyle, { grain: paint.grain }), paint.distortion > 0 && _jsx(Refraction, { identity: identity, distortion: paint.distortion }), children);
|
|
42
42
|
});
|
|
43
43
|
function inlineDeclarations(declarations) {
|
|
44
44
|
const result = {};
|
|
@@ -94,8 +94,8 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
|
|
|
94
94
|
? [material.backdrop > 0 ? `blur(${material.backdrop}px)` : "", material.saturation !== 1 ? `saturate(${material.saturation})` : ""].filter(Boolean).join(" ")
|
|
95
95
|
: "";
|
|
96
96
|
const grain = level >= materialLevel.basic && material.grain > 0 && material.grainAmount > 0
|
|
97
|
-
?
|
|
98
|
-
:
|
|
97
|
+
? grainTexture(grainSeed, material.grainAmount, level === materialLevel.basic ? material.grain * material.opacity : material.grain)
|
|
98
|
+
: null;
|
|
99
99
|
const ringBase = interaction?.invalid
|
|
100
100
|
? colors.danger
|
|
101
101
|
: !clear && (contrast(base, colors.background) ?? 1) >= 1.6 ? base : colors.primary;
|
|
@@ -135,9 +135,11 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
|
|
|
135
135
|
ring: colorOpacity(ringBase, 0.34),
|
|
136
136
|
shadow: [hairline, outer === "none" ? null : outer].filter(Boolean).join(", ") || "none",
|
|
137
137
|
distortion: level >= materialLevel.full && translucent ? material.distortion : 0,
|
|
138
|
+
grain,
|
|
138
139
|
variables: {
|
|
139
140
|
"--phreshos-surface-paint": translucent ? colorOpacity(fill, opacity) : fill,
|
|
140
|
-
|
|
141
|
+
// A grain texture arrives through its shared class; without one the layer is empty.
|
|
142
|
+
...(grain === null ? { "--phreshos-surface-grain": "none" } : {}),
|
|
141
143
|
"--phreshos-surface-frost": frost || "none",
|
|
142
144
|
"--phreshos-surface-rim": rim,
|
|
143
145
|
"--phreshos-surface-spill-top": spill("top"),
|
|
@@ -243,6 +245,10 @@ const stylesheet = `
|
|
|
243
245
|
mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
|
|
244
246
|
}
|
|
245
247
|
`;
|
|
248
|
+
/** One rule per grain texture, hoisted and deduplicated by React like the stylesheet below. */
|
|
249
|
+
function GrainStyle({ grain }) {
|
|
250
|
+
return _jsx("style", { href: grain.name, precedence: "phreshos", children: grain.rule });
|
|
251
|
+
}
|
|
246
252
|
/** React hoists and deduplicates this stylesheet, including inside portals. */
|
|
247
253
|
function SurfaceStyle() {
|
|
248
254
|
return _jsx("style", { href: "phreshos-react-ui-surface", precedence: "phreshos", children: stylesheet });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { TagGroupProps as AriaTagGroupProps, TagProps as AriaTagProps } from "react-aria-components";
|
|
3
|
+
import { type FieldProps } from "./control/control.js";
|
|
4
|
+
import { type Color } from "./foundation/color.js";
|
|
5
|
+
import type { ScaleLevel } from "./foundation/scale.js";
|
|
6
|
+
export interface TagGroupProps extends Omit<AriaTagGroupProps, "className" | "style" | "children">, Omit<FieldProps, "required" | "invalid"> {
|
|
7
|
+
readonly children?: ReactNode;
|
|
8
|
+
/** The paint of every Tag, in its subtle level; selection uses the selection color. */
|
|
9
|
+
readonly color?: Color;
|
|
10
|
+
readonly size?: ScaleLevel;
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
readonly style?: CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
export interface TagGroupTagProps extends Omit<AriaTagProps, "className" | "style" | "children"> {
|
|
15
|
+
readonly children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
declare function TagGroupTag({ children, ...properties }: TagGroupTagProps): import("react").JSX.Element;
|
|
18
|
+
export declare const TagGroup: import("react").ForwardRefExoticComponent<TagGroupProps & import("react").RefAttributes<HTMLDivElement>> & {
|
|
19
|
+
Tag: typeof TagGroupTag;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, forwardRef, useContext } from "react";
|
|
3
|
+
import { Tag as AriaTag, TagGroup as AriaTagGroup, TagList } from "react-aria-components";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { controlFontWeight, relativeFontSize, useControlMetrics } from "./control/control.js";
|
|
6
|
+
import { FieldButton } from "./control/field-button.js";
|
|
7
|
+
import { FieldFeedback, FieldLabel, fieldStyle } from "./control/field.js";
|
|
8
|
+
import { iconProps } from "./control/icon.js";
|
|
9
|
+
import { selectionColor } from "./control/item.js";
|
|
10
|
+
import { surfaceRender } from "./control/surface-render.js";
|
|
11
|
+
import { colorLevel, resolveColor } from "./foundation/color.js";
|
|
12
|
+
const TagGroupContext = createContext(null);
|
|
13
|
+
/** Tags sit one control step below the size of their group, like Badges. */
|
|
14
|
+
const smaller = Object.freeze({ xsmall: "xsmall", small: "xsmall", medium: "small", large: "medium", xlarge: "large" });
|
|
15
|
+
/**
|
|
16
|
+
* A set of short labels, such as applied filters. Tags can be selected like
|
|
17
|
+
* collection items, and with `onRemove` each one gains a remove button.
|
|
18
|
+
*/
|
|
19
|
+
const TagGroupRoot = forwardRef(function TagGroup({ label, description, errorMessage, children, color = "default", size = "medium", style, ...properties }, ref) {
|
|
20
|
+
const group = useControlMetrics(size);
|
|
21
|
+
const metrics = useControlMetrics(smaller[size]);
|
|
22
|
+
return _jsx(TagGroupContext.Provider, { value: { metrics, color, size }, children: _jsxs(AriaTagGroup, { ...properties, ref: ref, style: fieldStyle(group, style), children: [_jsx(FieldLabel, { label: label }), _jsx(TagList, { style: { display: "flex", flexWrap: "wrap", gap: group.gap }, children: children }), _jsx(FieldFeedback, { metrics: group, description: description, errorMessage: errorMessage })] }) });
|
|
23
|
+
});
|
|
24
|
+
function TagGroupTag({ children, ...properties }) {
|
|
25
|
+
const context = useContext(TagGroupContext);
|
|
26
|
+
if (context === null)
|
|
27
|
+
throw new Error("TagGroup.Tag must be inside TagGroup");
|
|
28
|
+
const { metrics, color, size } = context;
|
|
29
|
+
const { colors } = metrics.visual;
|
|
30
|
+
const base = resolveColor(color, colors);
|
|
31
|
+
return _jsx(AriaTag, { ...properties, textValue: properties.textValue ?? (typeof children === "string" ? children : undefined), render: surfaceRender("div", state => ({
|
|
32
|
+
color: state.isSelected ? selectionColor(base, colors) : colorLevel(base, "subtle", colors),
|
|
33
|
+
depth: "flat",
|
|
34
|
+
radius: "full",
|
|
35
|
+
interaction: { hovered: state.isHovered, pressed: state.isPressed, focusVisible: state.isFocusVisible, disabled: state.isDisabled }
|
|
36
|
+
})), style: state => ({
|
|
37
|
+
display: "inline-flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
gap: metrics.gap / 2,
|
|
40
|
+
height: metrics.height,
|
|
41
|
+
paddingInlineStart: metrics.inset,
|
|
42
|
+
paddingInlineEnd: state.allowsRemoving ? 4 : metrics.inset,
|
|
43
|
+
boxSizing: "border-box",
|
|
44
|
+
// The group root already applies its own text level.
|
|
45
|
+
fontSize: relativeFontSize(smaller[size], size),
|
|
46
|
+
fontWeight: controlFontWeight,
|
|
47
|
+
whiteSpace: "nowrap",
|
|
48
|
+
cursor: state.selectionMode === "none" ? "default" : "pointer",
|
|
49
|
+
outlineOffset: -1
|
|
50
|
+
}), children: state => _jsxs(_Fragment, { children: [children, state.allowsRemoving && _jsx(FieldButton, { slot: "remove", metrics: metrics, "aria-label": "Remove", children: _jsx(X, { ...iconProps(Math.round(metrics.height / 2)) }) })] }) });
|
|
51
|
+
}
|
|
52
|
+
export const TagGroup = Object.assign(TagGroupRoot, { Tag: TagGroupTag });
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type Color } from "./foundation/color.js";
|
|
3
|
+
export interface ToastContent {
|
|
4
|
+
readonly title: ReactNode;
|
|
5
|
+
readonly description?: ReactNode;
|
|
6
|
+
/** The meaning, as a color role: `info` by default. It sets the icon. */
|
|
7
|
+
readonly color?: Color;
|
|
8
|
+
}
|
|
9
|
+
export interface ToastOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Milliseconds before the Toast closes itself. Omitted, it stays until
|
|
12
|
+
* closed. Keep at least five seconds, so it can be read.
|
|
13
|
+
*/
|
|
14
|
+
readonly timeout?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Shows a brief message that does not interrupt, such as "Saved". It appears
|
|
18
|
+
* in the ToastRegion and returns a key that closes it early.
|
|
19
|
+
*/
|
|
20
|
+
export declare const toast: Readonly<{
|
|
21
|
+
show(content: ToastContent, options?: ToastOptions): string;
|
|
22
|
+
close(key: string): void;
|
|
23
|
+
}>;
|
|
24
|
+
export interface ToastRegionProps {
|
|
25
|
+
readonly className?: string;
|
|
26
|
+
readonly style?: CSSProperties;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Where Toasts appear: stacked at the end of the viewport bottom, above the
|
|
30
|
+
* interface, at the large spacing step from its edges. Render it once.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ToastRegion({ className, style }: ToastRegionProps): import("react").JSX.Element;
|