@phreshos/react-ui 0.1.38 → 0.1.40

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.
Files changed (45) hide show
  1. package/dist/alert.js +3 -4
  2. package/dist/app-layout.d.ts +2 -0
  3. package/dist/app-layout.js +54 -27
  4. package/dist/avatar.d.ts +23 -0
  5. package/dist/avatar.js +42 -0
  6. package/dist/badge.d.ts +20 -0
  7. package/dist/badge.js +31 -0
  8. package/dist/breadcrumbs.d.ts +20 -0
  9. package/dist/breadcrumbs.js +43 -0
  10. package/dist/control/control.d.ts +2 -0
  11. package/dist/control/control.js +4 -0
  12. package/dist/control/meaning.d.ts +5 -0
  13. package/dist/control/meaning.js +10 -0
  14. package/dist/dialog.js +2 -2
  15. package/dist/foundation/motion-style.d.ts +8 -0
  16. package/dist/foundation/motion-style.js +13 -0
  17. package/dist/link.d.ts +18 -0
  18. package/dist/link.js +32 -0
  19. package/dist/loading.d.ts +20 -0
  20. package/dist/loading.js +66 -0
  21. package/dist/main.d.ts +13 -1
  22. package/dist/main.js +12 -0
  23. package/dist/meter.d.ts +26 -0
  24. package/dist/meter.js +29 -0
  25. package/dist/progress-bar.d.ts +3 -0
  26. package/dist/progress-bar.js +7 -3
  27. package/dist/readiness.d.ts +46 -0
  28. package/dist/readiness.js +78 -0
  29. package/dist/search-field.d.ts +13 -0
  30. package/dist/search-field.js +27 -0
  31. package/dist/skeleton.d.ts +15 -0
  32. package/dist/skeleton.js +29 -0
  33. package/dist/spinner.d.ts +2 -0
  34. package/dist/spinner.js +5 -2
  35. package/dist/surface/surface.d.ts +11 -0
  36. package/dist/surface/surface.js +17 -7
  37. package/dist/tag-group.d.ts +21 -0
  38. package/dist/tag-group.js +52 -0
  39. package/dist/toast.d.ts +32 -0
  40. package/dist/toast.js +62 -0
  41. package/dist/typography.d.ts +49 -0
  42. package/dist/typography.js +71 -0
  43. package/dist/window.d.ts +4 -1
  44. package/dist/window.js +2 -0
  45. package/package.json +1 -1
package/dist/alert.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
- import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
4
3
  import { controlFontSizes, controlFontWeight, useControlMetrics } from "./control/control.js";
5
4
  import { iconProps } from "./control/icon.js";
5
+ import { meaningIcon, urgentMeaning } from "./control/meaning.js";
6
6
  import { colorLevel, resolveColor } from "./foundation/color.js";
7
7
  import { resolveRadius } from "./foundation/radius.js";
8
8
  import { Surface } from "./surface/surface.js";
9
- const meaningIcons = { danger: CircleAlert, warning: TriangleAlert, success: CircleCheck };
10
9
  /**
11
10
  * A message that stays in place, such as a failed save. It is painted in the
12
11
  * subtle level of its color, like a selection, and leads with the icon of its
@@ -15,8 +14,8 @@ const meaningIcons = { danger: CircleAlert, warning: TriangleAlert, success: Cir
15
14
  export const Alert = forwardRef(function Alert({ color = "info", title, icon, children, radius, style, ...properties }, ref) {
16
15
  const metrics = useControlMetrics();
17
16
  const { colors } = metrics.visual;
18
- const urgent = color === "danger" || color === "warning";
19
- const MeaningIcon = meaningIcons[color] ?? Info;
17
+ const urgent = urgentMeaning(color);
18
+ const MeaningIcon = meaningIcon(color);
20
19
  return _jsxs(Surface, { ...properties, ref: ref, role: urgent ? "alert" : "status", color: colorLevel(resolveColor(color, colors), "subtle", colors), depth: "flat", radius: radius ?? resolveRadius("medium", metrics.visual.radius), style: {
21
20
  display: "grid",
22
21
  gridTemplateColumns: icon === false ? "minmax(0, 1fr)" : "auto minmax(0, 1fr)",
@@ -5,7 +5,9 @@ export interface AppLayoutProps extends HTMLAttributes<HTMLDivElement> {
5
5
  readonly sidebarWidth?: CSSProperties["width"];
6
6
  }
7
7
  export type AppLayoutRegionProps = HTMLAttributes<HTMLElement>;
8
+ export type AppLayoutTitleProps = HTMLAttributes<HTMLHeadingElement>;
8
9
  export declare const AppLayout: import("react").ForwardRefExoticComponent<AppLayoutProps & import("react").RefAttributes<HTMLDivElement>> & {
10
+ Title: import("react").ForwardRefExoticComponent<AppLayoutTitleProps & import("react").RefAttributes<HTMLHeadingElement>>;
9
11
  Sidebar: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
10
12
  Header: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
11
13
  Content: import("react").ForwardRefExoticComponent<AppLayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
@@ -1,42 +1,66 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, forwardRef, useContext } from "react";
3
- import { useControlMetrics } from "./control/control.js";
4
- import { separatorColor } from "./control/field.js";
3
+ import { controlFontSizes, controlFontWeight } from "./control/control.js";
5
4
  import { headerHeight } from "./foundation/layout.js";
5
+ import { resolveRadius } from "./foundation/radius.js";
6
6
  import { scale } from "./foundation/scale.js";
7
+ import { useVisual } from "./foundation/visual.js";
7
8
  import { ScrollArea } from "./scroll-area.js";
8
- const AppLayoutContext = createContext(null);
9
+ import { Surface } from "./surface/surface.js";
10
+ const AppLayoutContext = createContext(false);
9
11
  /**
10
- * The frame of a program's interface: a sidebar beside a header, the content,
11
- * and an optional footer. It owns only the regions, their scrolling, and the
12
- * hairlines between them; the program places its own components inside.
12
+ * The frame of a program's interface: a title above a sidebar, and a header,
13
+ * the content, and an optional footer beside them. Only the content is a
14
+ * Surface, the same inset region as a Panel's content; the other regions sit
15
+ * on whatever holds the layout, separated by spacing alone.
13
16
  */
14
17
  const AppLayoutRoot = forwardRef(function AppLayout({ children, sidebarWidth, style, ...properties }, ref) {
15
- const metrics = useControlMetrics();
16
- const { spacing } = metrics.visual;
17
- return _jsx(AppLayoutContext.Provider, { value: { separator: separatorColor(metrics), spacing }, children: _jsx("div", { ...properties, ref: ref, style: {
18
+ const visual = useVisual();
19
+ const inset = scale(visual.spacing, "small");
20
+ return _jsx(AppLayoutContext.Provider, { value: true, children: _jsx("div", { ...properties, ref: ref, style: {
18
21
  display: "grid",
19
- gridTemplateColumns: `${typeof sidebarWidth === "number" ? `${sidebarWidth}px` : sidebarWidth ?? `${spacing * 18}px`} minmax(0, 1fr)`,
22
+ gridTemplateColumns: `${typeof sidebarWidth === "number" ? `${sidebarWidth}px` : sidebarWidth ?? `${visual.spacing * 18}px`} minmax(0, 1fr)`,
20
23
  gridTemplateRows: "auto minmax(0, 1fr) auto",
21
- gridTemplateAreas: `"sidebar header" "sidebar content" "sidebar footer"`,
24
+ gridTemplateAreas: `"title header" "sidebar content" "sidebar footer"`,
25
+ columnGap: inset,
26
+ // The header row supplies the space above the content, as in a Panel.
27
+ padding: inset,
28
+ paddingTop: 0,
29
+ boxSizing: "border-box",
22
30
  width: "100%",
23
31
  height: "100%",
24
32
  minWidth: 0,
25
33
  minHeight: 0,
26
34
  // The frame of a program: its text takes the Appearance text color.
27
- color: metrics.visual.colors.foreground,
35
+ color: visual.colors.foreground,
28
36
  fontFamily: "inherit",
29
37
  ...style
30
38
  }, children: children }) });
31
39
  });
40
+ /** The program's title above the sidebar, in the header row. */
41
+ const AppLayoutTitle = forwardRef(function AppLayoutTitle({ style, ...properties }, ref) {
42
+ const { spacing } = useLayout();
43
+ return _jsx("h1", { ...properties, ref: ref, style: {
44
+ gridArea: "title",
45
+ display: "flex",
46
+ alignItems: "center",
47
+ minWidth: 0,
48
+ minHeight: headerHeight(spacing),
49
+ margin: 0,
50
+ paddingInline: scale(spacing, "small"),
51
+ fontSize: controlFontSizes.large,
52
+ fontWeight: controlFontWeight,
53
+ ...style
54
+ } });
55
+ });
32
56
  /** The sidebar, such as a program's navigation. It scrolls on its own. */
33
57
  const AppLayoutSidebar = forwardRef(function AppLayoutSidebar({ children, style, ...properties }, ref) {
34
- const { separator, spacing } = useLayout();
35
- return _jsx("aside", { ...properties, ref: ref, style: { gridArea: "sidebar", minHeight: 0, boxSizing: "border-box", borderInlineEnd: `1px solid ${separator}`, ...style }, children: _jsx(ScrollArea, { style: { height: "100%" }, children: _jsx("div", { style: { padding: scale(spacing, "small") }, children: children }) }) });
58
+ const { spacing } = useLayout();
59
+ return _jsx("aside", { ...properties, ref: ref, style: { gridArea: "sidebar", minHeight: 0, ...style }, children: _jsx(ScrollArea, { style: { height: "100%" }, children: _jsx("div", { style: { padding: scale(spacing, "small") }, children: children }) }) });
36
60
  });
37
61
  /** The header row above the content, as tall as a Window or Panel header. */
38
62
  const AppLayoutHeader = forwardRef(function AppLayoutHeader({ style, ...properties }, ref) {
39
- const { separator, spacing } = useLayout();
63
+ const { spacing } = useLayout();
40
64
  return _jsx("header", { ...properties, ref: ref, style: {
41
65
  gridArea: "header",
42
66
  display: "flex",
@@ -44,20 +68,24 @@ const AppLayoutHeader = forwardRef(function AppLayoutHeader({ style, ...properti
44
68
  gap: scale(spacing, "small"),
45
69
  minWidth: 0,
46
70
  minHeight: headerHeight(spacing),
47
- paddingInline: spacing,
48
- boxSizing: "border-box",
49
- borderBlockEnd: `1px solid ${separator}`,
50
71
  ...style
51
72
  } });
52
73
  });
53
- /** The content. It scrolls on its own, padded by the Appearance spacing. */
74
+ /** The content, on its Surface. It scrolls on its own, padded by the Appearance spacing. */
54
75
  const AppLayoutContent = forwardRef(function AppLayoutContent({ children, style, ...properties }, ref) {
55
- const { spacing } = useLayout();
56
- return _jsx("main", { ...properties, ref: ref, style: { gridArea: "content", minWidth: 0, minHeight: 0, ...style }, children: _jsx(ScrollArea, { style: { height: "100%" }, children: _jsx("div", { style: { padding: spacing }, children: children }) }) });
76
+ const { spacing, radius } = useLayout();
77
+ return _jsx(Surface, { as: "main", ...properties, ref: ref, material: "extended", radius: resolveRadius("medium", radius), style: {
78
+ gridArea: "content",
79
+ minWidth: 0,
80
+ minHeight: 0,
81
+ // The Surface owns its shape: what scrolls inside stays within its corners.
82
+ overflow: "clip",
83
+ ...style
84
+ }, children: _jsx(ScrollArea, { style: { height: "100%" }, children: _jsx("div", { style: { padding: spacing }, children: children }) }) });
57
85
  });
58
86
  /** An optional row below the content, such as the actions that apply to it. */
59
87
  const AppLayoutFooter = forwardRef(function AppLayoutFooter({ style, ...properties }, ref) {
60
- const { separator, spacing } = useLayout();
88
+ const { spacing } = useLayout();
61
89
  return _jsx("footer", { ...properties, ref: ref, style: {
62
90
  gridArea: "footer",
63
91
  display: "flex",
@@ -65,18 +93,17 @@ const AppLayoutFooter = forwardRef(function AppLayoutFooter({ style, ...properti
65
93
  justifyContent: "flex-end",
66
94
  gap: scale(spacing, "small"),
67
95
  minWidth: 0,
68
- padding: `${scale(spacing, "small")}px ${spacing}px`,
69
- borderBlockStart: `1px solid ${separator}`,
96
+ paddingTop: scale(spacing, "small"),
70
97
  ...style
71
98
  } });
72
99
  });
73
100
  function useLayout() {
74
- const context = useContext(AppLayoutContext);
75
- if (context == null)
101
+ if (!useContext(AppLayoutContext))
76
102
  throw new Error("AppLayout parts must be used inside AppLayout");
77
- return context;
103
+ return useVisual();
78
104
  }
79
105
  export const AppLayout = Object.assign(AppLayoutRoot, {
106
+ Title: AppLayoutTitle,
80
107
  Sidebar: AppLayoutSidebar,
81
108
  Header: AppLayoutHeader,
82
109
  Content: AppLayoutContent,
@@ -0,0 +1,23 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { type Color } from "./foundation/color.js";
3
+ import type { ScaleLevel } from "./foundation/scale.js";
4
+ export interface AvatarProps {
5
+ /** Who it shows. It names the Avatar for assistive technology and supplies the initials. */
6
+ readonly name: string;
7
+ /** A picture that replaces the initials. */
8
+ readonly src?: string;
9
+ /** Replaces the initials, for example with an icon for an agent. */
10
+ readonly children?: ReactNode;
11
+ /** Otherwise one of the Appearance roles, chosen from the name so a person keeps one color. */
12
+ readonly color?: Color;
13
+ /** The Avatar is as tall as a control of this size. */
14
+ readonly size?: ScaleLevel;
15
+ readonly className?: string;
16
+ readonly style?: CSSProperties;
17
+ }
18
+ /**
19
+ * A person or an agent, drawn as a circle as tall as a control: a picture, or
20
+ * initials on the soft level of a color. The text color follows from the
21
+ * paint like any Surface.
22
+ */
23
+ export declare const Avatar: import("react").ForwardRefExoticComponent<AvatarProps & import("react").RefAttributes<HTMLSpanElement>>;
package/dist/avatar.js ADDED
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { controlFontWeight, useControlMetrics } from "./control/control.js";
4
+ import { colorLevel, resolveColor } from "./foundation/color.js";
5
+ import { Surface } from "./surface/surface.js";
6
+ const roles = ["primary", "secondary", "success", "warning", "info", "danger"];
7
+ /** The same name always lands on the same role. */
8
+ function nameRole(name) {
9
+ let hash = 0;
10
+ for (const character of name)
11
+ hash = (hash * 31 + character.codePointAt(0)) >>> 0;
12
+ return roles[hash % roles.length];
13
+ }
14
+ function initials(name) {
15
+ const words = name.trim().split(/\s+/).filter(Boolean);
16
+ return words.slice(0, 2).map(word => [...word][0].toLocaleUpperCase()).join("");
17
+ }
18
+ /**
19
+ * A person or an agent, drawn as a circle as tall as a control: a picture, or
20
+ * initials on the soft level of a color. The text color follows from the
21
+ * paint like any Surface.
22
+ */
23
+ export const Avatar = forwardRef(function Avatar({ name, src, children, color, size, className, style }, ref) {
24
+ const metrics = useControlMetrics(size);
25
+ const { colors } = metrics.visual;
26
+ const fill = colorLevel(resolveColor(color ?? nameRole(name), colors), "soft", colors);
27
+ return _jsx(Surface, { as: "span", ref: ref, role: "img", "aria-label": name, className: className, color: fill, depth: "flat", radius: "full", style: {
28
+ display: "inline-grid",
29
+ placeItems: "center",
30
+ flexShrink: 0,
31
+ width: metrics.height,
32
+ height: metrics.height,
33
+ overflow: "hidden",
34
+ fontSize: metrics.fontSize,
35
+ fontWeight: controlFontWeight,
36
+ lineHeight: 1,
37
+ userSelect: "none",
38
+ ...style
39
+ }, children: src !== undefined
40
+ ? _jsx("img", { src: src, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", borderRadius: "inherit" } })
41
+ : children ?? initials(name) });
42
+ });
@@ -0,0 +1,20 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { type Color } from "./foundation/color.js";
3
+ import type { ScaleLevel } from "./foundation/scale.js";
4
+ export interface BadgeProps {
5
+ readonly children?: ReactNode;
6
+ /** The color role of the state it names. Omitted, the Badge is neutral: the content color. */
7
+ readonly color?: Color;
8
+ /** Leads with a dot in the full color, for a live state such as `running`. */
9
+ readonly dot?: boolean;
10
+ /** The badge sizes its text one control step below this level. */
11
+ readonly size?: ScaleLevel;
12
+ readonly className?: string;
13
+ readonly style?: CSSProperties;
14
+ }
15
+ /**
16
+ * A short label for a state, such as `running` or `stopped`. It is not a
17
+ * control: it is painted in the subtle level of its color, like a selection,
18
+ * with its text in the strong level so the color stays readable.
19
+ */
20
+ export declare const Badge: import("react").ForwardRefExoticComponent<BadgeProps & import("react").RefAttributes<HTMLSpanElement>>;
package/dist/badge.js ADDED
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { controlFontSizes, controlFontWeight, useControlMetrics } from "./control/control.js";
4
+ import { colorLevel, resolveColor } from "./foundation/color.js";
5
+ import { Surface } from "./surface/surface.js";
6
+ const smaller = Object.freeze({ xsmall: "xsmall", small: "xsmall", medium: "small", large: "medium", xlarge: "large" });
7
+ /**
8
+ * A short label for a state, such as `running` or `stopped`. It is not a
9
+ * control: it is painted in the subtle level of its color, like a selection,
10
+ * with its text in the strong level so the color stays readable.
11
+ */
12
+ export const Badge = forwardRef(function Badge({ children, color = "foreground", dot = false, size, className, style }, ref) {
13
+ const metrics = useControlMetrics(size);
14
+ const { colors } = metrics.visual;
15
+ const base = resolveColor(color, colors);
16
+ const fill = colorLevel(base, "subtle", colors);
17
+ return _jsxs(Surface, { as: "span", ref: ref, className: className, color: fill, depth: "flat", radius: "full", style: {
18
+ display: "inline-flex",
19
+ alignItems: "center",
20
+ gap: metrics.gap / 2,
21
+ paddingInline: metrics.gap,
22
+ paddingBlock: metrics.gap / 4,
23
+ color: colorLevel(base, "strong", colors),
24
+ fontSize: controlFontSizes[smaller[size ?? "medium"]],
25
+ fontWeight: controlFontWeight,
26
+ lineHeight: 1.45,
27
+ whiteSpace: "nowrap",
28
+ verticalAlign: "middle",
29
+ ...style
30
+ }, children: [dot && _jsx("span", { "aria-hidden": "true", style: { width: "0.5em", height: "0.5em", borderRadius: "50%", background: base, flexShrink: 0 } }), children] });
31
+ });
@@ -0,0 +1,20 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { BreadcrumbsProps as AriaBreadcrumbsProps } from "react-aria-components";
3
+ import type { ScaleLevel } from "./foundation/scale.js";
4
+ export interface BreadcrumbsProps extends Omit<AriaBreadcrumbsProps<object>, "className" | "style" | "children"> {
5
+ readonly children?: ReactNode;
6
+ readonly size?: ScaleLevel;
7
+ readonly className?: string;
8
+ readonly style?: CSSProperties;
9
+ }
10
+ export interface BreadcrumbsItemProps {
11
+ readonly children?: ReactNode;
12
+ readonly href?: string;
13
+ readonly id?: string;
14
+ }
15
+ /** One step of the path. Its separator follows it, except after the current page. */
16
+ declare function BreadcrumbsItem({ children, href, id }: BreadcrumbsItemProps): import("react").JSX.Element;
17
+ export declare const Breadcrumbs: import("react").ForwardRefExoticComponent<BreadcrumbsProps & import("react").RefAttributes<HTMLOListElement>> & {
18
+ Item: typeof BreadcrumbsItem;
19
+ };
20
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, forwardRef, useContext } from "react";
3
+ import { Breadcrumb as AriaBreadcrumb, Breadcrumbs as AriaBreadcrumbs, Link as AriaLink } from "react-aria-components";
4
+ import { ChevronRight } from "lucide-react";
5
+ import { controlFontWeight, controlOpacity, transition, useControlMetrics } from "./control/control.js";
6
+ import { iconProps } from "./control/icon.js";
7
+ import { surfacePaint } from "./surface/surface.js";
8
+ const BreadcrumbsContext = createContext(null);
9
+ /** Where a page sits in a hierarchy: each ancestor is a link, the last entry is the current page. */
10
+ const BreadcrumbsRoot = forwardRef(function Breadcrumbs({ children, size, style, ...properties }, ref) {
11
+ const metrics = useControlMetrics(size);
12
+ return _jsx(BreadcrumbsContext.Provider, { value: metrics, children: _jsx(AriaBreadcrumbs, { ...properties, ref: ref, style: {
13
+ display: "flex",
14
+ flexWrap: "wrap",
15
+ alignItems: "center",
16
+ gap: metrics.gap / 2,
17
+ margin: 0,
18
+ padding: 0,
19
+ listStyle: "none",
20
+ fontSize: metrics.fontSize,
21
+ lineHeight: 1.45,
22
+ ...style
23
+ }, children: children }) });
24
+ });
25
+ /** One step of the path. Its separator follows it, except after the current page. */
26
+ function BreadcrumbsItem({ children, href, id }) {
27
+ const metrics = useContext(BreadcrumbsContext);
28
+ if (metrics === null)
29
+ throw new Error("Breadcrumbs.Item must be inside Breadcrumbs");
30
+ const ring = surfacePaint(metrics.visual, metrics.visual.colors.foreground, "flat", "none", false, { focusVisible: true }).ring;
31
+ return _jsx(AriaBreadcrumb, { id: id, style: { display: "flex", alignItems: "center", gap: metrics.gap / 2 }, children: ({ isCurrent }) => _jsxs(_Fragment, { children: [_jsx(AriaLink, { href: isCurrent ? undefined : href, style: state => ({
32
+ ...transition(metrics.visual, "opacity, outline-color"),
33
+ color: "inherit",
34
+ textDecoration: "none",
35
+ borderRadius: "0.2em",
36
+ fontWeight: isCurrent ? controlFontWeight : undefined,
37
+ opacity: isCurrent || state.isHovered ? 1 : controlOpacity.secondary,
38
+ outline: `3px solid ${state.isFocusVisible ? ring : "transparent"}`,
39
+ outlineOffset: 1,
40
+ cursor: isCurrent ? "default" : "pointer"
41
+ }), children: children }), !isCurrent && _jsx(ChevronRight, { ...iconProps(Math.round(metrics.height / 2)), style: { opacity: controlOpacity.placeholder } })] }) });
42
+ }
43
+ export const Breadcrumbs = Object.assign(BreadcrumbsRoot, { Item: BreadcrumbsItem });
@@ -27,6 +27,8 @@ export interface FieldProps {
27
27
  export type ControlOverrides = keyof ControlProps | keyof FieldProps | "children" | "isDisabled" | "isRequired" | "isInvalid" | "isReadOnly";
28
28
  /** Relative text scale for control content. Only a component's root applies it. */
29
29
  export declare const controlFontSizes: Readonly<Record<ScaleLevel, string>>;
30
+ /** A control text size relative to an element that already applies another level. */
31
+ export declare function relativeFontSize(level: ScaleLevel, within: ScaleLevel): string;
30
32
  export declare const controlFontWeight = 600;
31
33
  /** Attenuation shared by every control family. */
32
34
  export declare const controlOpacity: Readonly<{
@@ -10,6 +10,10 @@ export const controlFontSizes = Object.freeze({
10
10
  large: "0.875em",
11
11
  xlarge: "0.9375em"
12
12
  });
13
+ /** A control text size relative to an element that already applies another level. */
14
+ export function relativeFontSize(level, within) {
15
+ return `${Number.parseFloat(controlFontSizes[level]) / Number.parseFloat(controlFontSizes[within])}em`;
16
+ }
13
17
  export const controlFontWeight = 600;
14
18
  /** Attenuation shared by every control family. */
15
19
  export const controlOpacity = Object.freeze({
@@ -0,0 +1,5 @@
1
+ import type { Color } from "../foundation/color.js";
2
+ /** The icon of a message's meaning: its color role, or `i` for anything else. */
3
+ export declare function meaningIcon(color: Color): import("lucide-react").LucideIcon;
4
+ /** Whether a message of this meaning interrupts, and is announced as it appears. */
5
+ export declare function urgentMeaning(color: Color): boolean;
@@ -0,0 +1,10 @@
1
+ import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
2
+ const meaningIcons = { danger: CircleAlert, warning: TriangleAlert, success: CircleCheck };
3
+ /** The icon of a message's meaning: its color role, or `i` for anything else. */
4
+ export function meaningIcon(color) {
5
+ return meaningIcons[color] ?? Info;
6
+ }
7
+ /** Whether a message of this meaning interrupts, and is announced as it appears. */
8
+ export function urgentMeaning(color) {
9
+ return color === "danger" || color === "warning";
10
+ }
package/dist/dialog.js CHANGED
@@ -6,7 +6,7 @@ import { controlFontSizes, controlFontWeight, controlOpacity } from "./control/c
6
6
  import { ariaOpenState } from "./control/open-state.js";
7
7
  import { colorOpacity, darkCanvas } from "./foundation/color.js";
8
8
  import { resolveDirection, useDirection } from "./foundation/direction.js";
9
- import MotionStyle, { backdropMotionClass, overlayMotionClass, overlayTransition } from "./foundation/motion-style.js";
9
+ import MotionStyle, { backdropMotionClass, overlayLayer, overlayMotionClass, overlayTransition } from "./foundation/motion-style.js";
10
10
  import { scale } from "./foundation/scale.js";
11
11
  import { useVisual } from "./foundation/visual.js";
12
12
  import { floatingShadow } from "./surface/shadow-options.js";
@@ -34,7 +34,7 @@ export const DialogBackdrop = forwardRef(function DialogBackdrop({ children, cla
34
34
  ...transition,
35
35
  position: "fixed",
36
36
  inset: 0,
37
- zIndex: 1_000,
37
+ zIndex: overlayLayer,
38
38
  display: "grid",
39
39
  placeItems: "center",
40
40
  boxSizing: "border-box",
@@ -2,6 +2,14 @@ import type { CSSProperties } from "react";
2
2
  import type { Visual } from "./visual.js";
3
3
  /** Per-overlay variables consumed by the shared entrance keyframes. */
4
4
  export declare function overlayTransition(visual: Visual): CSSProperties;
5
+ /**
6
+ * How long one pass of a repeating motion takes, such as an indeterminate
7
+ * progress sweep or a skeleton pulse: eight Appearance transactions, and never
8
+ * quicker than a calm breath.
9
+ */
10
+ export declare function loopDuration(visual: Visual): number;
11
+ /** The stacking layer of content above an interface: dialogs and notifications. */
12
+ export declare const overlayLayer = 1000;
5
13
  export declare const overlayMotionClass = "phreshos-ui-overlay";
6
14
  export declare const backdropMotionClass = "phreshos-ui-backdrop";
7
15
  /** React hoists and deduplicates this stylesheet, including inside portals. */
@@ -6,6 +6,16 @@ export function overlayTransition(visual) {
6
6
  "--phreshos-ui-motion-easing": visual.easing
7
7
  };
8
8
  }
9
+ /**
10
+ * How long one pass of a repeating motion takes, such as an indeterminate
11
+ * progress sweep or a skeleton pulse: eight Appearance transactions, and never
12
+ * quicker than a calm breath.
13
+ */
14
+ export function loopDuration(visual) {
15
+ return Math.max(900, visual.appearance.transaction.duration * 8);
16
+ }
17
+ /** The stacking layer of content above an interface: dialogs and notifications. */
18
+ export const overlayLayer = 1_000;
9
19
  export const overlayMotionClass = "phreshos-ui-overlay";
10
20
  export const backdropMotionClass = "phreshos-ui-backdrop";
11
21
  const stylesheet = `
@@ -38,6 +48,9 @@ const stylesheet = `
38
48
  @keyframes phreshos-ui-spin {
39
49
  to { rotate: 360deg; }
40
50
  }
51
+ @keyframes phreshos-ui-pulse {
52
+ 50% { opacity: 0.55; }
53
+ }
41
54
  @keyframes phreshos-ui-sweep {
42
55
  from { inset-inline-start: -40%; }
43
56
  to { inset-inline-start: 100%; }
package/dist/link.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { type LinkProps as AriaLinkProps } from "react-aria-components";
3
+ import { type Color } from "./foundation/color.js";
4
+ export interface LinkProps extends Omit<AriaLinkProps, "children" | "className" | "style" | "isDisabled"> {
5
+ readonly children?: ReactNode;
6
+ /** The link's color role. Its text takes the strong level, so it reads on the canvas. */
7
+ readonly color?: Color;
8
+ readonly disabled?: boolean;
9
+ readonly className?: string;
10
+ readonly style?: CSSProperties;
11
+ }
12
+ /**
13
+ * A link inside text. Links inside the app go through the `navigate` given to
14
+ * UIProvider, like a Button with an href. Its underline rests at the
15
+ * placeholder strength and fills in on hover; focus draws the same ring as
16
+ * every other control.
17
+ */
18
+ export declare const Link: import("react").ForwardRefExoticComponent<LinkProps & import("react").RefAttributes<HTMLAnchorElement>>;
package/dist/link.js ADDED
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { Link as AriaLink } from "react-aria-components";
4
+ import { controlOpacity, transition } from "./control/control.js";
5
+ import { colorLevel, colorOpacity, resolveColor } from "./foundation/color.js";
6
+ import { useVisual } from "./foundation/visual.js";
7
+ import { surfacePaint } from "./surface/surface.js";
8
+ /**
9
+ * A link inside text. Links inside the app go through the `navigate` given to
10
+ * UIProvider, like a Button with an href. Its underline rests at the
11
+ * placeholder strength and fills in on hover; focus draws the same ring as
12
+ * every other control.
13
+ */
14
+ export const Link = forwardRef(function Link({ color = "primary", disabled, className, style, children, ...properties }, ref) {
15
+ const visual = useVisual();
16
+ const paint = colorLevel(resolveColor(color, visual.colors), "strong", visual.colors);
17
+ const ring = surfacePaint(visual, paint, "flat", "none", false, { focusVisible: true }).ring;
18
+ return _jsx(AriaLink, { ...properties, ref: ref, className: className, isDisabled: disabled, style: state => ({
19
+ ...transition(visual, "text-decoration-color, outline-color"),
20
+ color: paint,
21
+ textDecorationLine: "underline",
22
+ textDecorationThickness: "from-font",
23
+ textUnderlineOffset: "0.2em",
24
+ textDecorationColor: state.isHovered ? paint : colorOpacity(paint, controlOpacity.placeholder),
25
+ borderRadius: "0.2em",
26
+ outline: `3px solid ${state.isFocusVisible ? ring : "transparent"}`,
27
+ outlineOffset: 1,
28
+ cursor: state.isDisabled ? "not-allowed" : "pointer",
29
+ opacity: state.isDisabled ? controlOpacity.secondary : undefined,
30
+ ...style
31
+ }), children: children });
32
+ });
@@ -0,0 +1,20 @@
1
+ import type { CSSProperties } from "react";
2
+ import { type ReadinessProps } from "./readiness.js";
3
+ import { type SurfaceOwnProps } from "./surface/surface.js";
4
+ /** The cover is a Surface: its own props set its color, depth, material, and radius. */
5
+ export interface LoadingProps extends Pick<ReadinessProps, "children" | "delay">, SurfaceOwnProps {
6
+ readonly className?: string;
7
+ readonly style?: CSSProperties;
8
+ /** Lists every requirement with a message, each with its own progress, instead of one message. */
9
+ readonly steps?: boolean;
10
+ }
11
+ /**
12
+ * A Readiness boundary drawn the way the System draws loading: a Surface that
13
+ * covers the waiting interface, with a Spinner and the message of the first
14
+ * waiting requirement, or every step. A requirement's detail is its message
15
+ * when it is a string; other requirements are waited for without being shown.
16
+ *
17
+ * The cover fills the nearest positioned ancestor, which owns its shape. It appears at once and
18
+ * fades out with the Appearance transaction when everything is ready.
19
+ */
20
+ export declare function Loading({ children, delay, steps, ...surface }: LoadingProps): import("react").JSX.Element;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Check } from "lucide-react";
4
+ import { useControlMetrics } from "./control/control.js";
5
+ import { iconProps } from "./control/icon.js";
6
+ import { Readiness } from "./readiness.js";
7
+ import { Spinner, spinnerDiameter } from "./spinner.js";
8
+ import { Surface } from "./surface/surface.js";
9
+ import { Text } from "./typography.js";
10
+ /**
11
+ * A Readiness boundary drawn the way the System draws loading: a Surface that
12
+ * covers the waiting interface, with a Spinner and the message of the first
13
+ * waiting requirement, or every step. A requirement's detail is its message
14
+ * when it is a string; other requirements are waited for without being shown.
15
+ *
16
+ * The cover fills the nearest positioned ancestor, which owns its shape. It appears at once and
17
+ * fades out with the Appearance transaction when everything is ready.
18
+ */
19
+ export function Loading({ children, delay, steps = false, ...surface }) {
20
+ return _jsx(Readiness, { delay: delay, status: state => _jsx(LoadingCover, { state: state, steps: steps, surface: surface }), children: children });
21
+ }
22
+ function LoadingCover({ state, steps, surface }) {
23
+ const metrics = useControlMetrics();
24
+ const { duration, easing } = metrics.visual;
25
+ const { ready } = state;
26
+ const [gone, setGone] = useState(false);
27
+ if (!ready && gone)
28
+ setGone(false);
29
+ // Leaves the tree once faded, so it stops being a layer that repaints over the interface.
30
+ useEffect(() => {
31
+ if (!ready)
32
+ return;
33
+ const timer = setTimeout(() => setGone(true), duration);
34
+ return () => clearTimeout(timer);
35
+ }, [ready, duration]);
36
+ // What was shown stays in place while fading out.
37
+ const shown = useRef([]);
38
+ if (!ready)
39
+ shown.current = state.requirements.flatMap(requirement => typeof requirement.detail === "string" ? [{ message: requirement.detail, ready: requirement.ready }] : []);
40
+ if (gone)
41
+ return null;
42
+ const { depth = "flat", radius = 0, style, ...paint } = surface;
43
+ const cover = {
44
+ position: "absolute",
45
+ inset: 0,
46
+ display: "grid",
47
+ placeItems: "center",
48
+ opacity: ready ? 0 : 1,
49
+ // New waiting covers at once; only readiness fades.
50
+ transition: ready ? `opacity ${duration}ms ${easing}` : "none",
51
+ pointerEvents: ready ? "none" : undefined,
52
+ ...style
53
+ };
54
+ const progress = steps ? _jsx(StepList, { steps: shown.current }) : _jsx(Message, { message: shown.current.find(step => !step.ready)?.message });
55
+ // Flat and without a shape of its own by default: the container it fills owns its shape.
56
+ return _jsx(Surface, { ...paint, depth: depth, radius: radius, role: "status", "aria-hidden": ready || undefined, style: cover, children: progress });
57
+ }
58
+ function Message({ message }) {
59
+ const metrics = useControlMetrics();
60
+ return _jsxs("div", { style: { display: "grid", justifyItems: "center", gap: metrics.gap * 2 }, children: [_jsx(Spinner, { decorative: true }), message !== undefined && _jsx(Text, { tone: "secondary", size: "small", children: message })] });
61
+ }
62
+ function StepList({ steps }) {
63
+ const metrics = useControlMetrics();
64
+ const indicator = spinnerDiameter(metrics.visual.spacing, "small");
65
+ return _jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: metrics.gap * 2 }, children: steps.map((step, index) => _jsxs("li", { "data-ready": step.ready, style: { display: "flex", alignItems: "center", gap: metrics.gap * 2 }, children: [step.ready ? _jsx(Check, { ...iconProps(indicator) }) : _jsx(Spinner, { decorative: true, size: "small" }), _jsx(Text, { tone: step.ready ? "secondary" : "default", size: "small", children: step.message })] }, index)) });
66
+ }
package/dist/main.d.ts CHANGED
@@ -41,10 +41,22 @@ export { Spinner, type SpinnerProps } from "./spinner.js";
41
41
  export { NumberField, type NumberFieldProps } from "./number-field.js";
42
42
  export { SegmentedControl, type SegmentedControlProps, type SegmentedControlItemProps } from "./segmented-control.js";
43
43
  export { Alert, type AlertProps } from "./alert.js";
44
+ export { Link, type LinkProps } from "./link.js";
45
+ export { Heading, Text, Kbd, Code, type HeadingProps, type TextProps, type KbdProps, type CodeProps } from "./typography.js";
46
+ export { Badge, type BadgeProps } from "./badge.js";
47
+ export { Meter, type MeterProps } from "./meter.js";
48
+ export { SearchField, type SearchFieldProps } from "./search-field.js";
49
+ export { Breadcrumbs, type BreadcrumbsProps, type BreadcrumbsItemProps } from "./breadcrumbs.js";
50
+ export { Avatar, type AvatarProps } from "./avatar.js";
51
+ export { TagGroup, type TagGroupProps, type TagGroupTagProps } from "./tag-group.js";
52
+ export { Skeleton, type SkeletonProps } from "./skeleton.js";
53
+ export { toast, ToastRegion, type ToastContent, type ToastOptions, type ToastRegionProps } from "./toast.js";
54
+ export { Readiness, useReadiness, useRequirement, type ReadinessProps, type ReadinessState, type Requirement } from "./readiness.js";
55
+ export { Loading, type LoadingProps } from "./loading.js";
44
56
  export { FileTrigger, type FileTriggerProps } from "./file-trigger.js";
45
57
  export { DropZone, type DropZoneProps } from "./drop-zone.js";
46
58
  export { Fieldset, type FieldsetProps } from "./fieldset.js";
47
- export { AppLayout, type AppLayoutProps, type AppLayoutRegionProps } from "./app-layout.js";
59
+ export { AppLayout, type AppLayoutProps, type AppLayoutRegionProps, type AppLayoutTitleProps } from "./app-layout.js";
48
60
  export { ColorSwatch, type ColorSwatchProps } from "./color-swatch.js";
49
61
  export { ColorSwatchPicker, type ColorSwatchPickerProps, type ColorSwatchPickerItemProps } from "./color-swatch-picker.js";
50
62
  export { ColorSlider, type ColorSliderProps } from "./color-slider.js";