@phreshos/react-ui 0.1.37 → 0.1.39

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.
@@ -9,7 +9,7 @@ export type AlertDialogContentProps = Omit<DialogContentProps, "role">;
9
9
  export declare const AlertDialogContent: import("react").ForwardRefExoticComponent<AlertDialogContentProps & import("react").RefAttributes<HTMLElement>>;
10
10
  /** A dialog requiring an explicit decision rather than ambient dismissal. */
11
11
  export declare const AlertDialog: typeof AlertDialogRoot & {
12
- Trigger: import("react").ForwardRefExoticComponent<import("./button.js").ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
12
+ Trigger: import("react").ForwardRefExoticComponent<import("./button.js").ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
13
13
  Backdrop: import("react").ForwardRefExoticComponent<DialogBackdropProps & import("react").RefAttributes<HTMLDivElement>>;
14
14
  Content: import("react").ForwardRefExoticComponent<AlertDialogContentProps & import("react").RefAttributes<HTMLElement>>;
15
15
  Header: import("react").ForwardRefExoticComponent<import("./dialog.js").DialogHeaderProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -17,6 +17,6 @@ export declare const AlertDialog: typeof AlertDialogRoot & {
17
17
  Description: import("react").ForwardRefExoticComponent<import("react-aria-components").TextProps & import("react").RefAttributes<HTMLElement>>;
18
18
  Body: import("react").ForwardRefExoticComponent<import("./dialog.js").DialogBodyProps & import("react").RefAttributes<HTMLDivElement>>;
19
19
  Footer: import("react").ForwardRefExoticComponent<import("./dialog.js").DialogFooterProps & import("react").RefAttributes<HTMLDivElement>>;
20
- Close: import("react").ForwardRefExoticComponent<import("./button.js").ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
20
+ Close: import("react").ForwardRefExoticComponent<import("./button.js").ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
21
21
  };
22
22
  export type AlertDialogProps = ComponentProps<typeof AlertDialogRoot>;
package/dist/button.d.ts CHANGED
@@ -1,20 +1,36 @@
1
1
  import type { ReactNode } from "react";
2
- import { type ButtonProps as AriaButtonProps } from "react-aria-components";
2
+ import { type ButtonProps as AriaButtonProps, type LinkProps as AriaLinkProps } from "react-aria-components";
3
3
  import { type ControlProps } from "./control/control.js";
4
4
  import type { Color } from "./foundation/color.js";
5
5
  import type { RadiusProps } from "./foundation/radius.js";
6
6
  import type { MaterialOverrides } from "./surface/material-options.js";
7
7
  import type { ShadowOverrides } from "./surface/shadow-options.js";
8
- type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "render" | "style">;
8
+ type NativeButtonProps = Omit<AriaButtonProps, "children" | "className" | "color" | "isDisabled" | "isPending" | "onClick" | "onPress" | "render" | "style">;
9
+ type NativeLinkProps = Omit<AriaLinkProps, "children" | "className" | "color" | "href" | "isDisabled" | "onClick" | "onPress" | "render" | "style">;
9
10
  /** A color from Appearance or CSS; omission keeps the Button neutral. */
10
11
  export type ButtonColor = Color;
11
- export interface ButtonProps extends NativeButtonProps, ControlProps, RadiusProps, MaterialOverrides, ShadowOverrides {
12
+ interface ButtonOwnProps extends ControlProps, RadiusProps, MaterialOverrides, ShadowOverrides {
12
13
  readonly children?: ReactNode;
13
- /** Prevents activation while keeping the Button focusable. */
14
- readonly pending?: boolean;
15
14
  /** Runs once for a normalized pointer, Enter, or Space activation. */
16
15
  readonly onPress?: () => void;
17
16
  }
18
- /** A raised Surface you act on. */
19
- export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
17
+ /** A Button that acts: a native `<button>`. */
18
+ export interface ButtonActionProps extends NativeButtonProps, ButtonOwnProps {
19
+ readonly href?: undefined;
20
+ /** Prevents activation while keeping the Button focusable. */
21
+ readonly pending?: boolean;
22
+ }
23
+ /**
24
+ * A Button that goes somewhere: a native `<a>` that looks and behaves like
25
+ * every other Button. Links inside the app use the `navigate` given to
26
+ * UIProvider; any other link is followed by the browser.
27
+ */
28
+ export interface ButtonLinkProps extends NativeLinkProps, ButtonOwnProps {
29
+ readonly href: string;
30
+ readonly pending?: never;
31
+ readonly type?: never;
32
+ }
33
+ export type ButtonProps = ButtonActionProps | ButtonLinkProps;
34
+ /** A raised Surface you act on, or follow when it has an `href`. */
35
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLAnchorElement | HTMLButtonElement>>;
20
36
  export {};
package/dist/button.js CHANGED
@@ -1,25 +1,38 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
- import { Button as AriaButton } from "react-aria-components";
3
+ import { Button as AriaButton, Link as AriaLink } from "react-aria-components";
4
4
  import { controlOpacity, transition, useControlMetrics } from "./control/control.js";
5
5
  import { surfaceRender } from "./control/surface-render.js";
6
- /** A raised Surface you act on. */
7
- export const Button = forwardRef(function Button({ children, color = "default", disabled, pending = false, radius, size, style, className, material, shadow, type = "button", ...properties }, ref) {
6
+ /** A raised Surface you act on, or follow when it has an `href`. */
7
+ export const Button = forwardRef(function Button(properties, ref) {
8
+ const { children, color = "default", disabled, radius, size, style, className, material, shadow, ...rest } = properties;
8
9
  const metrics = useControlMetrics(size, radius);
10
+ const surface = (state, pending) => ({
11
+ color,
12
+ material,
13
+ shadow,
14
+ radius: metrics.radius,
15
+ interaction: {
16
+ hovered: !state.isDisabled && !pending && state.isHovered,
17
+ pressed: !state.isDisabled && !pending && state.isPressed,
18
+ focusVisible: state.isFocusVisible,
19
+ disabled: state.isDisabled
20
+ }
21
+ });
22
+ if (rest.href !== undefined) {
23
+ const { pending: _pending, type: _type, ...link } = rest;
24
+ return _jsx(AriaLink, { ...link, ref: ref, className: className, isDisabled: disabled, render: (native, state) => {
25
+ // A disabled link has no destination: React Aria expects a span without an href.
26
+ const { href: _href, ...inert } = native;
27
+ return state.isDisabled
28
+ ? surfaceRender("span", state => surface(state, false))(inert, state)
29
+ : surfaceRender("a", state => surface(state, false))(native, state);
30
+ }, style: state => buttonStyle(metrics, !state.isDisabled && state.isPressed, state.isDisabled, false, style), children: children });
31
+ }
32
+ const { pending = false, type = "button", ...action } = rest;
9
33
  // Disabled state is read from React Aria, so a Button placed in a slot, such
10
34
  // as a calendar's month buttons, follows the owner that disables it.
11
- return _jsx(AriaButton, { ...properties, ref: ref, type: type, className: className, isDisabled: disabled, isPending: pending, render: surfaceRender("button", state => ({
12
- color,
13
- material,
14
- shadow,
15
- radius: metrics.radius,
16
- interaction: {
17
- hovered: !state.isDisabled && !pending && state.isHovered,
18
- pressed: !state.isDisabled && !pending && state.isPressed,
19
- focusVisible: state.isFocusVisible,
20
- disabled: state.isDisabled
21
- }
22
- })), style: state => buttonStyle(metrics, !state.isDisabled && !pending && state.isPressed, state.isDisabled, pending, style), children: children });
35
+ return _jsx(AriaButton, { ...action, ref: ref, type: type, className: className, isDisabled: disabled, isPending: pending, render: surfaceRender("button", state => surface(state, pending)), style: state => buttonStyle(metrics, !state.isDisabled && !pending && state.isPressed, state.isDisabled, pending, style), children: children });
23
36
  });
24
37
  function buttonStyle(metrics, pressed, disabled, pending, style) {
25
38
  return {
@@ -4,8 +4,8 @@ import { isSameDay } from "@internationalized/date";
4
4
  import { CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading, CalendarStateContext, RangeCalendarStateContext } from "react-aria-components";
5
5
  import { Button } from "./button.js";
6
6
  import { controlFontWeight, controlOpacity, transition } from "./control/control.js";
7
- import { selectionLevel } from "./control/item.js";
8
- import { colorLevel, colorOpacity, readableColor, resolveColor } from "./foundation/color.js";
7
+ import { selectionColor } from "./control/item.js";
8
+ import { colorOpacity, readableColor, resolveColor } from "./foundation/color.js";
9
9
  import { useDirection } from "./foundation/direction.js";
10
10
  import { dimmedClass, surfacePaint } from "./surface/surface.js";
11
11
  import { ChevronLeft, ChevronRight } from "lucide-react";
@@ -51,7 +51,7 @@ function daySelection(state, calendar) {
51
51
  }
52
52
  /**
53
53
  * A chosen day is the calendar's one decision, so it takes the full color;
54
- * the days inside a range take the selection level; every other day only veils.
54
+ * the days inside a range take the selection color; every other day only veils.
55
55
  */
56
56
  function cellStyle(metrics, color, state, selection, range) {
57
57
  const { visual } = metrics;
@@ -61,7 +61,7 @@ function cellStyle(metrics, color, state, selection, range) {
61
61
  const fill = endpoint
62
62
  ? base
63
63
  : selection.selected
64
- ? colorLevel(base, selectionLevel, colors)
64
+ ? selectionColor(base, colors)
65
65
  : "transparent";
66
66
  const paint = surfacePaint(visual, fill, "flat", "none", false, {
67
67
  hovered: state.isHovered || state.isFocused,
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { type ButtonProps } from "./button.js";
2
+ import { type ButtonActionProps } from "./button.js";
3
3
  import { type OpenStateProps } from "./control/open-state.js";
4
4
  import { type PopoverContentProps } from "./popover.js";
5
5
  export type ColorPickerRootProps = OpenStateProps & Readonly<{
@@ -15,10 +15,10 @@ export type ColorPickerRootProps = OpenStateProps & Readonly<{
15
15
  * components edit it, which all follow the picker's color.
16
16
  */
17
17
  declare function ColorPickerRoot({ value, defaultValue, onChange, children, ...state }: ColorPickerRootProps): import("react").JSX.Element;
18
- export type ColorPickerTriggerProps = ButtonProps;
18
+ export type ColorPickerTriggerProps = ButtonActionProps;
19
19
  export type ColorPickerContentProps = PopoverContentProps;
20
20
  export declare const ColorPicker: typeof ColorPickerRoot & {
21
- Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
21
+ Trigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
22
22
  Content: import("react").ForwardRefExoticComponent<PopoverContentProps & import("react").RefAttributes<HTMLElement>>;
23
23
  };
24
24
  export {};
@@ -1,13 +1,16 @@
1
1
  import type { CSSProperties } from "react";
2
+ import type { AppearanceColors } from "../foundation/appearance.js";
2
3
  import { type Color } from "../foundation/color.js";
3
4
  import type { Visual } from "../foundation/visual.js";
4
5
  import { type ControlMetrics } from "./control.js";
5
6
  import type { SurfaceRenderProps } from "./surface-render.js";
6
7
  /**
7
- * The level that marks what is selected, in every collection and inside a
8
- * calendar range.
8
+ * What marks a selection, in every collection and inside a calendar range:
9
+ * the subtle level of its color under the same veil as hover. The veil keeps
10
+ * a light color apart from its canvas, so a selection never shows less
11
+ * clearly than hovering does.
9
12
  */
10
- export declare const selectionLevel = "subtle";
13
+ export declare function selectionColor(base: string, colors: AppearanceColors): string;
11
14
  export type ItemState = Readonly<{
12
15
  selected: boolean;
13
16
  hovered: boolean;
@@ -17,7 +20,7 @@ export type ItemState = Readonly<{
17
20
  }>;
18
21
  /**
19
22
  * One entry of a collection. At rest it has no paint of its own; hover and
20
- * press veil it, and selection lays the selection level of the collection
23
+ * press veil it, and selection lays the selection color of the collection
21
24
  * color beneath it. Every collection uses this same treatment.
22
25
  */
23
26
  export declare function itemSurface(visual: Visual, color: Color, state: ItemState, radius: CSSProperties["borderRadius"]): SurfaceRenderProps;
@@ -1,22 +1,26 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { colorLevel, resolveColor } from "../foundation/color.js";
3
- import { surfacePaint } from "../surface/surface.js";
2
+ import { colorLevel, mixColor, resolveColor } from "../foundation/color.js";
3
+ import { clearVeil, interactionShift, surfacePaint } from "../surface/surface.js";
4
4
  import { transition } from "./control.js";
5
5
  import { Check } from "lucide-react";
6
6
  import { iconProps } from "./icon.js";
7
7
  /**
8
- * The level that marks what is selected, in every collection and inside a
9
- * calendar range.
8
+ * What marks a selection, in every collection and inside a calendar range:
9
+ * the subtle level of its color under the same veil as hover. The veil keeps
10
+ * a light color apart from its canvas, so a selection never shows less
11
+ * clearly than hovering does.
10
12
  */
11
- export const selectionLevel = "subtle";
13
+ export function selectionColor(base, colors) {
14
+ return mixColor(colorLevel(base, "subtle", colors), colors.foreground, interactionShift.hovered * clearVeil);
15
+ }
12
16
  /**
13
17
  * One entry of a collection. At rest it has no paint of its own; hover and
14
- * press veil it, and selection lays the selection level of the collection
18
+ * press veil it, and selection lays the selection color of the collection
15
19
  * color beneath it. Every collection uses this same treatment.
16
20
  */
17
21
  export function itemSurface(visual, color, state, radius) {
18
22
  return {
19
- color: state.selected ? colorLevel(resolveColor(color, visual.colors), selectionLevel, visual.colors) : "transparent",
23
+ color: state.selected ? selectionColor(resolveColor(color, visual.colors), visual.colors) : "transparent",
20
24
  depth: "flat",
21
25
  material: "none",
22
26
  radius,
@@ -25,7 +29,7 @@ export function itemSurface(visual, color, state, radius) {
25
29
  }
26
30
  /** The same treatment as a plain paint, for elements that cannot host a Surface. */
27
31
  export function itemPaint(visual, color, state) {
28
- const paint = surfacePaint(visual, state.selected ? colorLevel(resolveColor(color, visual.colors), selectionLevel, visual.colors) : "transparent", "flat", "none", false, itemInteraction(state));
32
+ const paint = surfacePaint(visual, state.selected ? selectionColor(resolveColor(color, visual.colors), visual.colors) : "transparent", "flat", "none", false, itemInteraction(state));
29
33
  return { background: paint.fill, color: paint.text };
30
34
  }
31
35
  function itemInteraction(state) {
package/dist/dialog.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { ComponentProps, CSSProperties, HTMLAttributes, ReactNode } from "react";
2
2
  import type { DialogProps as AriaDialogProps, HeadingProps as AriaHeadingProps, ModalOverlayProps as AriaModalOverlayProps, TextProps as AriaTextProps } from "react-aria-components";
3
- import { type ButtonProps } from "./button.js";
3
+ import { type ButtonActionProps } from "./button.js";
4
4
  import { type AriaOverlayInternals, type OverlayRootProps } from "./control/open-state.js";
5
5
  import { type Direction } from "./foundation/direction.js";
6
6
  import { type SurfaceOwnProps } from "./surface/surface.js";
7
7
  export type DialogRootProps = OverlayRootProps;
8
8
  export declare function DialogRoot({ children, ...state }: DialogRootProps): import("react").JSX.Element;
9
- export type DialogTriggerProps = ButtonProps;
10
- export declare const DialogTrigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
9
+ export type DialogTriggerProps = ButtonActionProps;
10
+ export declare const DialogTrigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
11
11
  /**
12
12
  * The layer behind a Dialog. Its open state belongs to the Dialog root; the
13
13
  * backdrop only decides how the Dialog may be dismissed.
@@ -45,11 +45,11 @@ export type DialogBodyProps = HTMLAttributes<HTMLDivElement>;
45
45
  export declare const DialogBody: import("react").ForwardRefExoticComponent<DialogBodyProps & import("react").RefAttributes<HTMLDivElement>>;
46
46
  export type DialogFooterProps = HTMLAttributes<HTMLDivElement>;
47
47
  export declare const DialogFooter: import("react").ForwardRefExoticComponent<DialogFooterProps & import("react").RefAttributes<HTMLDivElement>>;
48
- export type DialogCloseProps = ButtonProps;
49
- export declare const DialogClose: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
48
+ export type DialogCloseProps = ButtonActionProps;
49
+ export declare const DialogClose: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
50
50
  /** A modal dialog whose behavioral and structural roles remain independently composable. */
51
51
  export declare const Dialog: typeof DialogRoot & {
52
- Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
52
+ Trigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
53
53
  Backdrop: import("react").ForwardRefExoticComponent<DialogBackdropProps & import("react").RefAttributes<HTMLDivElement>>;
54
54
  Content: import("react").ForwardRefExoticComponent<DialogContentProps & import("react").RefAttributes<HTMLElement>>;
55
55
  Header: import("react").ForwardRefExoticComponent<DialogHeaderProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -57,6 +57,6 @@ export declare const Dialog: typeof DialogRoot & {
57
57
  Description: import("react").ForwardRefExoticComponent<AriaTextProps & import("react").RefAttributes<HTMLElement>>;
58
58
  Body: import("react").ForwardRefExoticComponent<DialogBodyProps & import("react").RefAttributes<HTMLDivElement>>;
59
59
  Footer: import("react").ForwardRefExoticComponent<DialogFooterProps & import("react").RefAttributes<HTMLDivElement>>;
60
- Close: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
60
+ Close: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
61
61
  };
62
62
  export type DialogProps = ComponentProps<typeof DialogRoot>;
@@ -1,14 +1,14 @@
1
1
  import type { ComponentProps } from "react";
2
2
  import type { MenuTriggerProps as AriaMenuTriggerProps } from "react-aria-components";
3
- import { type ButtonProps } from "./button.js";
3
+ import { type ButtonActionProps } from "./button.js";
4
4
  import { type OverlayRootProps } from "./control/open-state.js";
5
5
  export type DropdownMenuRootProps = OverlayRootProps & Readonly<Pick<AriaMenuTriggerProps, "trigger">>;
6
6
  export declare function DropdownMenuRoot({ children, trigger, ...state }: DropdownMenuRootProps): import("react").JSX.Element;
7
- export type DropdownMenuTriggerProps = ButtonProps;
8
- export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
7
+ export type DropdownMenuTriggerProps = ButtonActionProps;
8
+ export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
9
9
  /** A button-triggered overlay. Its Content owns positioning; Menu owns commands and selection. */
10
10
  export declare const DropdownMenu: typeof DropdownMenuRoot & {
11
- Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
11
+ Trigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
12
12
  Content: import("react").ForwardRefExoticComponent<import("./popover.js").PopoverContentProps & Readonly<{
13
13
  nonModal?: boolean;
14
14
  }> & import("react").RefAttributes<HTMLElement>>;
@@ -7,7 +7,13 @@ export interface UIProviderProps {
7
7
  readonly children: ReactNode;
8
8
  /** Establishes the concrete direction inherited by the provider subtree. */
9
9
  readonly direction?: Direction;
10
+ /**
11
+ * Follows a link inside the app, such as a Button `href`, with the app's own
12
+ * router instead of a page load. Links to other sites, links with a `target`,
13
+ * and modified clicks are left to the browser.
14
+ */
15
+ readonly navigate?: (href: string) => void;
10
16
  readonly preferences?: PreferencesUpdate;
11
17
  }
12
18
  /** Establishes only the explicitly supplied UI values for a React subtree. */
13
- export declare function UIProvider({ appearance, children, direction, preferences }: UIProviderProps): import("react").JSX.Element;
19
+ export declare function UIProvider({ appearance, children, direction, navigate, preferences }: UIProviderProps): import("react").JSX.Element;
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { LucideProvider } from "lucide-react";
3
3
  import { useMemo } from "react";
4
+ import { RouterProvider } from "react-aria-components";
4
5
  import { mergeAppearance } from "./appearance.js";
5
6
  import { DirectionContext, fixedDirectionSource } from "./direction.js";
6
7
  import { AppearanceContext, fixedPreferencesSource, PreferencesContext, useAppearance, usePreferences } from "./visual.js";
7
8
  const directionBoundaryStyle = { display: "contents" };
8
9
  /** Establishes only the explicitly supplied UI values for a React subtree. */
9
- export function UIProvider({ appearance, children, direction, preferences }) {
10
+ export function UIProvider({ appearance, children, direction, navigate, preferences }) {
10
11
  let subtree = children;
11
12
  const directionSource = useMemo(() => direction === undefined ? undefined : fixedDirectionSource(direction), [direction]);
12
13
  if (preferences !== undefined)
@@ -16,6 +17,8 @@ export function UIProvider({ appearance, children, direction, preferences }) {
16
17
  if (directionSource !== undefined) {
17
18
  subtree = _jsx(DirectionContext.Provider, { value: directionSource, children: _jsx("div", { dir: direction, style: directionBoundaryStyle, children: subtree }) });
18
19
  }
20
+ if (navigate !== undefined)
21
+ subtree = _jsx(RouterProvider, { navigate: navigate, children: subtree });
19
22
  // Icons are not a supplied value: every provider gives its subtree the same
20
23
  // icon treatment. An icon follows its text size and color, with a constant
21
24
  // 1.5px stroke; a `size` or `strokeWidth` on the icon still wins.
package/dist/main.d.ts CHANGED
@@ -16,7 +16,7 @@ export { resolveColor, useColor, useContrastingColor, type Color, type ColorLeve
16
16
  export { Panel, type PanelProps, type PanelHeaderProps, type PanelContentProps } from "./panel.js";
17
17
  export { Window, type WindowProps, type WindowContentProps, type WindowHeaderProps, type WindowIdentityProps, type WindowCenterProps, type WindowActionsProps, type WindowActionProps, type WindowControlProps, type WindowMaximizeProps, type WindowCloseProps, type WindowMoveCaptureProps } from "./window.js";
18
18
  export { default as useWindowMoveHandle, type WindowMoveHandle } from "./use-window-move-handle.js";
19
- export { Button, type ButtonColor, type ButtonProps } from "./button.js";
19
+ export { Button, type ButtonActionProps, type ButtonColor, type ButtonLinkProps, type ButtonProps } from "./button.js";
20
20
  export { Input, type InputProps } from "./input.js";
21
21
  export { Textarea, type TextareaProps } from "./textarea.js";
22
22
  export { DateField, type DateFieldProps } from "./date-field.js";
package/dist/panel.js CHANGED
@@ -5,7 +5,7 @@ import { headerHeight } from "./foundation/layout.js";
5
5
  import { resolveRadius } from "./foundation/radius.js";
6
6
  import { scale } from "./foundation/scale.js";
7
7
  import { useVisual } from "./foundation/visual.js";
8
- import { paintClass, paintClassesAvailable } from "./surface/paint-class.js";
8
+ import { paintClass, usePaintClasses } from "./surface/paint-class.js";
9
9
  import { Surface } from "./surface/surface.js";
10
10
  const PanelContext = createContext({ color: undefined, hasHeader: false, radius: undefined });
11
11
  export const PanelHeader = forwardRef(function PanelHeader({ style, ...properties }, ref) {
@@ -44,7 +44,7 @@ export const PanelContent = forwardRef(function PanelContent({ className, style,
44
44
  const { color, hasHeader, radius } = useContext(PanelContext);
45
45
  // A zero-specificity class keeps the padding a default: any consumer class
46
46
  // or style still sets its own.
47
- const padded = paintClassesAvailable ? paintClass(`padding: ${padding}px`) : undefined;
47
+ const padded = usePaintClasses() ? paintClass(`padding: ${padding}px`) : undefined;
48
48
  return _jsx(Surface, { color: color, material: "extended", radius: radius, ...properties, ref: ref, className: [padded, className].filter(Boolean).join(" ") || undefined, style: {
49
49
  ...(padded === undefined ? { padding } : {}),
50
50
  minWidth: 0,
package/dist/popover.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { ComponentProps, CSSProperties, ReactNode } from "react";
2
2
  import type { DialogProps as AriaDialogProps, HeadingProps as AriaHeadingProps, PopoverProps as AriaPopoverProps } from "react-aria-components";
3
- import { type ButtonProps } from "./button.js";
3
+ import { type ButtonActionProps } from "./button.js";
4
4
  import { type AriaOverlayInternals, type OverlayRootProps } from "./control/open-state.js";
5
5
  import { type Direction } from "./foundation/direction.js";
6
6
  import { type SurfaceOwnProps } from "./surface/surface.js";
7
7
  export type PopoverRootProps = OverlayRootProps;
8
8
  export declare function PopoverRoot({ children, ...state }: PopoverRootProps): import("react").JSX.Element;
9
- export type PopoverTriggerProps = ButtonProps;
10
- export declare const PopoverTrigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
9
+ export type PopoverTriggerProps = ButtonActionProps;
10
+ export declare const PopoverTrigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
11
11
  /**
12
12
  * The floating Surface of an overlay. Its open state belongs to the family
13
13
  * root, so this part takes placement, its own Surface, and ordinary element
@@ -26,18 +26,18 @@ export type PopoverDialogProps = AriaDialogProps;
26
26
  export declare const PopoverDialog: import("react").ForwardRefExoticComponent<AriaDialogProps & import("react").RefAttributes<HTMLElement>>;
27
27
  export type PopoverTitleProps = AriaHeadingProps;
28
28
  export declare const PopoverTitle: import("react").ForwardRefExoticComponent<AriaHeadingProps & import("react").RefAttributes<HTMLHeadingElement>>;
29
- export type PopoverCloseProps = ButtonProps;
30
- export declare const PopoverClose: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
29
+ export type PopoverCloseProps = ButtonActionProps;
30
+ export declare const PopoverClose: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
31
31
  /** Positioned content that holds a Menu; it keeps the Appearance radius like every shell. */
32
32
  export declare const MenuContent: import("react").ForwardRefExoticComponent<PopoverContentProps & Readonly<{
33
33
  nonModal?: boolean;
34
34
  }> & import("react").RefAttributes<HTMLElement>>;
35
35
  /** An anchored non-modal overlay with explicit trigger, surface, and dialog roles. */
36
36
  export declare const Popover: typeof PopoverRoot & {
37
- Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
37
+ Trigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
38
38
  Content: import("react").ForwardRefExoticComponent<PopoverContentProps & import("react").RefAttributes<HTMLElement>>;
39
39
  Dialog: import("react").ForwardRefExoticComponent<AriaDialogProps & import("react").RefAttributes<HTMLElement>>;
40
40
  Title: import("react").ForwardRefExoticComponent<AriaHeadingProps & import("react").RefAttributes<HTMLHeadingElement>>;
41
- Close: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
41
+ Close: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
42
42
  };
43
43
  export type PopoverProps = ComponentProps<typeof PopoverRoot>;
@@ -4,3 +4,10 @@ export declare function paintClass(declarations: string): string;
4
4
  export declare function flushPaintRules(): void;
5
5
  /** Whether generated rules can reach a document in this environment. */
6
6
  export declare const paintClassesAvailable: boolean;
7
+ /**
8
+ * Whether this render may use paint classes. Server rendering sends paints
9
+ * inline, so hydration renders inline too and matches the server HTML; React
10
+ * then renders again with classes. Without this, the server's inline paint
11
+ * would stay on the element, because hydration does not patch attributes.
12
+ */
13
+ export declare function usePaintClasses(): boolean;
@@ -1,3 +1,4 @@
1
+ import { useSyncExternalStore } from "react";
1
2
  /**
2
3
  * Every distinct Surface paint becomes one shared class. Surfaces repeat the
3
4
  * same few paints across a whole interface, so changing Theme or hovering a
@@ -34,3 +35,13 @@ export function flushPaintRules() {
34
35
  }
35
36
  /** Whether generated rules can reach a document in this environment. */
36
37
  export const paintClassesAvailable = typeof document !== "undefined";
38
+ const unchanging = () => () => undefined;
39
+ /**
40
+ * Whether this render may use paint classes. Server rendering sends paints
41
+ * inline, so hydration renders inline too and matches the server HTML; React
42
+ * then renders again with classes. Without this, the server's inline paint
43
+ * would stay on the element, because hydration does not patch attributes.
44
+ */
45
+ export function usePaintClasses() {
46
+ return useSyncExternalStore(unchanging, () => paintClassesAvailable, () => false);
47
+ }
@@ -62,6 +62,13 @@ export declare const Surface: SurfaceComponent;
62
62
  * calendar's days, are disabled with it and do not fade a second time.
63
63
  */
64
64
  export declare function dimmedClass(disabled: boolean, className?: string): string | undefined;
65
+ /** How far hover and press move a paint toward the text color. */
66
+ export declare const interactionShift: Readonly<{
67
+ hovered: 0.05;
68
+ pressed: 0.1;
69
+ }>;
70
+ /** A clear Surface has no paint to move, so it shows the same shift as a slightly stronger veil. */
71
+ export declare const clearVeil = 1.3;
65
72
  /** Resolves every visual value of one Surface. Pure over its inputs. */
66
73
  export declare function surfacePaint(visual: Visual, color: Color, depth: SurfaceDepth, options: MaterialMode | MaterialOptions | undefined, shadowOptions: ShadowOverrides["shadow"], interaction: SurfaceInteraction | undefined): {
67
74
  fill: string;
@@ -71,3 +78,14 @@ export declare function surfacePaint(visual: Visual, color: Color, depth: Surfac
71
78
  distortion: number;
72
79
  variables: CSSProperties;
73
80
  };
81
+ /**
82
+ * How far the inward spill reaches: at most a fixed depth, and less on short
83
+ * Surfaces. At a fixed depth a small button was lit through nearly half its
84
+ * height and read as swollen, while a large panel barely changes.
85
+ *
86
+ * The limit is the height of the spill's background layer, not the gradient's
87
+ * size: browsers reject a size that mixes pixels and percentages inside
88
+ * radial-gradient(), and one rejected layer drops the whole background,
89
+ * grain included.
90
+ */
91
+ export declare const spillDepth = "min(6px, 12%)";
@@ -4,7 +4,7 @@ import { canvasDarkness, colorOpacity, contrast, mixColor, readableColor, recess
4
4
  import { resolveRadius } from "../foundation/radius.js";
5
5
  import { useVisual } from "../foundation/visual.js";
6
6
  import { grainImage, grainSeed, grainSize } from "./grain.js";
7
- import { flushPaintRules, paintClass, paintClassesAvailable } from "./paint-class.js";
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";
10
10
  /**
@@ -30,7 +30,7 @@ export const SurfaceView = forwardRef(function Surface({ as: Element = "div", co
30
30
  "outline-offset: 1px",
31
31
  `border-radius: ${typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius}`
32
32
  ].join("; ");
33
- const painted = paintClassesAvailable ? paintClass(declarations) : undefined;
33
+ const painted = usePaintClasses() ? paintClass(declarations) : undefined;
34
34
  useInsertionEffect(flushPaintRules);
35
35
  return createElement(Element, {
36
36
  ...properties,
@@ -63,12 +63,16 @@ const disabledOpacity = 0.46;
63
63
  const dimmed = "phreshos-dimmed";
64
64
  const surfaceClass = "phreshos-surface";
65
65
  const materialLevel = Object.freeze({ none: 0, basic: 1, extended: 2, full: 3 });
66
+ /** How far hover and press move a paint toward the text color. */
67
+ export const interactionShift = Object.freeze({ hovered: 0.05, pressed: 0.1 });
68
+ /** A clear Surface has no paint to move, so it shows the same shift as a slightly stronger veil. */
69
+ export const clearVeil = 1.3;
66
70
  /** Resolves every visual value of one Surface. Pure over its inputs. */
67
71
  export function surfacePaint(visual, color, depth, options, shadowOptions, interaction) {
68
72
  const { colors } = visual;
69
73
  const base = resolveColor(color, colors);
70
74
  const active = interaction !== undefined && !interaction.disabled;
71
- const shift = active ? interaction.pressed ? 0.1 : interaction.hovered ? 0.05 : 0 : 0;
75
+ const shift = active ? interaction.pressed ? interactionShift.pressed : interaction.hovered ? interactionShift.hovered : 0 : 0;
72
76
  // Recessed paint sinks slightly below its surroundings; interaction then
73
77
  // moves the same base toward the content color so every state stays related.
74
78
  const darkness = canvasDarkness(colors);
@@ -79,7 +83,7 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
79
83
  // color of whatever it sits on and only reveals interaction as a veil.
80
84
  const clear = base === "transparent";
81
85
  const fill = clear
82
- ? shift > 0 ? colorOpacity(colors.foreground, shift * 1.3) : "transparent"
86
+ ? shift > 0 ? colorOpacity(colors.foreground, shift * clearVeil) : "transparent"
83
87
  : mixColor(rest, colors.foreground, shift);
84
88
  const mode = clear ? "none" : typeof options === "object" ? "full" : options ?? "basic";
85
89
  const level = materialLevel[mode];
@@ -107,8 +111,10 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
107
111
  const rim = edged
108
112
  ? `linear-gradient(to right, transparent, ${colorOpacity(light, lit.side)} 18%, ${colorOpacity(light, lit.edge)} 50%, ${colorOpacity(light, lit.side)} 82%, transparent)`
109
113
  : "none";
114
+ // The spill fills its own background layer, whose height the stylesheet
115
+ // limits (see spillDepth).
110
116
  const spill = (edge) => edged && lit.spill > 0
111
- ? `radial-gradient(50% 6px at 50% ${edge === "top" ? "0" : "100%"}, ${colorOpacity(light, lit.spill)}, transparent)`
117
+ ? `radial-gradient(50% 100% at 50% ${edge === "top" ? "0" : "100%"}, ${colorOpacity(light, lit.spill)}, transparent)`
112
118
  : "none";
113
119
  // A recessed Surface holds a value, so focus and validity also claim its hairline.
114
120
  const claimed = interaction?.invalid || (depth === "recessed" && interaction?.focusVisible);
@@ -141,6 +147,17 @@ export function surfacePaint(visual, color, depth, options, shadowOptions, inter
141
147
  }
142
148
  };
143
149
  }
150
+ /**
151
+ * How far the inward spill reaches: at most a fixed depth, and less on short
152
+ * Surfaces. At a fixed depth a small button was lit through nearly half its
153
+ * height and read as swollen, while a large panel barely changes.
154
+ *
155
+ * The limit is the height of the spill's background layer, not the gradient's
156
+ * size: browsers reject a size that mixes pixels and percentages inside
157
+ * radial-gradient(), and one rejected layer drops the whole background,
158
+ * grain included.
159
+ */
160
+ export const spillDepth = "min(6px, 12%)";
144
161
  /** How much light each part of the rim receives, interpolated along the canvas darkness. */
145
162
  function edgeLight(depth, darkness) {
146
163
  const { light, dark } = edgeLightEnds[depth];
@@ -210,7 +227,9 @@ const stylesheet = `
210
227
  z-index: -1;
211
228
  background-color: var(--phreshos-surface-paint);
212
229
  background-image: var(--phreshos-surface-spill-top), var(--phreshos-surface-spill-bottom), var(--phreshos-surface-grain);
213
- background-size: auto, auto, ${grainSize}px ${grainSize}px;
230
+ background-size: 100% ${spillDepth}, 100% ${spillDepth}, ${grainSize}px ${grainSize}px;
231
+ background-position: center top, center bottom, 0 0;
232
+ background-repeat: no-repeat, no-repeat, repeat;
214
233
  -webkit-backdrop-filter: var(--phreshos-surface-frost);
215
234
  backdrop-filter: var(--phreshos-surface-frost);
216
235
  transition: background-color var(--phreshos-surface-duration) var(--phreshos-surface-easing);
package/dist/tooltip.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ComponentProps, CSSProperties, ReactNode } from "react";
2
2
  import type { TooltipProps as AriaTooltipProps, TooltipTriggerComponentProps as AriaTooltipTriggerProps } from "react-aria-components";
3
- import { type ButtonProps } from "./button.js";
3
+ import { type ButtonActionProps } from "./button.js";
4
4
  import { type AriaOverlayInternals, type OverlayRootProps } from "./control/open-state.js";
5
5
  import { type Direction } from "./foundation/direction.js";
6
6
  import { type SurfaceOwnProps } from "./surface/surface.js";
@@ -11,8 +11,8 @@ export type TooltipRootProps = OverlayRootProps & Readonly<Pick<AriaTooltipTrigg
11
11
  closeOnPress?: boolean;
12
12
  }>;
13
13
  export declare function TooltipRoot({ children, delay, closeDelay, trigger, disabled, closeOnPress, ...state }: TooltipRootProps): import("react").JSX.Element;
14
- export type TooltipTriggerProps = ButtonProps;
15
- export declare const TooltipTrigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
14
+ export type TooltipTriggerProps = ButtonActionProps;
15
+ export declare const TooltipTrigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
16
16
  /** The floating Surface of a Tooltip: its placement, its own Surface, and element attributes. */
17
17
  export interface TooltipContentProps extends Omit<AriaTooltipProps, AriaOverlayInternals | "children" | "className" | "color" | "style" | "dir">, SurfaceOwnProps {
18
18
  readonly children?: ReactNode;
@@ -23,7 +23,7 @@ export interface TooltipContentProps extends Omit<AriaTooltipProps, AriaOverlayI
23
23
  export declare const TooltipContent: import("react").ForwardRefExoticComponent<TooltipContentProps & import("react").RefAttributes<HTMLDivElement>>;
24
24
  /** A description shown from the focus and hover state of its trigger. */
25
25
  export declare const Tooltip: typeof TooltipRoot & {
26
- Trigger: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
26
+ Trigger: import("react").ForwardRefExoticComponent<ButtonActionProps & import("react").RefAttributes<HTMLButtonElement>>;
27
27
  Content: import("react").ForwardRefExoticComponent<TooltipContentProps & import("react").RefAttributes<HTMLDivElement>>;
28
28
  };
29
29
  export type TooltipProps = ComponentProps<typeof TooltipRoot>;
package/dist/window.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
2
  import type { BeginWindowMoveGesture } from "@phreshos/core";
3
- import { type ButtonProps } from "./button.js";
3
+ import { type ButtonActionProps } from "./button.js";
4
4
  import { type Color } from "./foundation/color.js";
5
5
  import { type SurfaceProps } from "./surface/surface.js";
6
6
  /** The material shell that establishes the complete header-and-content layout. */
@@ -27,7 +27,7 @@ export interface WindowIdentityProps extends Omit<HTMLAttributes<HTMLDivElement>
27
27
  }
28
28
  export type WindowCenterProps = HTMLAttributes<HTMLDivElement>;
29
29
  export type WindowActionsProps = HTMLAttributes<HTMLDivElement>;
30
- export type WindowActionProps = Omit<ButtonProps, "size">;
30
+ export type WindowActionProps = Omit<ButtonActionProps, "size">;
31
31
  export type WindowControlProps = Omit<WindowActionProps, "children">;
32
32
  export interface WindowMaximizeProps extends WindowControlProps {
33
33
  readonly maximized?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/react-ui",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "React components for coherent PhreshOS Program interfaces.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -57,7 +57,7 @@
57
57
  "dependencies": {
58
58
  "@base-ui/react": "^1.7.0",
59
59
  "@internationalized/date": "^3.12.3",
60
- "@phreshos/core": "^0.1.61",
60
+ "@phreshos/core": "^0.1.62",
61
61
  "colorjs.io": "^0.7.1",
62
62
  "lucide-react": "^1.48.0",
63
63
  "react-aria": "^3.51.0",