@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 +4 -3
- package/dist/components/Code.js +3 -2
- package/dist/components/Dialog.d.ts +23 -6
- package/dist/components/Dialog.js +113 -28
- package/dist/components/MermaidDiagram.d.ts +6 -0
- package/dist/components/MermaidDiagram.js +90 -0
- package/dist/components/Tooltip.js +43 -42
- package/dist/maui.d.ts +3 -2
- package/dist/maui.js +3 -2
- package/dist/tokens/motion.d.ts +28 -0
- package/dist/tokens/motion.js +16 -0
- package/package.json +4 -3
- package/skills/maui/references/components/buttons.md +23 -8
- package/skills/maui/references/components/tooltip.md +5 -2
- package/skills/maui/references/installing_maui.md +6 -0
- package/skills/maui/references/tokens.md +6 -1
- package/src/apps/JsxEditor/catalog.ts +51 -1
- package/src/components/Code.tsx +3 -2
- package/src/components/Dialog.tsx +222 -43
- package/src/components/MermaidDiagram.test.tsx +76 -0
- package/src/components/MermaidDiagram.tsx +200 -0
- package/src/components/Tooltip.tsx +51 -44
- package/src/maui.ts +16 -1
- package/src/pages/ButtonsPage.tsx +20 -8
- package/src/pages/DialogPage.tsx +108 -0
- package/src/pages/Maui.tsx +8 -0
- package/src/pages/MermaidDiagramPage.tsx +45 -0
- package/src/pages/MotionTokenPage.tsx +48 -4
- package/src/tokens/motion.ts +17 -0
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
|
|
package/dist/components/Code.js
CHANGED
|
@@ -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: "
|
|
11
|
+
boxDecorationBreak: "slice",
|
|
12
12
|
fontSize: "0.875em",
|
|
13
|
-
whiteSpace: "
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
|
98
|
-
//
|
|
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,
|
|
106
|
-
bottom: "translate(0px, -
|
|
107
|
-
left: "translate(
|
|
108
|
-
right: "translate(-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
147
|
+
return ReactDOM.createPortal(_jsx("div", { ref: overlayRef, className: className, ...mergeProps(otherProps, tooltipProps), style: {
|
|
152
148
|
...overlayProps.style,
|
|
153
|
-
|
|
154
|
-
|
|
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 }),
|
|
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";
|
package/dist/tokens/motion.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/tokens/motion.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|