@tanishqkancharla/maui 0.0.62 → 0.0.65
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 +17 -1
- package/dist/components/ActivityIndicator.d.ts +3 -1
- package/dist/components/ActivityIndicator.js +5 -1
- package/dist/components/Banner.d.ts +32 -0
- package/dist/components/Banner.js +106 -0
- package/dist/components/Button.d.ts +4 -0
- package/dist/components/Button.js +80 -18
- package/dist/maui.d.ts +1 -0
- package/dist/maui.js +1 -0
- package/dist/utils/shiki.js +2 -2
- package/package.json +12 -13
- package/skills/maui/SKILL.md +2 -3
- package/skills/maui/references/components/activity-indicator.md +4 -5
- package/skills/maui/references/components/banner.md +42 -0
- package/skills/maui/references/components/buttons.md +3 -0
- package/skills/maui/references/components/code.md +1 -1
- package/src/apps/JsxEditor/catalog.ts +17 -0
- package/src/apps/JsxEditor/evaluate.test.ts +12 -1
- package/src/components/ActivityIndicator.test.tsx +30 -0
- package/src/components/ActivityIndicator.tsx +7 -2
- package/src/components/Banner.test.tsx +52 -0
- package/src/components/Banner.tsx +184 -0
- package/src/components/Button.test.tsx +46 -0
- package/src/components/Button.tsx +173 -23
- package/src/maui.ts +7 -0
- package/src/pages/ActivityIndicatorPage.tsx +22 -9
- package/src/pages/BannerPage.tsx +214 -0
- package/src/pages/ButtonsPage.tsx +61 -4
- package/src/pages/Maui.tsx +2 -0
- package/src/patterns/AssistantMessage.tsx +0 -2
- package/src/utils/shiki.ts +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ function App() {
|
|
|
41
41
|
|
|
42
42
|
Put the exported `themeFoucScript` in a classic inline `<script>` in `<head>` so `data-theme` is correct on first paint.
|
|
43
43
|
|
|
44
|
+
`CodeBlock` uses Shiki's JavaScript regex engine with Maui themes and lazy-loaded languages. No highlighting configuration is required.
|
|
45
|
+
|
|
44
46
|
## Develop
|
|
45
47
|
|
|
46
48
|
```bash
|
|
@@ -63,7 +65,21 @@ Vite serves the gallery at [http://localhost:5173/](http://localhost:5173/).
|
|
|
63
65
|
|
|
64
66
|
The default package export is compiled `dist/` JavaScript and declarations. TypeScript source remains available through the `source` export condition and `maui/src`. Patterns, demo apps, and the gallery `Panel` preview frame are reference implementations, not package components.
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
Push directly to `main`; PRs are not required. Each push prepares the next version of `@tanishqkancharla/maui`, creates a `vX.Y.Z` tag, and stages the verified tarball on npm with provenance. **Staging does not make the version public.** Add `[skip release]` to the push's head commit message to skip staging. Manual `vX.Y.Z` tag pushes also stage a release when the tag matches `package.json`.
|
|
69
|
+
|
|
70
|
+
To publish, review the version in npm's **Staged Packages** tab and approve it with 2FA. With npm 11.15.0 or newer, you can also use `npm stage list @tanishqkancharla/maui`, `npm stage view <stage-id>`, `npm stage download <stage-id>`, and `npm stage approve <stage-id>`. Approve versions in increasing order so an older approval does not move `latest` backward. Git tags reserve version numbers even while approval is pending or after a staged version is rejected; the next branch release uses a new version. A failed staging upload can be retried by manually running the workflow against its existing `vX.Y.Z` tag.
|
|
71
|
+
|
|
72
|
+
### Required npm security settings
|
|
73
|
+
|
|
74
|
+
In the package's npm settings:
|
|
75
|
+
|
|
76
|
+
1. Configure the GitHub trusted publisher for `tanishqkancharla/maui`, workflow filename `publish.yml`, with **stage-only** permission. Do not allow direct `npm publish` or dist-tag management. Audit all trusted publisher entries, not just this workflow's entry. If npm requires replacing an existing connection, copy its repository/workflow details, replace it with a stage-only connection, and complete the first staged upload within two days before the new connection expires.
|
|
77
|
+
2. Set **Publishing access** to **Require two-factor authentication and disallow bypass 2fa tokens (recommended)**. Revoke unused automation tokens.
|
|
78
|
+
3. Keep 2FA enabled on maintainer accounts. CI must never approve its own staged release.
|
|
79
|
+
|
|
80
|
+
These npm-side restrictions are essential: changing the workflow command alone does not prevent a modified workflow from attempting a direct publish. See npm's [staged publishing](https://docs.npmjs.com/staged-publishing) and [trusted publisher](https://docs.npmjs.com/trusted-publishers) documentation.
|
|
81
|
+
|
|
82
|
+
`npm run release` also stages rather than publishing directly and requires npm 11.15.0 or newer. The gallery still deploys independently to [maui.tanishqkancharla.dev](https://maui.tanishqkancharla.dev) through Vercel on `main`; npm approval does not gate that deployment.
|
|
67
83
|
|
|
68
84
|
Agent conventions for consuming Maui: [`skills/maui`](./skills/maui/SKILL.md).
|
|
69
85
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type IconSize } from "../tokens/sizing";
|
|
1
2
|
export type ActivityIndicatorVariant = "primary" | "accent" | "muted";
|
|
2
3
|
export type ActivityIndicatorProps = {
|
|
3
|
-
size
|
|
4
|
+
/** Icon t-shirt size, or a custom CSS length. Defaults to 1em. */
|
|
5
|
+
size?: IconSize | (string & {});
|
|
4
6
|
variant?: ActivityIndicatorVariant;
|
|
5
7
|
className?: string;
|
|
6
8
|
"aria-label"?: string;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { style, useStyles } from "purse-styles";
|
|
4
4
|
import { colors } from "../tokens/colors";
|
|
5
|
+
import { iconSizeValues } from "../tokens/sizing";
|
|
5
6
|
import { cls } from "../utils/cls";
|
|
6
7
|
// Geometry is in a 24×24 viewBox. Spokes end this far from the center.
|
|
7
8
|
const VIEWBOX_CENTER = 12;
|
|
@@ -28,6 +29,9 @@ const spokes = Array.from({ length: SPOKE_COUNT }, (_, index) => {
|
|
|
28
29
|
export function ActivityIndicator({ size = "1em", variant = "accent", className, "aria-label": ariaLabel = "Loading", }) {
|
|
29
30
|
const svgRef = useRef(null);
|
|
30
31
|
const rootClassName = useStyles(rootClass, variantStyles[variant]);
|
|
32
|
+
const dimension = Object.hasOwn(iconSizeValues, size)
|
|
33
|
+
? iconSizeValues[size]
|
|
34
|
+
: size;
|
|
31
35
|
useEffect(() => {
|
|
32
36
|
const animation = svgRef.current?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], {
|
|
33
37
|
duration: ROTATION_MS,
|
|
@@ -36,7 +40,7 @@ export function ActivityIndicator({ size = "1em", variant = "accent", className,
|
|
|
36
40
|
});
|
|
37
41
|
return () => animation?.cancel();
|
|
38
42
|
}, []);
|
|
39
|
-
return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: cls(rootClassName, className), style: { width:
|
|
43
|
+
return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: cls(rootClassName, className), style: { width: dimension, height: dimension }, children: _jsx("svg", { ref: svgRef, "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", width: "100%", height: "100%", children: spokes.map(({ angle, opacity }) => (_jsx("line", { x1: VIEWBOX_CENTER, y1: SPOKE_TOP, x2: VIEWBOX_CENTER, y2: SPOKE_TOP + SPOKE_LENGTH, stroke: "currentColor", strokeWidth: SPOKE_WIDTH, strokeLinecap: "round", opacity: opacity, transform: `rotate(${angle} ${VIEWBOX_CENTER} ${VIEWBOX_CENTER})` }, angle))) }) }));
|
|
40
44
|
}
|
|
41
45
|
const variantStyles = {
|
|
42
46
|
primary: style({
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
export type BannerTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
3
|
+
export type BannerLayout = "inline" | "stacked";
|
|
4
|
+
export type BannerProps = Omit<ComponentPropsWithoutRef<"div">, "title"> & {
|
|
5
|
+
tone?: BannerTone;
|
|
6
|
+
/** Use stacked for multi-sentence messages or a title with supporting text. */
|
|
7
|
+
layout?: BannerLayout;
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
/** A decorative icon. Defaults to the tone's icon; false hides it. */
|
|
10
|
+
icon?: ReactNode | false;
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
/** Adds a close button. The caller owns visibility. */
|
|
13
|
+
onDismiss?: () => void;
|
|
14
|
+
dismissLabel?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Plain div parts; icon is decorative. Layout changes action placement, not semantics. */
|
|
17
|
+
export declare const bannerStyles: {
|
|
18
|
+
readonly root: (args_0?: {
|
|
19
|
+
tone?: BannerTone;
|
|
20
|
+
} | undefined) => import("purse-styles").StyleElement;
|
|
21
|
+
readonly icon: import("purse-styles").StyleElement;
|
|
22
|
+
readonly body: (args_0?: {
|
|
23
|
+
layout?: BannerLayout;
|
|
24
|
+
} | undefined) => import("purse-styles").StyleElement;
|
|
25
|
+
readonly content: import("purse-styles").StyleElement;
|
|
26
|
+
readonly title: import("purse-styles").StyleElement;
|
|
27
|
+
readonly description: import("purse-styles").StyleElement;
|
|
28
|
+
readonly actions: import("purse-styles").StyleElement;
|
|
29
|
+
readonly dismiss: import("purse-styles").StyleElement;
|
|
30
|
+
};
|
|
31
|
+
/** Persistent in-flow message. Set role="status" or role="alert" explicitly to announce it. */
|
|
32
|
+
export declare function Banner({ tone, layout, title, children, icon, actions, onDismiss, dismissLabel, className, ...props }: BannerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { style, useStyles } from "purse-styles";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { Text } from "./Text";
|
|
5
|
+
import { CheckCircle } from "../icons/CheckCircle";
|
|
6
|
+
import { Close } from "../icons/Close";
|
|
7
|
+
import { CircleX } from "../icons/CircleX";
|
|
8
|
+
import { Information } from "../icons/Information";
|
|
9
|
+
import { WarningTriangle } from "../icons/WarningTriangle";
|
|
10
|
+
import { colors } from "../tokens/colors";
|
|
11
|
+
import { radius } from "../tokens/radius";
|
|
12
|
+
import { controlSize } from "../tokens/sizing";
|
|
13
|
+
import { spacing } from "../tokens/spacing";
|
|
14
|
+
import { text } from "../tokens/text";
|
|
15
|
+
import { cls } from "../utils/cls";
|
|
16
|
+
import { memoize } from "../utils/memoize";
|
|
17
|
+
const palettes = {
|
|
18
|
+
neutral: "gray",
|
|
19
|
+
info: "blue",
|
|
20
|
+
success: "green",
|
|
21
|
+
warning: "amber",
|
|
22
|
+
danger: "red",
|
|
23
|
+
};
|
|
24
|
+
const icons = {
|
|
25
|
+
neutral: Information,
|
|
26
|
+
info: Information,
|
|
27
|
+
success: CheckCircle,
|
|
28
|
+
warning: WarningTriangle,
|
|
29
|
+
danger: CircleX,
|
|
30
|
+
};
|
|
31
|
+
const root = memoize(({ tone = "neutral" } = {}) => style(text({ size: "sm" }), radius.lg, spacing.padding({ all: 3 }), {
|
|
32
|
+
display: "flex",
|
|
33
|
+
alignItems: "flex-start",
|
|
34
|
+
gap: spacing.value(3),
|
|
35
|
+
minWidth: 0,
|
|
36
|
+
backgroundColor: colors[palettes[tone]][2],
|
|
37
|
+
border: `1px solid ${colors[palettes[tone]][6]}`,
|
|
38
|
+
// Give text and icons the same inset beside standard-height controls.
|
|
39
|
+
"--banner-text-inset": `max(0px, calc((${controlSize.height} - 1lh) / 2))`,
|
|
40
|
+
"--banner-icon-color": colors[palettes[tone]][11],
|
|
41
|
+
}));
|
|
42
|
+
const icon = style({
|
|
43
|
+
flex: "0 0 auto",
|
|
44
|
+
width: "1lh",
|
|
45
|
+
height: "1lh",
|
|
46
|
+
marginBlockStart: "var(--banner-text-inset)",
|
|
47
|
+
color: "var(--banner-icon-color)",
|
|
48
|
+
display: "flex",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
"& > svg": { width: "100%", height: "100%" },
|
|
52
|
+
});
|
|
53
|
+
const body = memoize(({ layout = "inline" } = {}) => style({
|
|
54
|
+
display: "flex",
|
|
55
|
+
flex: "1 1 auto",
|
|
56
|
+
minWidth: 0,
|
|
57
|
+
flexDirection: layout === "stacked" ? "column" : "row",
|
|
58
|
+
flexWrap: "wrap",
|
|
59
|
+
alignItems: "flex-start",
|
|
60
|
+
gap: spacing.value(4),
|
|
61
|
+
"& > :first-child": {
|
|
62
|
+
flex: layout === "inline" ? "1 1 12rem" : "0 1 auto",
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
const content = style({
|
|
66
|
+
minWidth: 0,
|
|
67
|
+
maxWidth: "100%",
|
|
68
|
+
paddingBlock: "var(--banner-text-inset)",
|
|
69
|
+
overflowWrap: "anywhere",
|
|
70
|
+
});
|
|
71
|
+
const title = style({ display: "block" });
|
|
72
|
+
const description = style({ display: "block" });
|
|
73
|
+
const actions = style({
|
|
74
|
+
display: "flex",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
justifyContent: "flex-end",
|
|
77
|
+
flexWrap: "wrap",
|
|
78
|
+
gap: spacing.value(4),
|
|
79
|
+
maxWidth: "100%",
|
|
80
|
+
marginInlineStart: "auto",
|
|
81
|
+
});
|
|
82
|
+
const dismiss = style({ flexShrink: 0 });
|
|
83
|
+
/** Plain div parts; icon is decorative. Layout changes action placement, not semantics. */
|
|
84
|
+
export const bannerStyles = {
|
|
85
|
+
root,
|
|
86
|
+
icon,
|
|
87
|
+
body,
|
|
88
|
+
content,
|
|
89
|
+
title,
|
|
90
|
+
description,
|
|
91
|
+
actions,
|
|
92
|
+
dismiss,
|
|
93
|
+
};
|
|
94
|
+
/** Persistent in-flow message. Set role="status" or role="alert" explicitly to announce it. */
|
|
95
|
+
export function Banner({ tone = "neutral", layout = "inline", title, children, icon, actions, onDismiss, dismissLabel = "Dismiss notification", className, ...props }) {
|
|
96
|
+
const rootClassName = useStyles(bannerStyles.root({ tone }));
|
|
97
|
+
const iconClassName = useStyles(bannerStyles.icon);
|
|
98
|
+
const bodyClassName = useStyles(bannerStyles.body({ layout }));
|
|
99
|
+
const contentClassName = useStyles(bannerStyles.content);
|
|
100
|
+
const titleClassName = useStyles(bannerStyles.title);
|
|
101
|
+
const descriptionClassName = useStyles(bannerStyles.description, title != null && style({ color: colors.gray[11] }));
|
|
102
|
+
const actionsClassName = useStyles(bannerStyles.actions);
|
|
103
|
+
const dismissClassName = useStyles(bannerStyles.dismiss);
|
|
104
|
+
const Icon = icons[tone];
|
|
105
|
+
return (_jsxs("div", { ...props, className: cls(rootClassName, className), children: [icon !== false && (_jsx("div", { className: iconClassName, "aria-hidden": "true", children: icon ?? _jsx(Icon, {}) })), _jsxs("div", { className: bodyClassName, children: [_jsxs("div", { className: contentClassName, children: [title != null && (_jsx(Text, { size: "sm", fontWeight: 500, className: titleClassName, children: title })), children != null && (_jsx("div", { className: descriptionClassName, children: children }))] }), actions != null && actions !== false && (_jsx("div", { className: actionsClassName, children: actions }))] }), onDismiss && (_jsx(Button, { variant: "quiet", variantColor: palettes[tone], "aria-label": dismissLabel, onPress: onDismiss, className: dismissClassName, children: _jsx(Close, { size: "sm" }) }))] }));
|
|
106
|
+
}
|
|
@@ -24,6 +24,8 @@ export type ButtonProps = Omit<RACButtonProps, "children" | "className"> & {
|
|
|
24
24
|
size?: ButtonSize;
|
|
25
25
|
variant?: ButtonVariant;
|
|
26
26
|
variantColor?: ButtonVariantColor;
|
|
27
|
+
/** Visible text beside the indicator while pending. Defaults to children. */
|
|
28
|
+
pendingLabel?: string;
|
|
27
29
|
};
|
|
28
30
|
export declare function useButton(props: Pick<ButtonProps, "onClick" | "onFocus">, forwardedRef?: React.ForwardedRef<HTMLButtonElement>): [
|
|
29
31
|
ButtonData,
|
|
@@ -40,5 +42,7 @@ export declare const Button: React.ForwardRefExoticComponent<Omit<RACButtonProps
|
|
|
40
42
|
size?: ButtonSize;
|
|
41
43
|
variant?: ButtonVariant;
|
|
42
44
|
variantColor?: ButtonVariantColor;
|
|
45
|
+
/** Visible text beside the indicator while pending. Defaults to children. */
|
|
46
|
+
pendingLabel?: string;
|
|
43
47
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
44
48
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useLayoutEffect, useState } from "react";
|
|
3
|
+
import { AnimatePresence, motion, useIsPresent, useReducedMotion, } from "motion/react";
|
|
3
4
|
import { useId, useObjectRef } from "react-aria";
|
|
4
|
-
import { Button as RACButton, } from "react-aria-components";
|
|
5
|
+
import { Button as RACButton, ProgressBar, } from "react-aria-components";
|
|
5
6
|
import { style, useStyles } from "purse-styles";
|
|
7
|
+
import { ActivityIndicator } from "./ActivityIndicator";
|
|
6
8
|
import { useFocus } from "../hooks/useFocus";
|
|
7
9
|
import { useRefCurrent } from "../hooks/useRefCurrent";
|
|
8
10
|
import { backgroundColor, surfaceMixPercent, surfaceWash, } from "../tokens/background";
|
|
@@ -15,6 +17,7 @@ import { spacing } from "../tokens/spacing";
|
|
|
15
17
|
import { text } from "../tokens/text";
|
|
16
18
|
import { memoize } from "../utils/memoize";
|
|
17
19
|
const buttonShadow = shadowVars.control;
|
|
20
|
+
const pendingTransition = { duration: 0.15, ease: "easeInOut" };
|
|
18
21
|
const disabledRaised = {
|
|
19
22
|
cursor: "default",
|
|
20
23
|
opacity: 1,
|
|
@@ -43,21 +46,28 @@ const buttonBaseClass = style(text({ size: "xs", fontWeight: 500, color: "highCo
|
|
|
43
46
|
textOverflow: "ellipsis",
|
|
44
47
|
overflow: "hidden",
|
|
45
48
|
whiteSpace: "nowrap",
|
|
46
|
-
|
|
49
|
+
transition: `padding-inline-start ${pendingTransition.duration}s ease-in-out`,
|
|
50
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
51
|
+
transition: "none",
|
|
52
|
+
},
|
|
53
|
+
"& > svg, & [data-button-content] > svg": {
|
|
47
54
|
flexShrink: 0,
|
|
48
55
|
},
|
|
49
|
-
"&:has(> svg)": {
|
|
56
|
+
"&:has(> svg), &:has([data-button-content] > svg)": {
|
|
50
57
|
paddingBlock: spacing.value(3),
|
|
51
58
|
},
|
|
52
|
-
"&:has(> svg:first-child:not(:only-child))": {
|
|
59
|
+
"&:has(> svg:first-child:not(:only-child)), &:has([data-button-content] > svg:first-child:not(:only-child))": {
|
|
53
60
|
paddingInlineStart: spacing.value(4),
|
|
54
61
|
},
|
|
55
|
-
"&:has(> svg:last-child:not(:only-child))": {
|
|
62
|
+
"&:has(> svg:last-child:not(:only-child)), &:has([data-button-content] > svg:last-child:not(:only-child))": {
|
|
56
63
|
paddingInlineEnd: spacing.value(4),
|
|
57
64
|
},
|
|
58
|
-
"&:has(> svg:only-child)": {
|
|
65
|
+
"&:has(> svg:only-child), &:has([data-button-content] > svg:only-child)": {
|
|
59
66
|
paddingInline: spacing.value(3),
|
|
60
67
|
},
|
|
68
|
+
"&[data-pending]": {
|
|
69
|
+
paddingInlineStart: `calc((${controlSize.height} - ${iconSizeValues.xs}) / 2)`,
|
|
70
|
+
},
|
|
61
71
|
"&:disabled": disabledRaised,
|
|
62
72
|
});
|
|
63
73
|
const buttonTextClass = style({
|
|
@@ -65,32 +75,60 @@ const buttonTextClass = style({
|
|
|
65
75
|
minWidth: 0,
|
|
66
76
|
textBox: "trim-both cap alphabetic",
|
|
67
77
|
});
|
|
78
|
+
const pendingIndicatorClass = style({
|
|
79
|
+
display: "inline-flex",
|
|
80
|
+
flexShrink: 0,
|
|
81
|
+
"& > span": { display: "inline-flex" },
|
|
82
|
+
});
|
|
83
|
+
const contentRootClass = style({
|
|
84
|
+
position: "relative",
|
|
85
|
+
display: "inline-flex",
|
|
86
|
+
alignItems: "center",
|
|
87
|
+
justifyContent: "center",
|
|
88
|
+
gap: "inherit",
|
|
89
|
+
minWidth: 0,
|
|
90
|
+
});
|
|
91
|
+
const contentLayerClass = style({
|
|
92
|
+
display: "inline-flex",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
gap: "inherit",
|
|
95
|
+
flexShrink: 0,
|
|
96
|
+
"&[inert]": {
|
|
97
|
+
position: "absolute",
|
|
98
|
+
insetBlock: 0,
|
|
99
|
+
insetInlineStart: 0,
|
|
100
|
+
width: "max-content",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
68
103
|
const smallButtonClass = style(text({ size: "2xs", fontWeight: 500, color: "highContrast" }), {
|
|
69
104
|
height: controlSize.smHeight,
|
|
70
105
|
gap: spacing.value(1),
|
|
71
106
|
paddingInline: spacing.value(4),
|
|
72
107
|
paddingBlock: spacing.value(2),
|
|
73
|
-
"& > svg": {
|
|
108
|
+
"& > svg, & [data-button-content] > svg": {
|
|
74
109
|
width: iconSizeValues.xs,
|
|
75
110
|
height: iconSizeValues.xs,
|
|
76
111
|
},
|
|
77
|
-
"&:has(> svg)": {
|
|
112
|
+
"&:has(> svg), &:has([data-button-content] > svg)": {
|
|
78
113
|
paddingBlock: spacing.value(2),
|
|
79
114
|
},
|
|
80
|
-
"&:has(> svg:first-child:not(:only-child))": {
|
|
115
|
+
"&:has(> svg:first-child:not(:only-child)), &:has([data-button-content] > svg:first-child:not(:only-child))": {
|
|
81
116
|
paddingInlineStart: spacing.value(3),
|
|
82
117
|
},
|
|
83
|
-
"&:has(> svg:last-child:not(:only-child))": {
|
|
118
|
+
"&:has(> svg:last-child:not(:only-child)), &:has([data-button-content] > svg:last-child:not(:only-child))": {
|
|
84
119
|
paddingInlineEnd: spacing.value(3),
|
|
85
120
|
},
|
|
86
|
-
"&:has(> svg:only-child)": {
|
|
121
|
+
"&:has(> svg:only-child), &:has([data-button-content] > svg:only-child)": {
|
|
87
122
|
width: controlSize.smHeight,
|
|
88
123
|
paddingInline: 0,
|
|
89
124
|
},
|
|
125
|
+
"&[data-pending]": {
|
|
126
|
+
paddingInlineStart: `calc((${controlSize.smHeight} - ${iconSizeValues["2xs"]}) / 2)`,
|
|
127
|
+
},
|
|
90
128
|
});
|
|
91
129
|
const buttonClass = style(buttonBaseClass, {
|
|
92
130
|
backgroundColor: backgroundColor.element,
|
|
93
|
-
"&:hover:not(:disabled)": {
|
|
131
|
+
"&:hover:not(:disabled):not([data-pending])": {
|
|
94
132
|
backgroundColor: backgroundColor.elementHover,
|
|
95
133
|
},
|
|
96
134
|
});
|
|
@@ -101,7 +139,7 @@ const quietClass = memoize((color, hoverColor, wash) => style(buttonBaseClass, f
|
|
|
101
139
|
color,
|
|
102
140
|
backgroundColor: "transparent",
|
|
103
141
|
boxShadow: "none",
|
|
104
|
-
"&:hover:not(:disabled)": {
|
|
142
|
+
"&:hover:not(:disabled):not([data-pending])": {
|
|
105
143
|
color: hoverColor,
|
|
106
144
|
backgroundColor: surfaceWash(wash, surfaceMixPercent.hover),
|
|
107
145
|
},
|
|
@@ -163,7 +201,7 @@ const primaryClass = memoize((fill, textColor, lightEdge) => {
|
|
|
163
201
|
backgroundColor: fill,
|
|
164
202
|
backgroundImage: gradient,
|
|
165
203
|
boxShadow: primaryShadow,
|
|
166
|
-
"&:hover:not(:disabled)": {
|
|
204
|
+
"&:hover:not(:disabled):not([data-pending])": {
|
|
167
205
|
backgroundImage: `${primaryHoverSheen}, ${gradient}`,
|
|
168
206
|
},
|
|
169
207
|
"&:disabled": disabledRaised,
|
|
@@ -209,13 +247,37 @@ export const buttonStyles = {
|
|
|
209
247
|
label: buttonTextClass,
|
|
210
248
|
};
|
|
211
249
|
export const Button = React.forwardRef(function Button(props, forwardedRef) {
|
|
212
|
-
const { children, className: classNameProp, onClick, onFocus, type = "button", size = "default", variant = "default", variantColor, isDisabled, ...buttonProps } = props;
|
|
250
|
+
const { children, className: classNameProp, onClick, onFocus, type = "button", size = "default", variant = "default", variantColor, isDisabled, pendingLabel, ...buttonProps } = props;
|
|
213
251
|
const [, attributes] = useButton({ onClick, onFocus }, forwardedRef);
|
|
214
252
|
const className = useStyles(buttonStyles.root({ variant, size, variantColor }));
|
|
215
253
|
const textClassName = useStyles(buttonStyles.label);
|
|
254
|
+
const indicatorClassName = useStyles(pendingIndicatorClass);
|
|
216
255
|
const mergedClassName = [className, classNameProp].filter(Boolean).join(" ");
|
|
217
|
-
return (_jsx(RACButton, { ...buttonProps, ref: attributes.ref, type: type, isDisabled: isDisabled, className: mergedClassName, onClick: attributes.onClick, onFocus: attributes.onFocus, children: renderButtonChildren(children, textClassName) }));
|
|
256
|
+
return (_jsx(RACButton, { ...buttonProps, ref: attributes.ref, type: type, isDisabled: isDisabled, className: mergedClassName, onClick: attributes.onClick, onFocus: attributes.onFocus, children: ({ isPending }) => (_jsxs(ButtonContents, { isPending: isPending, children: [isPending && (_jsx(ProgressBar, { isIndeterminate: true, "aria-label": "Pending", className: indicatorClassName, children: _jsx("span", { "aria-hidden": "true", children: _jsx(ActivityIndicator, { variant: "primary", size: size === "sm" ? "2xs" : "xs" }) }) })), renderButtonChildren(isPending ? (pendingLabel ?? children) : children, textClassName)] })) }));
|
|
218
257
|
});
|
|
258
|
+
function ButtonContents({ isPending, children, }) {
|
|
259
|
+
const [width, setWidth] = useState();
|
|
260
|
+
const reduceMotion = useReducedMotion();
|
|
261
|
+
const className = useStyles(contentRootClass);
|
|
262
|
+
const transition = reduceMotion ? { duration: 0 } : pendingTransition;
|
|
263
|
+
return (_jsx(motion.span, { className: className, initial: false, animate: { width: width ?? "auto" }, transition: transition, children: _jsx(AnimatePresence, { initial: false, children: _jsx(ButtonContentLayer, { onResize: setWidth, transition: transition, children: children }, isPending ? "pending" : "ready") }) }));
|
|
264
|
+
}
|
|
265
|
+
function ButtonContentLayer({ children, onResize, transition, }) {
|
|
266
|
+
const ref = React.useRef(null);
|
|
267
|
+
const isPresent = useIsPresent();
|
|
268
|
+
const className = useStyles(contentLayerClass);
|
|
269
|
+
useLayoutEffect(() => {
|
|
270
|
+
const node = ref.current;
|
|
271
|
+
if (!node || !isPresent)
|
|
272
|
+
return;
|
|
273
|
+
const measure = () => onResize(node.getBoundingClientRect().width);
|
|
274
|
+
measure();
|
|
275
|
+
const observer = new ResizeObserver(measure);
|
|
276
|
+
observer.observe(node);
|
|
277
|
+
return () => observer.disconnect();
|
|
278
|
+
}, [isPresent, onResize, ref]);
|
|
279
|
+
return (_jsx(motion.span, { ref: ref, className: className, "data-button-content": isPresent ? "" : undefined, "aria-hidden": !isPresent || undefined, inert: !isPresent || undefined, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: transition, children: children }));
|
|
280
|
+
}
|
|
219
281
|
function buttonVariantClass(variant, variantColor) {
|
|
220
282
|
if (variant === "primary") {
|
|
221
283
|
return coloredButtonClass("primary", variantColor ?? "accent");
|
package/dist/maui.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { prose, proseRhythm, proseHtml, proseStreamingMarkers, type ProseSize, }
|
|
|
20
20
|
export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
21
21
|
export { Avatar, avatarStyles } from "./components/Avatar";
|
|
22
22
|
export { Badge, badgeStyles } from "./components/Badge";
|
|
23
|
+
export { Banner, bannerStyles, type BannerProps, type BannerTone, type BannerLayout, } from "./components/Banner";
|
|
23
24
|
export { LogoIcon, type LogoIconProps } from "./components/LogoIcon";
|
|
24
25
|
export { Button, buttonStyles, useButton, type ButtonCssColor, type ButtonProps, type ButtonSize, type ButtonVariant, type ButtonVariantColor, } from "./components/Button";
|
|
25
26
|
export { TabBar, tabBarStyles, type TabBarItem, type TabBarProps, } from "./components/TabBar";
|
package/dist/maui.js
CHANGED
|
@@ -23,6 +23,7 @@ export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
|
23
23
|
// Components
|
|
24
24
|
export { Avatar, avatarStyles } from "./components/Avatar";
|
|
25
25
|
export { Badge, badgeStyles } from "./components/Badge";
|
|
26
|
+
export { Banner, bannerStyles, } from "./components/Banner";
|
|
26
27
|
export { LogoIcon } from "./components/LogoIcon";
|
|
27
28
|
export { Button, buttonStyles, useButton, } from "./components/Button";
|
|
28
29
|
export { TabBar, tabBarStyles, } from "./components/TabBar";
|
package/dist/utils/shiki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHighlighterCore } from "shiki/core";
|
|
2
|
-
import {
|
|
2
|
+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
3
3
|
import { mauiShikiThemeDark, mauiShikiThemeLight } from "./mauiShikiTheme";
|
|
4
4
|
/** Canonical Shiki language ids loaded into the highlighter. */
|
|
5
5
|
export const supportedCodeLangs = [
|
|
@@ -36,7 +36,7 @@ function getHighlighter() {
|
|
|
36
36
|
import("@shikijs/langs/json"),
|
|
37
37
|
import("@shikijs/langs/bash"),
|
|
38
38
|
],
|
|
39
|
-
engine:
|
|
39
|
+
engine: createJavaScriptRegexEngine(),
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
return highlighterPromise;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanishqkancharla/maui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "Maui design system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/maui.js",
|
|
@@ -74,32 +74,32 @@
|
|
|
74
74
|
"test": "vitest",
|
|
75
75
|
"mcp:browser": "node .cursor/libretto-browser-mcp.mjs",
|
|
76
76
|
"mcp:agentation": "node .cursor/agentation-mcp.mjs",
|
|
77
|
-
"release": "npm publish --access public --provenance"
|
|
77
|
+
"release": "npm stage publish --access public --provenance"
|
|
78
78
|
},
|
|
79
79
|
"author": "Tanishq Kancharla",
|
|
80
80
|
"license": "MIT",
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@base-ui/react": "^1.9.0",
|
|
83
|
-
"@modelcontextprotocol/sdk": "^1.32.
|
|
83
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
84
84
|
"@resvg/resvg-js": "^2.6.2",
|
|
85
85
|
"@silk-hq/components": "^0.10.1",
|
|
86
|
-
"@tiptap/core": "^3.32.
|
|
87
|
-
"@tiptap/extension-placeholder": "^3.32.
|
|
88
|
-
"@tiptap/markdown": "^3.32.
|
|
89
|
-
"@tiptap/pm": "^3.32.
|
|
90
|
-
"@tiptap/react": "^3.32.
|
|
91
|
-
"@tiptap/starter-kit": "^3.32.
|
|
86
|
+
"@tiptap/core": "^3.32.1",
|
|
87
|
+
"@tiptap/extension-placeholder": "^3.32.1",
|
|
88
|
+
"@tiptap/markdown": "^3.32.1",
|
|
89
|
+
"@tiptap/pm": "^3.32.1",
|
|
90
|
+
"@tiptap/react": "^3.32.1",
|
|
91
|
+
"@tiptap/starter-kit": "^3.32.1",
|
|
92
92
|
"@types/lodash": "^4.17.27",
|
|
93
|
-
"@types/node": "^26.6.
|
|
93
|
+
"@types/node": "^26.6.6",
|
|
94
94
|
"@types/react": "^19.3.0",
|
|
95
95
|
"@types/react-dom": "^19.3.0",
|
|
96
96
|
"@vitejs/plugin-react": "^6.1.2",
|
|
97
97
|
"agentation": "^3.1.2",
|
|
98
98
|
"agentation-mcp": "^1.3.2",
|
|
99
|
-
"dialkit": "^2.0
|
|
99
|
+
"dialkit": "^2.1.0",
|
|
100
100
|
"esbuild": "^0.28.2",
|
|
101
101
|
"libretto-browser-tools": "^0.1.2",
|
|
102
|
-
"motion": "^14.
|
|
102
|
+
"motion": "^14.8.0",
|
|
103
103
|
"process": "^0.11.10",
|
|
104
104
|
"satori": "^0.63.2",
|
|
105
105
|
"serve": "^14.2.6",
|
|
@@ -119,7 +119,6 @@
|
|
|
119
119
|
"@fontsource/commit-mono": "^5.3.0",
|
|
120
120
|
"@lezer/highlight": "^1.2.5",
|
|
121
121
|
"@radix-ui/colors": "^3.0.0",
|
|
122
|
-
"@streamdown/code": "^2.0.0",
|
|
123
122
|
"@types/inline-style-prefixer": "^5.0.3",
|
|
124
123
|
"@uiw/react-codemirror": "^4.25.12",
|
|
125
124
|
"beautiful-mermaid": "^1.1.3",
|
package/skills/maui/SKILL.md
CHANGED
|
@@ -5,10 +5,8 @@ description: Conventions for installing, updating, and consuming the Maui design
|
|
|
5
5
|
|
|
6
6
|
# Maui
|
|
7
7
|
|
|
8
|
-
Read the reference for the work you are doing:
|
|
9
|
-
|
|
10
8
|
- When installing or updating the Maui package, read [installing_maui.md](references/installing_maui.md).
|
|
11
|
-
-
|
|
9
|
+
- For Maui UI work, read [design_conventions.md](references/design_conventions.md) and all matching references below.
|
|
12
10
|
|
|
13
11
|
Use Maui components. When one can't do what you need, build from the matching `react-aria-components` element with that component's exported styles (`buttonStyles`, `listBoxStyles`, …).
|
|
14
12
|
|
|
@@ -24,6 +22,7 @@ Use Maui components. When one can't do what you need, build from the matching `r
|
|
|
24
22
|
| ---------------- | -------------------------------------------------------------------------------------------- |
|
|
25
23
|
| Avatar | [avatar.md](references/components/avatar.md) |
|
|
26
24
|
| Badge | [badge.md](references/components/badge.md) |
|
|
25
|
+
| Banner | [banner.md](references/components/banner.md) |
|
|
27
26
|
| Buttons | [buttons.md](references/components/buttons.md) (`Button`, `Overlay`, `Dialog`) |
|
|
28
27
|
| Tab bar | [tab-bar.md](references/components/tab-bar.md) |
|
|
29
28
|
| Drawer | [drawer.md](references/components/drawer.md) |
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Gallery: `/components/activity-indicator`. Import from `"maui"`.
|
|
4
4
|
|
|
5
|
-
iOS-style spinner: eight spokes with a fading tail that rotate in 45° steps. `size`
|
|
5
|
+
iOS-style spinner: eight spokes with a fading tail that rotate in 45° steps. `size` accepts the icon t-shirt scale (`"2xs"`–`"xl"`, including large-scale sizing) or a CSS length; default `1em`. `variant`: `"accent"` (default) | `"primary"` (currentColor) | `"muted"`. `aria-label` defaults to `"Loading"`.
|
|
6
6
|
|
|
7
7
|
On an accent fill, such as a primary button, pass `variant="primary"` so the spinner takes the button text color.
|
|
8
8
|
|
|
9
9
|
Use it for loading: fetching, saving, syncing, and other indeterminate waits. When waiting on LLM inference, use [Thinking](thinking.md) instead. The `Spinner` icon is a static glyph, not this component.
|
|
10
10
|
|
|
11
11
|
```tsx
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</Button>
|
|
12
|
+
<ActivityIndicator size="xs" aria-label="Syncing" />
|
|
13
|
+
<ActivityIndicator size="37px" />
|
|
14
|
+
<Button variant="primary" isPending pendingLabel="Saving…">Save</Button>
|
|
16
15
|
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Banner
|
|
2
|
+
|
|
3
|
+
Gallery: `/components/banner`. Import `Banner` from `"maui"`.
|
|
4
|
+
|
|
5
|
+
Persistent in-flow message. Use `layout="inline"` (default) for one sentence,
|
|
6
|
+
even when it wraps. Always use `layout="stacked"` for multiple sentences or a
|
|
7
|
+
title with supporting text. Choose layout explicitly; Banner does not parse copy.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<Banner
|
|
11
|
+
tone="info"
|
|
12
|
+
actions={<Button variant="quiet" variantColor="blue" onPress={reload}>Reload</Button>}
|
|
13
|
+
>
|
|
14
|
+
A new version is available.
|
|
15
|
+
</Banner>
|
|
16
|
+
|
|
17
|
+
<Banner
|
|
18
|
+
tone="warning"
|
|
19
|
+
layout="stacked"
|
|
20
|
+
title="Your connection was interrupted."
|
|
21
|
+
actions={<Button variant="quiet" variantColor="amber" onPress={reconnect}>Reconnect</Button>}
|
|
22
|
+
>
|
|
23
|
+
Changes are saved on this device. Reconnect to sync.
|
|
24
|
+
</Banner>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `tone`: `neutral` (default), `info`, `success`, `warning`, `danger`.
|
|
28
|
+
- Compose `actions` with quiet buttons in the matching palette: gray, blue, green,
|
|
29
|
+
amber, red respectively. Banner does not restyle supplied actions. Stacked and
|
|
30
|
+
wrapped actions align right.
|
|
31
|
+
- `title` and children accept React nodes. `icon` defaults to the tone's icon;
|
|
32
|
+
supply a decorative SVG or `false` to hide it. Icons match the first line height.
|
|
33
|
+
Icon-to-text gap and outer padding share spacing step 3 (6px; 8px at large scale).
|
|
34
|
+
- `onDismiss` adds a close button; caller owns visibility. `dismissLabel` defaults
|
|
35
|
+
to `"Dismiss notification"`. No timer or automatic dismissal.
|
|
36
|
+
- No automatic announcements. Set `role="status"` for live updates or
|
|
37
|
+
`role="alert"` for urgent failures; tone does not determine urgency.
|
|
38
|
+
|
|
39
|
+
## Styles
|
|
40
|
+
|
|
41
|
+
**Banner** — `bannerStyles`: `root({ tone })`, `icon`, `body({ layout })`, `content`,
|
|
42
|
+
`title`, `description`, `actions`, `dismiss`. Plain div parts; icon is decorative.
|
|
@@ -14,6 +14,7 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
14
14
|
<Button variant="primary" variantColor="#6366f1">Indigo</Button>
|
|
15
15
|
<Button size="sm">Compact</Button>
|
|
16
16
|
<Button isDisabled>Wait</Button>
|
|
17
|
+
<Button isPending={isSaving} pendingLabel="Saving…">Save</Button>
|
|
17
18
|
<Button aria-label="Search"><Search size="sm" /></Button>
|
|
18
19
|
<Button>
|
|
19
20
|
<Plus size="sm" />
|
|
@@ -27,6 +28,8 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
27
28
|
| `variant` | `"default"` (raised element) \| `"quiet"` (no shadow, transparent) \| `"primary"` (solid fill) |
|
|
28
29
|
| `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary default is `"accent"`. Quiet ignores color unless set; when set, tints the label with no fill |
|
|
29
30
|
| `isDisabled` | React Aria name. Maps to native `disabled`. **No `disabled` React prop** |
|
|
31
|
+
| `isPending` | Shows a current-color indicator beside visible content: `xs` (14px) by default, `2xs` (12px) for small buttons, with equal leading/top/bottom insets. Crossfades content and resizes over 150ms in both directions; reduced motion switches instantly. Blocks activation immediately and suppresses hover fill, retaining focusability and variant color. Controlled by the caller, not by returned promises |
|
|
32
|
+
| `pendingLabel` | Optional visible string replacing children while pending (e.g. `"Saving…"`). Omit to keep children; provide it to replace an icon with progress text |
|
|
30
33
|
| `onClick` | Still supported. React Aria also exposes `onPress` |
|
|
31
34
|
| `children` | Text is wrapped for cap-height trim; SVG icons sit beside text. Icon-only needs `aria-label` |
|
|
32
35
|
|
|
@@ -8,7 +8,7 @@ Gallery: `/components/code`. Import from `"maui"`.
|
|
|
8
8
|
<CodeBlock lang="ts">{`const n = 1`}</CodeBlock>
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`CodeBlock` highlights with Shiki using the resolved theme. `lang` is required. Unsupported langs / pending highlight render a plain `<pre>`.
|
|
11
|
+
`CodeBlock` highlights with Shiki's JavaScript engine using the resolved theme. `lang` is required. Unsupported langs / pending highlight render a plain `<pre>`.
|
|
12
12
|
|
|
13
13
|
## Styles
|
|
14
14
|
|