@tanishqkancharla/maui 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,18 +11,19 @@ Maui is open source under the MIT License and published on npm as `@tanishqkanch
11
11
  Halo keeps the short `maui` import alias:
12
12
 
13
13
  ```sh
14
- pnpm add maui@npm:@tanishqkancharla/maui@0.0.9
14
+ pnpm add maui@npm:@tanishqkancharla/maui@0.0.9 motion@^13.2.0
15
15
  ```
16
16
 
17
17
  ```json
18
18
  {
19
19
  "dependencies": {
20
- "maui": "npm:@tanishqkancharla/maui@0.0.9"
20
+ "maui": "npm:@tanishqkancharla/maui@0.0.9",
21
+ "motion": "^13.2.0"
21
22
  }
22
23
  }
23
24
  ```
24
25
 
25
- The published tarball contains compiled `dist/` JavaScript and declarations. Installs do not run a Maui build.
26
+ Motion is a required peer dependency used by Maui components with animated presence and transitions. The published tarball contains compiled `dist/` JavaScript and declarations. Installs do not run a Maui build.
26
27
 
27
28
  Wrap the app in `MauiProvider`, then import tokens and components:
28
29
 
@@ -8,9 +8,10 @@ const codeClass = style(monospace, radius.sm, spacing.padding({ x: 2, y: 1 }), {
8
8
  backgroundColor: colors.gray[3],
9
9
  color: colors.gray[12],
10
10
  display: "inline",
11
- boxDecorationBreak: "clone",
11
+ boxDecorationBreak: "slice",
12
12
  fontSize: "0.875em",
13
- whiteSpace: "nowrap",
13
+ whiteSpace: "normal",
14
+ overflowWrap: "anywhere",
14
15
  });
15
16
  const kbdClass = style(text({ size: "xs", fontWeight: 400, color: "lowContrast" }), radius.sm, {
16
17
  backgroundColor: colors.gray[3],
@@ -1,7 +1,24 @@
1
- import React from "react";
2
- type DialogProps = {
3
- children?: React.ReactNode;
4
- onClickOutside?: () => void;
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from "react";
2
+ import { Heading } from "react-aria-components";
3
+ export type DialogSize = "sm" | "md" | "lg";
4
+ export type DialogProps = {
5
+ children: ReactNode;
6
+ isOpen?: boolean;
7
+ defaultOpen?: boolean;
8
+ onOpenChange?: (isOpen: boolean) => void;
9
+ /** Enables outside-click dismissal. Escape remains enabled by default. */
10
+ isDismissable?: boolean;
11
+ isKeyboardDismissDisabled?: boolean;
12
+ size?: DialogSize;
13
+ role?: "dialog" | "alertdialog";
14
+ "aria-label"?: string;
15
+ "aria-labelledby"?: string;
16
+ className?: string;
5
17
  };
6
- export declare function Dialog(props: DialogProps): React.JSX.Element;
7
- export {};
18
+ export declare function Dialog({ children, isOpen: isOpenProp, defaultOpen, onOpenChange, isDismissable, isKeyboardDismissDisabled, size, role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, className, }: DialogProps): import("react").JSX.Element;
19
+ export type DialogTitleProps = Omit<ComponentPropsWithoutRef<typeof Heading>, "slot">;
20
+ export declare function DialogTitle({ className, ...props }: DialogTitleProps): import("react").JSX.Element;
21
+ export type DialogBodyProps = ComponentPropsWithoutRef<"div">;
22
+ export declare function DialogBody({ className, ...props }: DialogBodyProps): import("react").JSX.Element;
23
+ export type DialogActionsProps = ComponentPropsWithoutRef<"div">;
24
+ export declare function DialogActions({ className, ...props }: DialogActionsProps): import("react").JSX.Element;
@@ -1,35 +1,120 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { animated, useSpring } from "@react-spring/web";
2
+ import { useCallback, useState, } from "react";
3
+ import { AnimatePresence, motion, useReducedMotion, } from "motion/react";
4
+ import { Dialog as AriaDialog, Heading, Modal, ModalOverlay, } from "react-aria-components";
3
5
  import { style, useStyles } from "purse-styles";
4
- import { FocusScope } from "../hooks/useFocus";
5
6
  import { background } from "../tokens/background";
6
7
  import { border } from "../tokens/borders";
7
- import { Overlay } from "./Overlay";
8
- const dialogStyle = style(background.element, border([], "border"), {
9
- flex: 1,
10
- borderRadius: "4px",
11
- margin: "15vh 20vw",
12
- padding: "32px",
8
+ import { overlayMotion } from "../tokens/motion";
9
+ import { radius } from "../tokens/radius";
10
+ import { spacing } from "../tokens/spacing";
11
+ import { text } from "../tokens/text";
12
+ import { cls } from "../utils/cls";
13
+ const MotionModalOverlay = motion.create(ModalOverlay);
14
+ const MotionModal = motion.create(Modal);
15
+ const dialogEnterTransition = {
16
+ type: "spring",
17
+ visualDuration: overlayMotion.dialogEnter.durationMs / 1000,
18
+ bounce: overlayMotion.dialogEnter.bounce,
19
+ };
20
+ const dialogExitTransition = {
21
+ duration: overlayMotion.dialogExit.durationMs / 1000,
22
+ ease: "easeIn",
23
+ };
24
+ const backdropTransition = {
25
+ duration: overlayMotion.backdrop.durationMs / 1000,
26
+ ease: "easeInOut",
27
+ };
28
+ const reducedMotionTransition = {
29
+ duration: overlayMotion.reduced.durationMs / 1000,
30
+ ease: "easeInOut",
31
+ };
32
+ const overlayClass = style({
33
+ position: "fixed",
34
+ inset: 0,
35
+ zIndex: 1200,
36
+ display: "flex",
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ boxSizing: "border-box",
40
+ padding: spacing.value(8),
41
+ backgroundColor: "rgb(0 0 0 / 0.1)",
42
+ overflow: "auto",
43
+ overscrollBehavior: "contain",
44
+ });
45
+ const surfaceClass = style(background.element, border([], "outline"), radius.lg, {
46
+ width: "min(440px, calc(100vw - 32px))",
47
+ maxWidth: "100%",
48
+ maxHeight: "calc(100dvh - 32px)",
49
+ display: "flex",
50
+ flexDirection: "column",
51
+ boxSizing: "border-box",
52
+ padding: spacing.value(12),
53
+ overflow: "hidden",
54
+ outline: "none",
55
+ transformOrigin: "center",
56
+ willChange: "opacity, transform",
13
57
  });
14
- const springConfigs = {
15
- big: {
16
- tension: 700,
17
- friction: 50,
18
- // clamp: true,
19
- },
58
+ const surfaceSizeClasses = {
59
+ sm: style({ width: "min(440px, calc(100vw - 32px))" }),
60
+ md: style({ width: "min(640px, calc(100vw - 32px))" }),
61
+ lg: style({ width: "min(960px, calc(100vw - 32px))" }),
20
62
  };
21
- export function Dialog(props) {
22
- const className = useStyles(dialogStyle);
23
- const dialogSprings = useSpring({
24
- config: springConfigs.big,
25
- from: {
26
- transform: "scale(0.95)",
27
- opacity: 0,
28
- },
29
- to: { transform: "scale(1)", opacity: 1 },
30
- });
31
- return (_jsx(Overlay, { onClickOutside: props.onClickOutside, children: _jsx(FocusScope, { containBehavior: "lock", autoFocus: true, children: _jsx(animated.div, { style: {
32
- ...dialogSprings,
33
- }, className: className, children: props.children }) }) }));
63
+ const dialogClass = style({
64
+ minWidth: 0,
65
+ minHeight: 0,
66
+ display: "flex",
67
+ flexDirection: "column",
68
+ gap: spacing.value(8),
69
+ outline: "none",
70
+ });
71
+ const titleClass = style(text({ size: "lg", fontWeight: 600 }), {
72
+ margin: 0,
73
+ });
74
+ const bodyClass = style({
75
+ minWidth: 0,
76
+ minHeight: 0,
77
+ overflowY: "auto",
78
+ });
79
+ const actionsClass = style({
80
+ display: "flex",
81
+ alignItems: "center",
82
+ justifyContent: "flex-end",
83
+ flexWrap: "wrap",
84
+ gap: spacing.value(4),
85
+ });
86
+ export function Dialog({ children, isOpen: isOpenProp, defaultOpen = false, onOpenChange, isDismissable = true, isKeyboardDismissDisabled, size = "sm", role = "dialog", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, className, }) {
87
+ const isControlled = isOpenProp !== undefined;
88
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
89
+ const isOpen = isControlled ? isOpenProp : uncontrolledOpen;
90
+ const reduceMotion = Boolean(useReducedMotion());
91
+ const overlayClassName = useStyles(overlayClass);
92
+ const surfaceClassName = useStyles(surfaceClass, surfaceSizeClasses[size]);
93
+ const dialogClassName = useStyles(dialogClass);
94
+ const setOpen = useCallback((next) => {
95
+ if (!isControlled) {
96
+ setUncontrolledOpen(next);
97
+ }
98
+ onOpenChange?.(next);
99
+ }, [isControlled, onOpenChange]);
100
+ const enterTransition = reduceMotion
101
+ ? reducedMotionTransition
102
+ : dialogEnterTransition;
103
+ const exitTransition = reduceMotion
104
+ ? reducedMotionTransition
105
+ : dialogExitTransition;
106
+ const scale = reduceMotion ? 1 : 0.98;
107
+ return (_jsx(AnimatePresence, { children: isOpen && (_jsx(MotionModalOverlay, { isOpen: true, onOpenChange: setOpen, isDismissable: isDismissable, isKeyboardDismissDisabled: isKeyboardDismissDisabled, className: overlayClassName, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: reduceMotion ? reducedMotionTransition : backdropTransition, children: _jsx(MotionModal, { className: cls(surfaceClassName, className), initial: { opacity: 0, scale }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale, transition: exitTransition }, transition: enterTransition, children: _jsx(AriaDialog, { role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, className: dialogClassName, children: children }) }) })) }));
108
+ }
109
+ export function DialogTitle({ className, ...props }) {
110
+ const titleClassName = useStyles(titleClass);
111
+ return (_jsx(Heading, { ...props, slot: "title", className: cls(titleClassName, className) }));
112
+ }
113
+ export function DialogBody({ className, ...props }) {
114
+ const bodyClassName = useStyles(bodyClass);
115
+ return _jsx("div", { ...props, className: cls(bodyClassName, className) });
116
+ }
117
+ export function DialogActions({ className, ...props }) {
118
+ const actionsClassName = useStyles(actionsClass);
119
+ return _jsx("div", { ...props, className: cls(actionsClassName, className) });
34
120
  }
35
- function AnimatedComponent(props) { }
@@ -0,0 +1,6 @@
1
+ import { type ComponentPropsWithoutRef } from "react";
2
+ export type MermaidDiagramProps = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
3
+ source: string;
4
+ };
5
+ export declare function beautifulMermaidEditorUrl(source: string): string;
6
+ export declare function MermaidDiagram({ source, className, "aria-label": ariaLabel, ...props }: MermaidDiagramProps): import("react").JSX.Element;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { renderMermaidSVG } from "beautiful-mermaid";
3
+ import { useMemo, useState } from "react";
4
+ import { style, useStyles } from "purse-styles";
5
+ import { Close, Copy, Fullscreen, LinkIcon } from "../icons/root";
6
+ import { backgroundColor } from "../tokens/background";
7
+ import { colors } from "../tokens/colors";
8
+ import { radius } from "../tokens/radius";
9
+ import { spacing } from "../tokens/spacing";
10
+ import { text } from "../tokens/text";
11
+ import { cls } from "../utils/cls";
12
+ import { Button } from "./Button";
13
+ import { Dialog } from "./Dialog";
14
+ import { Tooltip } from "./Tooltip";
15
+ const mauiMermaidTheme = {
16
+ bg: backgroundColor.app,
17
+ fg: colors.gray[12],
18
+ line: colors.gray[8],
19
+ accent: colors.accent[9],
20
+ muted: colors.gray[11],
21
+ surface: backgroundColor.element,
22
+ border: colors.gray[7],
23
+ font: "system-ui",
24
+ transparent: true,
25
+ };
26
+ const beautifulMermaidEditor = "https://agents.craft.do/mermaid/editor";
27
+ export function beautifulMermaidEditorUrl(source) {
28
+ const bytes = new TextEncoder().encode(JSON.stringify({ source }));
29
+ let binary = "";
30
+ const chunkSize = 0x8000;
31
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
32
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
33
+ }
34
+ return `${beautifulMermaidEditor}#${btoa(binary)}`;
35
+ }
36
+ const containerClass = style({
37
+ position: "relative",
38
+ maxWidth: "100%",
39
+ });
40
+ const diagramClass = style({
41
+ maxWidth: "100%",
42
+ overflowX: "auto",
43
+ "& svg": {
44
+ display: "block",
45
+ height: "auto",
46
+ maxWidth: "100%",
47
+ minWidth: "720px",
48
+ },
49
+ });
50
+ const actionsClass = style({
51
+ position: "absolute",
52
+ top: spacing.value(3),
53
+ right: spacing.value(3),
54
+ zIndex: 1,
55
+ display: "flex",
56
+ gap: spacing.value(2),
57
+ });
58
+ const dialogContentClass = style({
59
+ position: "relative",
60
+ minWidth: 0,
61
+ paddingTop: spacing.value(16),
62
+ });
63
+ const errorClass = style(text({ size: "xs", color: "lowContrast", monospace: true }), spacing.padding({ all: 6 }), radius.sm, {
64
+ backgroundColor: colors.gray[3],
65
+ whiteSpace: "pre-wrap",
66
+ });
67
+ export function MermaidDiagram({ source, className, "aria-label": ariaLabel = "Mermaid diagram", ...props }) {
68
+ const [dialogOpen, setDialogOpen] = useState(false);
69
+ const containerClassName = useStyles(containerClass);
70
+ const diagramClassName = useStyles(diagramClass);
71
+ const actionsClassName = useStyles(actionsClass);
72
+ const dialogContentClassName = useStyles(dialogContentClass);
73
+ const errorClassName = useStyles(errorClass);
74
+ const result = useMemo(() => {
75
+ try {
76
+ return { svg: renderMermaidSVG(source, mauiMermaidTheme), error: null };
77
+ }
78
+ catch (error) {
79
+ return {
80
+ svg: null,
81
+ error: error instanceof Error ? error.message : String(error),
82
+ };
83
+ }
84
+ }, [source]);
85
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ...props, className: cls("maui-mermaid-diagram", containerClassName, className), children: [_jsxs("div", { className: actionsClassName, children: [_jsx(Tooltip, { content: "Open in Beautiful Mermaid", placement: "top", children: _jsx(Button, { "aria-label": "Open in Beautiful Mermaid", size: "sm", onClick: () => {
86
+ window.open(beautifulMermaidEditorUrl(source), "_blank", "noopener,noreferrer");
87
+ }, children: _jsx(LinkIcon, {}) }) }), _jsx(Tooltip, { content: "Copy Mermaid source", placement: "top", children: _jsx(Button, { "aria-label": "Copy Mermaid source", size: "sm", onClick: () => {
88
+ void navigator.clipboard.writeText(source);
89
+ }, children: _jsx(Copy, {}) }) }), _jsx(Tooltip, { content: "View fullscreen", placement: "top", children: _jsx(Button, { "aria-label": "View fullscreen", size: "sm", onClick: () => setDialogOpen(true), children: _jsx(Fullscreen, {}) }) })] }), result.error ? (_jsx("div", { className: errorClassName, role: "alert", children: result.error })) : (_jsx("div", { "aria-label": ariaLabel, className: diagramClassName, role: "img", dangerouslySetInnerHTML: { __html: result.svg ?? "" } }))] }), _jsx(Dialog, { "aria-label": `${ariaLabel} fullscreen`, isOpen: dialogOpen, onOpenChange: setDialogOpen, size: "lg", children: _jsxs("div", { className: dialogContentClassName, children: [_jsx("div", { className: actionsClassName, children: _jsx(Tooltip, { content: "Close", placement: "top", children: _jsx(Button, { "aria-label": "Close fullscreen dialog", size: "sm", slot: "close", children: _jsx(Close, {}) }) }) }), result.error ? (_jsx("div", { className: errorClassName, role: "alert", children: result.error })) : (_jsx("div", { "aria-label": `${ariaLabel} fullscreen`, className: diagramClassName, role: "img", dangerouslySetInnerHTML: { __html: result.svg ?? "" } }))] }) })] }));
90
+ }
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { animated, useSpring } from "@react-spring/web";
3
2
  import { useEffect, useRef, useState } from "react";
4
3
  import ReactDOM from "react-dom";
5
4
  import { mergeProps, useOverlayPosition, useTooltip, useTooltipTrigger, } from "react-aria";
6
5
  import { useTooltipTriggerState } from "react-stately";
6
+ import { useReducedMotion } from "motion/react";
7
7
  import { style, useStyles } from "purse-styles";
8
8
  import { background } from "../tokens/background";
9
+ import { border } from "../tokens/borders";
10
+ import { overlayMotion } from "../tokens/motion";
9
11
  import { radius } from "../tokens/radius";
10
- import { shadow } from "../tokens/shadow";
11
12
  import { spacing } from "../tokens/spacing";
12
13
  import { text } from "../tokens/text";
13
14
  export function Tooltip(props) {
14
- const { content, children, placement = "top", delay = 500, isDisabled, } = props;
15
+ const { content, children, placement = "top", delay = 400, isDisabled, } = props;
15
16
  const state = useTooltipTriggerState({ delay, isDisabled });
16
17
  const triggerRef = useRef(null);
17
18
  const { triggerProps, tooltipProps } = useTooltipTrigger({ delay, isDisabled }, state, triggerRef);
@@ -94,26 +95,15 @@ function useTooltipTransition(isOpen) {
94
95
  };
95
96
  }
96
97
  // The tooltip enters from the trigger's side: it starts nudged toward the
97
- // trigger and scaled down, then settles into its final position. The exit
98
- // reverses this, collapsing back toward the trigger.
99
- //
100
- // Both states MUST use the identical transform function structure
101
- // (`translate(x, y) scale(s)`). react-spring interpolates transforms by
102
- // matching numbers positionally between strings, so mismatched structures
103
- // (e.g. `translateY(...)` vs `translate(...)`) interpolate into garbage.
98
+ // trigger, then settles into its final position. The final exit only fades so
99
+ // dismissals feel faster than entrances.
104
100
  const hiddenTransforms = {
105
- top: "translate(0px, 3px) scale(0.94)",
106
- bottom: "translate(0px, -3px) scale(0.94)",
107
- left: "translate(3px, 0px) scale(0.94)",
108
- right: "translate(-3px, 0px) scale(0.94)",
109
- };
110
- const shownTransform = "translate(0px, 0px) scale(1)";
111
- const transformOrigins = {
112
- top: "center bottom",
113
- bottom: "center top",
114
- left: "right center",
115
- right: "left center",
101
+ top: "translate(0px, 2px)",
102
+ bottom: "translate(0px, -2px)",
103
+ left: "translate(2px, 0px)",
104
+ right: "translate(-2px, 0px)",
116
105
  };
106
+ const shownTransform = "translate(0px, 0px)";
117
107
  function TooltipPopup(props) {
118
108
  const { state, triggerRef, placement, animateIn, phase, onExited, children, ...otherProps } = props;
119
109
  const overlayRef = useRef(null);
@@ -128,30 +118,41 @@ function TooltipPopup(props) {
128
118
  });
129
119
  const resolvedPlacement = actualPlacement ?? placement;
130
120
  const hidden = hiddenTransforms[resolvedPlacement];
131
- // The spring's onRest closes over props from the render that scheduled the
132
- // animation. If the user re-enters mid-exit, react-spring interrupts the
133
- // exit and fires onRest — guard against that stale callback unmounting a
134
- // tooltip that is now animating back in.
121
+ // A transition end can arrive after the user re-enters mid-exit. Guard
122
+ // against that stale event unmounting a tooltip that is visible again.
135
123
  const phaseRef = useRef(phase);
136
124
  phaseRef.current = phase;
137
- const springStyle = useSpring({
138
- config: { tension: 1200, friction: 60 },
139
- from: { opacity: 0, transform: hidden },
140
- to: phase === "out"
141
- ? { opacity: 0, transform: hidden }
142
- : { opacity: 1, transform: shownTransform },
143
- // A tooltip opened via a "change" appears instantly (no enter animation).
144
- immediate: phase === "in" && !animateIn,
145
- onRest: (result) => {
146
- if (result.finished && phaseRef.current === "out")
147
- onExited();
148
- },
149
- });
125
+ const [visible, setVisible] = useState(!animateIn);
126
+ const reduceMotion = Boolean(useReducedMotion());
127
+ useEffect(() => {
128
+ if (phase === "out") {
129
+ setVisible(false);
130
+ return;
131
+ }
132
+ if (!animateIn) {
133
+ setVisible(true);
134
+ return;
135
+ }
136
+ const frame = requestAnimationFrame(() => setVisible(true));
137
+ return () => cancelAnimationFrame(frame);
138
+ }, [animateIn, phase]);
139
+ const transition = reduceMotion
140
+ ? `opacity ${overlayMotion.reduced.durationMs}ms ${overlayMotion.reduced.easing}`
141
+ : phase === "out"
142
+ ? `opacity ${overlayMotion.tooltipExit.durationMs}ms ${overlayMotion.tooltipExit.easing}`
143
+ : ["opacity", "transform"]
144
+ .map((property) => `${property} ${overlayMotion.tooltipEnter.durationMs}ms ${overlayMotion.tooltipEnter.easing}`)
145
+ .join(", ");
150
146
  const className = useStyles(tooltipClass);
151
- return ReactDOM.createPortal(_jsx(animated.div, { ref: overlayRef, className: className, ...mergeProps(otherProps, tooltipProps), style: {
147
+ return ReactDOM.createPortal(_jsx("div", { ref: overlayRef, className: className, ...mergeProps(otherProps, tooltipProps), style: {
152
148
  ...overlayProps.style,
153
- transformOrigin: transformOrigins[resolvedPlacement],
154
- ...springStyle,
149
+ opacity: visible ? 1 : 0,
150
+ transform: reduceMotion || phase === "out" || visible ? shownTransform : hidden,
151
+ transition: phase === "in" && !animateIn ? "none" : transition,
152
+ }, onTransitionEnd: (event) => {
153
+ if (event.propertyName === "opacity" && phaseRef.current === "out") {
154
+ onExited();
155
+ }
155
156
  }, children: children }), document.body);
156
157
  }
157
158
  // The wrapper makes the trigger hoverable/focusable as a unit without
@@ -159,7 +160,7 @@ function TooltipPopup(props) {
159
160
  const triggerClass = style({
160
161
  display: "inline-block",
161
162
  });
162
- const tooltipClass = style(text({ size: "xs", fontWeight: 400, color: "highContrast" }), radius.sm, spacing.padding({ x: 3, y: 2 }), shadow.medium, background.element, {
163
+ const tooltipClass = style(text({ size: "xs", fontWeight: 400, color: "highContrast" }), radius.sm, spacing.padding({ x: 3, y: 2 }), border([], "outline"), background.element, {
163
164
  zIndex: 1000,
164
165
  maxWidth: "240px",
165
166
  });
package/dist/maui.d.ts CHANGED
@@ -14,7 +14,7 @@ export { spacing, type Space } from "./tokens/spacing";
14
14
  export { flex, flexItem, grid, gridItem, type AlignItems, type JustifyContent } from "./tokens/layout";
15
15
  export { text, monospace, baseTextStyle, fontFamily, monoFontFamily, monoFontStyle, type TextSize, type TextWeight, type TextColor, type TextOptions, } from "./tokens/text";
16
16
  export { avatar } from "./tokens/avatar";
17
- export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, } from "./tokens/motion";
17
+ export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, overlayMotion, } from "./tokens/motion";
18
18
  export { iconSizeValues, sizingTokens as sizing, type IconSize, } from "./tokens/sizing";
19
19
  export { prose, proseRhythm, proseHtml, proseStreamingMarkers, type ProseSize, } from "./tokens/prose";
20
20
  export { visuallyHidden } from "./tokens/visuallyHidden";
@@ -28,7 +28,7 @@ export { Slider } from "./components/Slider";
28
28
  export { RadioOptionGroup, RadioOption } from "./components/Radio";
29
29
  export { TextField, SearchField, NumberField, QuietTextField, type TextFieldProps, type SearchFieldProps, } from "./components/Input";
30
30
  export { Select, SelectItem, type SelectProps, type SelectItemProps, } from "./components/Select";
31
- export { Dialog } from "./components/Dialog";
31
+ export { Dialog, DialogActions, DialogBody, DialogTitle, type DialogActionsProps, type DialogBodyProps, type DialogProps, type DialogSize, type DialogTitleProps, } from "./components/Dialog";
32
32
  export { Tooltip } from "./components/Tooltip";
33
33
  export { Overlay } from "./components/Overlay";
34
34
  export { Drawer, type DrawerProps, type DrawerSide, } from "./components/Drawer";
@@ -47,5 +47,6 @@ export { Text, type TextProps } from "./components/Text";
47
47
  export { H1, H2, H3, H4, P, Label, labelText, Blockquote, Ul, Ol, Li, Link, } from "./components/Typography";
48
48
  export { CodeBlock } from "./components/CodeBlock";
49
49
  export { Code, Kbd, type KbdProps, type KbdVariant } from "./components/Code";
50
+ export { MermaidDiagram, type MermaidDiagramProps, } from "./components/MermaidDiagram";
50
51
  export { CollectionPopover, type CollectionPopoverProps, } from "./components/CollectionPopover";
51
52
  export { navigationItem } from "./components/navigationItem";
package/dist/maui.js CHANGED
@@ -16,7 +16,7 @@ export { spacing } from "./tokens/spacing";
16
16
  export { flex, flexItem, grid, gridItem } from "./tokens/layout";
17
17
  export { text, monospace, baseTextStyle, fontFamily, monoFontFamily, monoFontStyle, } from "./tokens/text";
18
18
  export { avatar } from "./tokens/avatar";
19
- export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, } from "./tokens/motion";
19
+ export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, overlayMotion, } from "./tokens/motion";
20
20
  export { iconSizeValues, sizingTokens as sizing, } from "./tokens/sizing";
21
21
  export { prose, proseRhythm, proseHtml, proseStreamingMarkers, } from "./tokens/prose";
22
22
  export { visuallyHidden } from "./tokens/visuallyHidden";
@@ -31,7 +31,7 @@ export { Slider } from "./components/Slider";
31
31
  export { RadioOptionGroup, RadioOption } from "./components/Radio";
32
32
  export { TextField, SearchField, NumberField, QuietTextField, } from "./components/Input";
33
33
  export { Select, SelectItem, } from "./components/Select";
34
- export { Dialog } from "./components/Dialog";
34
+ export { Dialog, DialogActions, DialogBody, DialogTitle, } from "./components/Dialog";
35
35
  export { Tooltip } from "./components/Tooltip";
36
36
  export { Overlay } from "./components/Overlay";
37
37
  export { Drawer, } from "./components/Drawer";
@@ -50,5 +50,6 @@ export { Text } from "./components/Text";
50
50
  export { H1, H2, H3, H4, P, Label, labelText, Blockquote, Ul, Ol, Li, Link, } from "./components/Typography";
51
51
  export { CodeBlock } from "./components/CodeBlock";
52
52
  export { Code, Kbd } from "./components/Code";
53
+ export { MermaidDiagram, } from "./components/MermaidDiagram";
53
54
  export { CollectionPopover, } from "./components/CollectionPopover";
54
55
  export { navigationItem } from "./components/navigationItem";
@@ -3,6 +3,34 @@ export declare const motionDurationMs = 80;
3
3
  /** Entrance duration for streaming token reveals (Streamdown word fade). */
4
4
  export declare const motionStreamDurationMs = 80;
5
5
  export declare const motionEasing = "ease-in-out";
6
+ /** Shared timings for transient overlay surfaces. */
7
+ export declare const overlayMotion: {
8
+ readonly tooltipEnter: {
9
+ readonly durationMs: 120;
10
+ readonly easing: "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
11
+ };
12
+ readonly tooltipExit: {
13
+ readonly durationMs: 80;
14
+ readonly easing: "ease-in";
15
+ };
16
+ readonly dialogEnter: {
17
+ readonly durationMs: 200;
18
+ readonly easing: "spring (bounce 0)";
19
+ readonly bounce: 0;
20
+ };
21
+ readonly dialogExit: {
22
+ readonly durationMs: 120;
23
+ readonly easing: "ease-in";
24
+ };
25
+ readonly backdrop: {
26
+ readonly durationMs: 100;
27
+ readonly easing: "ease-in-out";
28
+ };
29
+ readonly reduced: {
30
+ readonly durationMs: 80;
31
+ readonly easing: "ease-in-out";
32
+ };
33
+ };
6
34
  export declare const motion: {
7
35
  readonly standard: (...args: string[]) => import("purse-styles").StyleElement;
8
36
  };
@@ -5,6 +5,22 @@ export const motionDurationMs = 80;
5
5
  /** Entrance duration for streaming token reveals (Streamdown word fade). */
6
6
  export const motionStreamDurationMs = 80;
7
7
  export const motionEasing = "ease-in-out";
8
+ /** Shared timings for transient overlay surfaces. */
9
+ export const overlayMotion = {
10
+ tooltipEnter: {
11
+ durationMs: 120,
12
+ easing: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
13
+ },
14
+ tooltipExit: { durationMs: 80, easing: "ease-in" },
15
+ dialogEnter: {
16
+ durationMs: 200,
17
+ easing: "spring (bounce 0)",
18
+ bounce: 0,
19
+ },
20
+ dialogExit: { durationMs: 120, easing: "ease-in" },
21
+ backdrop: { durationMs: 100, easing: "ease-in-out" },
22
+ reduced: { durationMs: 80, easing: "ease-in-out" },
23
+ };
8
24
  const standardDuration = `${motionDurationMs}ms`;
9
25
  export const motion = {
10
26
  standard: memoize((...properties) => style({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/maui",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "Maui design system",
5
5
  "type": "module",
6
6
  "main": "./dist/maui.js",
@@ -100,6 +100,7 @@
100
100
  "dialkit": "^2.0.2",
101
101
  "esbuild": "^0.28.0",
102
102
  "libretto-browser-tools": "^0.1.2",
103
+ "motion": "^13.2.0",
103
104
  "process": "^0.11.10",
104
105
  "satori": "^0.29.0",
105
106
  "serve": "^14.2.6",
@@ -120,14 +121,13 @@
120
121
  "@fontsource/commit-mono": "^5.3.0",
121
122
  "@lezer/highlight": "^1.2.3",
122
123
  "@radix-ui/colors": "^3.0.0",
123
- "@react-spring/web": "^10.1.0",
124
124
  "@streamdown/code": "^1.1.1",
125
125
  "@types/inline-style-prefixer": "^5.0.3",
126
126
  "@uiw/react-codemirror": "^4.25.11",
127
+ "beautiful-mermaid": "^1.1.3",
127
128
  "dedent": "^1.7.2",
128
129
  "inline-style-prefixer": "^7.0.1",
129
130
  "lodash": "^4.18.1",
130
- "motion": "^13.2.0",
131
131
  "prettier": "^3.9.6",
132
132
  "purse-styles": "^0.2.4",
133
133
  "react": "^19.0.0",
@@ -149,6 +149,7 @@
149
149
  "@tiptap/pm": "^3.29.2",
150
150
  "@tiptap/react": "^3.29.2",
151
151
  "@tiptap/starter-kit": "^3.29.2",
152
+ "motion": "^13.2.0",
152
153
  "react": "^19.0.0",
153
154
  "react-dom": "^19.0.0"
154
155
  },
@@ -50,16 +50,31 @@ Full-viewport portal. `onClickOutside` fires when the backdrop itself is the mou
50
50
 
51
51
  ## `Dialog`
52
52
 
53
- `Overlay` + focus lock + scale/fade in. Children are the dialog body (padding 32px, `background.element`, 4px radius). Not React Aria Dialog — you own title and close.
53
+ Accessible React Aria modal with focus containment/restoration, Escape handling,
54
+ outside-click dismissal, scroll locking, and reversible scale/fade motion. The
55
+ page is covered by a neutral 10% black scrim and the solid surface uses an
56
+ outline border with no blur or shadow.
54
57
 
55
58
  ```tsx
56
- {open && (
57
- <Dialog onClickOutside={() => setOpen(false)}>
58
- <H3>Confirm</H3>
59
- <P>This cannot be undone.</P>
60
- <Button onClick={() => setOpen(false)}>Close</Button>
61
- </Dialog>
62
- )}
59
+ <Dialog isOpen={open} onOpenChange={setOpen}>
60
+ <DialogTitle>Delete project?</DialogTitle>
61
+ <DialogBody>
62
+ <P>This cannot be undone.</P>
63
+ </DialogBody>
64
+ <DialogActions>
65
+ <Button slot="close">Cancel</Button>
66
+ <Button variant="primary" variantColor="red">
67
+ Delete
68
+ </Button>
69
+ </DialogActions>
70
+ </Dialog>
63
71
  ```
64
72
 
73
+ Keep `Dialog` mounted and control it with `isOpen`; conditional mounting prevents
74
+ the exit animation. `size` is `"sm" | "md" | "lg"` (`"sm"` default, 440px;
75
+ `"md"` is 640px for forms).
76
+ `isDismissable` defaults to true. Use `role="alertdialog"` and disable pointer and
77
+ keyboard dismissal when a response is required. Actions are composed rather than
78
+ passed as callback props so forms, loading states, links, and custom controls work.
79
+
65
80
  Edge-anchored mobile nav is [`Drawer`](drawer.md), not this Dialog and not Overlay.