@tanishqkancharla/maui 0.0.42 → 0.0.44
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/dist/components/Button.d.ts +9 -0
- package/dist/components/Button.js +32 -3
- package/dist/components/TabBar.d.ts +18 -0
- package/dist/components/TabBar.js +177 -0
- package/dist/maui.d.ts +2 -1
- package/dist/maui.js +1 -0
- package/dist/tokens/prose.d.ts +5 -0
- package/dist/tokens/prose.js +81 -17
- package/dist/tokens/sizing.d.ts +4 -0
- package/dist/tokens/sizing.js +1 -0
- package/package.json +1 -1
- package/skills/maui/SKILL.md +1 -0
- package/skills/maui/references/components/buttons.md +3 -1
- package/skills/maui/references/components/tab-bar.md +34 -0
- package/skills/maui/references/tokens.md +3 -1
- package/src/apps/JsxEditor/JsxEditor.tsx +1 -1
- package/src/apps/JsxEditor/catalog.ts +15 -0
- package/src/apps/JsxEditor/lint.test.ts +13 -0
- package/src/components/Button.tsx +42 -2
- package/src/components/TabBar.tsx +289 -0
- package/src/maui.ts +6 -0
- package/src/pages/ButtonsPage.tsx +21 -0
- package/src/pages/Maui.tsx +2 -0
- package/src/pages/TabBarPage.tsx +90 -0
- package/src/tokens/prose.ts +90 -19
- package/src/tokens/sizing.ts +1 -0
|
@@ -2,6 +2,13 @@ import React from "react";
|
|
|
2
2
|
import { type ButtonProps as RACButtonProps } from "react-aria-components";
|
|
3
3
|
import { type ColorName } from "../tokens/colors";
|
|
4
4
|
export type ButtonVariant = "default" | "quiet" | "primary";
|
|
5
|
+
export type ButtonSize = "default" | "sm";
|
|
6
|
+
/** Internal style primitives for controls that compose Button visuals. */
|
|
7
|
+
export declare const buttonStyles: {
|
|
8
|
+
readonly base: import("purse-styles").StyleElement;
|
|
9
|
+
readonly quiet: import("purse-styles").StyleElement;
|
|
10
|
+
readonly text: import("purse-styles").StyleElement;
|
|
11
|
+
};
|
|
5
12
|
/** Opaque hex or `rgb()`. Palette names stay ColorName (`"blue"` is Radix, not CSS `blue`). */
|
|
6
13
|
export type ButtonCssColor = `#${string}` | `rgb(${string}`;
|
|
7
14
|
export type ButtonVariantColor = ColorName | ButtonCssColor;
|
|
@@ -14,6 +21,7 @@ type ButtonData = {
|
|
|
14
21
|
export type ButtonProps = Omit<RACButtonProps, "children" | "className"> & {
|
|
15
22
|
children: React.ReactNode;
|
|
16
23
|
className?: string;
|
|
24
|
+
size?: ButtonSize;
|
|
17
25
|
variant?: ButtonVariant;
|
|
18
26
|
variantColor?: ButtonVariantColor;
|
|
19
27
|
};
|
|
@@ -24,6 +32,7 @@ export declare function useButton(props: Pick<ButtonProps, "onClick" | "onFocus"
|
|
|
24
32
|
export declare const Button: React.ForwardRefExoticComponent<Omit<RACButtonProps, "children" | "className"> & {
|
|
25
33
|
children: React.ReactNode;
|
|
26
34
|
className?: string;
|
|
35
|
+
size?: ButtonSize;
|
|
27
36
|
variant?: ButtonVariant;
|
|
28
37
|
variantColor?: ButtonVariantColor;
|
|
29
38
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -10,7 +10,7 @@ import { colors } from "../tokens/colors";
|
|
|
10
10
|
import { focusRing } from "../tokens/focusRing";
|
|
11
11
|
import { radius } from "../tokens/radius";
|
|
12
12
|
import { shadowVars } from "../tokens/shadow";
|
|
13
|
-
import { controlSize } from "../tokens/sizing";
|
|
13
|
+
import { controlSize, iconSizeValues } from "../tokens/sizing";
|
|
14
14
|
import { spacing } from "../tokens/spacing";
|
|
15
15
|
import { text } from "../tokens/text";
|
|
16
16
|
import { memoize } from "../utils/memoize";
|
|
@@ -64,6 +64,29 @@ const buttonTextClass = style({
|
|
|
64
64
|
minWidth: 0,
|
|
65
65
|
textBox: "trim-both cap alphabetic",
|
|
66
66
|
});
|
|
67
|
+
const smallButtonClass = style(text({ size: "2xs", fontWeight: 400, color: "highContrast" }), {
|
|
68
|
+
height: controlSize.smHeight,
|
|
69
|
+
gap: spacing.value(1),
|
|
70
|
+
paddingInline: spacing.value(4),
|
|
71
|
+
paddingBlock: spacing.value(2),
|
|
72
|
+
"& > svg": {
|
|
73
|
+
width: iconSizeValues.xs,
|
|
74
|
+
height: iconSizeValues.xs,
|
|
75
|
+
},
|
|
76
|
+
"&:has(> svg)": {
|
|
77
|
+
paddingBlock: spacing.value(2),
|
|
78
|
+
},
|
|
79
|
+
"&:has(> svg:first-child:not(:only-child))": {
|
|
80
|
+
paddingInlineStart: spacing.value(3),
|
|
81
|
+
},
|
|
82
|
+
"&:has(> svg:last-child:not(:only-child))": {
|
|
83
|
+
paddingInlineEnd: spacing.value(3),
|
|
84
|
+
},
|
|
85
|
+
"&:has(> svg:only-child)": {
|
|
86
|
+
width: controlSize.smHeight,
|
|
87
|
+
paddingInline: 0,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
67
90
|
const buttonClass = style(buttonBaseClass, {
|
|
68
91
|
backgroundColor: backgroundColor.element,
|
|
69
92
|
"&:hover:not(:disabled)": {
|
|
@@ -84,6 +107,12 @@ const quietClass = memoize((color, hoverColor, wash) => style(buttonBaseClass, f
|
|
|
84
107
|
"&:disabled": disabledQuiet,
|
|
85
108
|
}));
|
|
86
109
|
const quietButtonClass = quietClass(colors.gray[11], colors.gray[12], colors.grayAlpha[9]);
|
|
110
|
+
/** Internal style primitives for controls that compose Button visuals. */
|
|
111
|
+
export const buttonStyles = {
|
|
112
|
+
base: buttonBaseClass,
|
|
113
|
+
quiet: quietButtonClass,
|
|
114
|
+
text: buttonTextClass,
|
|
115
|
+
};
|
|
87
116
|
const darkTextOnSolid = new Set([
|
|
88
117
|
"amber",
|
|
89
118
|
"lime",
|
|
@@ -169,9 +198,9 @@ export function useButton(props, forwardedRef) {
|
|
|
169
198
|
];
|
|
170
199
|
}
|
|
171
200
|
export const Button = React.forwardRef(function Button(props, forwardedRef) {
|
|
172
|
-
const { children, className: classNameProp, onClick, onFocus, type = "button", variant = "default", variantColor, isDisabled, ...buttonProps } = props;
|
|
201
|
+
const { children, className: classNameProp, onClick, onFocus, type = "button", size = "default", variant = "default", variantColor, isDisabled, ...buttonProps } = props;
|
|
173
202
|
const [, attributes] = useButton({ onClick, onFocus }, forwardedRef);
|
|
174
|
-
const className = useStyles(buttonVariantClass(variant, variantColor));
|
|
203
|
+
const className = useStyles(buttonVariantClass(variant, variantColor), size === "sm" && smallButtonClass);
|
|
175
204
|
const textClassName = useStyles(buttonTextClass);
|
|
176
205
|
const mergedClassName = [className, classNameProp].filter(Boolean).join(" ");
|
|
177
206
|
return (_jsx(RACButton, { ...buttonProps, ref: attributes.ref, type: type, isDisabled: isDisabled, className: mergedClassName, onClick: attributes.onClick, onFocus: attributes.onFocus, children: renderButtonChildren(children, textClassName) }));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type TabBarItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
isClosable?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type TabBarProps = {
|
|
9
|
+
items: readonly TabBarItem[];
|
|
10
|
+
selectedId: string;
|
|
11
|
+
onSelectionChange: (id: string) => void;
|
|
12
|
+
onClose?: (id: string) => void;
|
|
13
|
+
onAdd?: () => void;
|
|
14
|
+
addLabel?: string;
|
|
15
|
+
"aria-label": string;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function TabBar({ items, selectedId, onSelectionChange, onClose, onAdd, addLabel, "aria-label": ariaLabel, className, }: TabBarProps): React.JSX.Element;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { useLocale } from "react-aria";
|
|
4
|
+
import { Button as RACButton } from "react-aria-components";
|
|
5
|
+
import { style, useStyles } from "purse-styles";
|
|
6
|
+
import { backgroundColor, surfaceMixPercent, surfaceWash, } from "../tokens/background";
|
|
7
|
+
import { colors } from "../tokens/colors";
|
|
8
|
+
import { controlSize } from "../tokens/sizing";
|
|
9
|
+
import { spacing } from "../tokens/spacing";
|
|
10
|
+
import { Button, buttonStyles } from "./Button";
|
|
11
|
+
import { Icons } from "./Icons";
|
|
12
|
+
const tabBarClass = style({
|
|
13
|
+
display: "flex",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
height: `calc(${controlSize.height} + ${spacing.value(3)} * 2)`,
|
|
16
|
+
minWidth: 0,
|
|
17
|
+
paddingInline: spacing.value(4),
|
|
18
|
+
backgroundColor: backgroundColor.app,
|
|
19
|
+
borderBottom: `1px solid ${colors.gray[6]}`,
|
|
20
|
+
});
|
|
21
|
+
const tabListClass = style({
|
|
22
|
+
display: "flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
flex: "0 1 auto",
|
|
25
|
+
minWidth: 0,
|
|
26
|
+
paddingBlock: spacing.value(3),
|
|
27
|
+
overflowX: "auto",
|
|
28
|
+
overflowY: "hidden",
|
|
29
|
+
scrollbarWidth: "none",
|
|
30
|
+
"&::-webkit-scrollbar": {
|
|
31
|
+
display: "none",
|
|
32
|
+
},
|
|
33
|
+
"& > div:has(button[data-hovered], button[aria-pressed='true']) + span": {
|
|
34
|
+
backgroundColor: "transparent",
|
|
35
|
+
},
|
|
36
|
+
"& > span:has(+ div button[data-hovered]), & > span:has(+ div button[aria-pressed='true'])": {
|
|
37
|
+
backgroundColor: "transparent",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const quietTabHover = {
|
|
41
|
+
color: colors.gray[12],
|
|
42
|
+
backgroundColor: surfaceWash(colors.grayAlpha[9], surfaceMixPercent.hover),
|
|
43
|
+
};
|
|
44
|
+
const tabClass = style({
|
|
45
|
+
position: "relative",
|
|
46
|
+
display: "flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
flex: "0 1 140px",
|
|
49
|
+
width: "140px",
|
|
50
|
+
minWidth: "56px",
|
|
51
|
+
maxWidth: "140px",
|
|
52
|
+
"&:not([data-selected='true']) > [data-close-control]": {
|
|
53
|
+
opacity: 0,
|
|
54
|
+
pointerEvents: "none",
|
|
55
|
+
},
|
|
56
|
+
"&:not([data-selected='true']):hover > [data-close-control]": {
|
|
57
|
+
opacity: 1,
|
|
58
|
+
pointerEvents: "auto",
|
|
59
|
+
},
|
|
60
|
+
"&:not([data-selected='true']):has(> button:first-child[data-hovered]) > [data-close-control], &:not([data-selected='true']):has(> [data-close-control][data-hovered]) > [data-close-control]": {
|
|
61
|
+
opacity: 1,
|
|
62
|
+
pointerEvents: "auto",
|
|
63
|
+
},
|
|
64
|
+
"&:not([data-selected='true']):hover > button:first-child": quietTabHover,
|
|
65
|
+
"&:not([data-selected='true']):has(> button:first-child[data-hovered]) > button:first-child, &:not([data-selected='true']):has(> [data-close-control][data-hovered]) > button:first-child": quietTabHover,
|
|
66
|
+
"&[data-selected='true'] > [data-close-control]:hover, &[data-selected='true'] > [data-close-control][data-hovered]": {
|
|
67
|
+
backgroundColor: backgroundColor.elementHover,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const tabButtonClass = style({
|
|
71
|
+
width: "100%",
|
|
72
|
+
minWidth: 0,
|
|
73
|
+
justifyContent: "flex-start",
|
|
74
|
+
"& > span": {
|
|
75
|
+
minWidth: 0,
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
textOverflow: "ellipsis",
|
|
78
|
+
textBox: "normal",
|
|
79
|
+
whiteSpace: "nowrap",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const selectedTabButtonClass = style({
|
|
83
|
+
backgroundColor: backgroundColor.element,
|
|
84
|
+
});
|
|
85
|
+
const closeButtonClass = style({
|
|
86
|
+
position: "absolute",
|
|
87
|
+
top: "50%",
|
|
88
|
+
insetInlineEnd: spacing.value(1),
|
|
89
|
+
zIndex: 1,
|
|
90
|
+
width: controlSize.smHeight,
|
|
91
|
+
minWidth: controlSize.smHeight,
|
|
92
|
+
maxWidth: controlSize.smHeight,
|
|
93
|
+
flexShrink: 0,
|
|
94
|
+
transform: "translateY(-50%)",
|
|
95
|
+
});
|
|
96
|
+
const separatorClass = style({
|
|
97
|
+
flex: "0 0 auto",
|
|
98
|
+
width: "0.5px",
|
|
99
|
+
height: "14px",
|
|
100
|
+
backgroundColor: colors.gray[6],
|
|
101
|
+
});
|
|
102
|
+
const addButtonClass = style({
|
|
103
|
+
flex: "0 0 auto",
|
|
104
|
+
marginInlineStart: spacing.value(2),
|
|
105
|
+
});
|
|
106
|
+
export function TabBar({ items, selectedId, onSelectionChange, onClose, onAdd, addLabel = "New tab", "aria-label": ariaLabel, className, }) {
|
|
107
|
+
const { direction } = useLocale();
|
|
108
|
+
const buttons = useRef(new Map());
|
|
109
|
+
const focusSelectedAfterClose = useRef(false);
|
|
110
|
+
const tabBarClassName = useStyles(tabBarClass);
|
|
111
|
+
const tabListClassName = useStyles(tabListClass);
|
|
112
|
+
const tabClassName = useStyles(tabClass);
|
|
113
|
+
const selectedTabButtonClassName = useStyles(buttonStyles.base, selectedTabButtonClass, tabButtonClass);
|
|
114
|
+
const quietTabButtonClassName = useStyles(buttonStyles.quiet, tabButtonClass);
|
|
115
|
+
const tabTextClassName = useStyles(buttonStyles.text);
|
|
116
|
+
const closeButtonClassName = useStyles(closeButtonClass);
|
|
117
|
+
const separatorClassName = useStyles(separatorClass);
|
|
118
|
+
const addButtonClassName = useStyles(addButtonClass);
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
if (!focusSelectedAfterClose.current)
|
|
121
|
+
return;
|
|
122
|
+
focusSelectedAfterClose.current = false;
|
|
123
|
+
buttons.current.get(selectedId)?.focus();
|
|
124
|
+
}, [items, selectedId]);
|
|
125
|
+
function closeTab(id) {
|
|
126
|
+
if (!onClose)
|
|
127
|
+
return;
|
|
128
|
+
focusSelectedAfterClose.current = true;
|
|
129
|
+
onClose(id);
|
|
130
|
+
}
|
|
131
|
+
function selectAt(index) {
|
|
132
|
+
const item = items[index];
|
|
133
|
+
if (!item)
|
|
134
|
+
return;
|
|
135
|
+
onSelectionChange(item.id);
|
|
136
|
+
buttons.current.get(item.id)?.focus();
|
|
137
|
+
}
|
|
138
|
+
function onTabKeyDown(event, index, item) {
|
|
139
|
+
let nextIndex;
|
|
140
|
+
if (event.key === "Home")
|
|
141
|
+
nextIndex = 0;
|
|
142
|
+
if (event.key === "End")
|
|
143
|
+
nextIndex = items.length - 1;
|
|
144
|
+
if (event.key === "ArrowRight") {
|
|
145
|
+
nextIndex =
|
|
146
|
+
(index + (direction === "rtl" ? -1 : 1) + items.length) % items.length;
|
|
147
|
+
}
|
|
148
|
+
if (event.key === "ArrowLeft") {
|
|
149
|
+
nextIndex =
|
|
150
|
+
(index + (direction === "rtl" ? 1 : -1) + items.length) % items.length;
|
|
151
|
+
}
|
|
152
|
+
if ((event.key === "Delete" || event.key === "Backspace") &&
|
|
153
|
+
item.id === selectedId &&
|
|
154
|
+
item.isClosable &&
|
|
155
|
+
onClose) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
closeTab(item.id);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (nextIndex === undefined)
|
|
161
|
+
return;
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
selectAt(nextIndex);
|
|
164
|
+
}
|
|
165
|
+
return (_jsxs("div", { className: [tabBarClassName, className].filter(Boolean).join(" "), children: [_jsx("div", { className: tabListClassName, role: "toolbar", "aria-label": ariaLabel, children: items.map((item, index) => {
|
|
166
|
+
const selected = item.id === selectedId;
|
|
167
|
+
const reserveClose = Boolean(item.isClosable && onClose);
|
|
168
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("div", { className: tabClassName, "data-selected": selected ? "true" : undefined, children: [_jsxs(RACButton, { ref: (element) => {
|
|
169
|
+
if (element)
|
|
170
|
+
buttons.current.set(item.id, element);
|
|
171
|
+
else
|
|
172
|
+
buttons.current.delete(item.id);
|
|
173
|
+
}, "aria-pressed": selected, excludeFromTabOrder: !selected, className: selected
|
|
174
|
+
? selectedTabButtonClassName
|
|
175
|
+
: quietTabButtonClassName, style: reserveClose ? { paddingInlineEnd: "32px" } : undefined, onPress: () => onSelectionChange(item.id), onKeyDown: (event) => onTabKeyDown(event, index, item), children: [item.icon, _jsx("span", { className: tabTextClassName, children: item.label })] }), reserveClose && (_jsx(Button, { size: "sm", variant: "quiet", "aria-label": `Close ${item.label}`, "data-close-control": true, excludeFromTabOrder: !selected, className: closeButtonClassName, onPress: () => closeTab(item.id), children: _jsx(Icons.Close, {}) }))] }), index < items.length - 1 && (_jsx("span", { className: separatorClassName, "aria-hidden": "true" }))] }, item.id));
|
|
176
|
+
}) }), onAdd && (_jsx(Button, { size: "sm", variant: "quiet", "aria-label": addLabel, className: addButtonClassName, onPress: onAdd, children: _jsx(Icons.Plus, {}) }))] }));
|
|
177
|
+
}
|
package/dist/maui.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export { prose, proseRhythm, proseHtml, proseStreamingMarkers, type ProseSize, }
|
|
|
20
20
|
export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
21
21
|
export { Avatar } from "./components/Avatar";
|
|
22
22
|
export { Badge } from "./components/Badge";
|
|
23
|
-
export { Button, useButton, type ButtonCssColor, type ButtonProps, type ButtonVariant, type ButtonVariantColor, } from "./components/Button";
|
|
23
|
+
export { Button, useButton, type ButtonCssColor, type ButtonProps, type ButtonSize, type ButtonVariant, type ButtonVariantColor, } from "./components/Button";
|
|
24
|
+
export { TabBar, type TabBarItem, type TabBarProps, } from "./components/TabBar";
|
|
24
25
|
export { Checkbox } from "./components/Checkbox";
|
|
25
26
|
export { Switch } from "./components/Switch";
|
|
26
27
|
export { Slider } from "./components/Slider";
|
package/dist/maui.js
CHANGED
|
@@ -24,6 +24,7 @@ export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
|
24
24
|
export { Avatar } from "./components/Avatar";
|
|
25
25
|
export { Badge } from "./components/Badge";
|
|
26
26
|
export { Button, useButton, } from "./components/Button";
|
|
27
|
+
export { TabBar, } from "./components/TabBar";
|
|
27
28
|
export { Checkbox } from "./components/Checkbox";
|
|
28
29
|
export { Switch } from "./components/Switch";
|
|
29
30
|
export { Slider } from "./components/Slider";
|
package/dist/tokens/prose.d.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* (https://github.com/tailwindlabs/tailwindcss-typography): `sm` -> 14px base,
|
|
9
9
|
* `md` -> Tailwind's `base` (16px), `lg` -> 18px. Tailwind's em-relative values
|
|
10
10
|
* are resolved to px here so they compose with Maui's px-based tokens.
|
|
11
|
+
*
|
|
12
|
+
* On the Large platform scale (coarse pointers) block font sizes and line
|
|
13
|
+
* heights step up ~1.2x — the same idea as the `text` tokens — so reading
|
|
14
|
+
* content grows with the rest of the UI on touch devices. Block gaps tighten
|
|
15
|
+
* separately (see `blockGaps`).
|
|
11
16
|
*/
|
|
12
17
|
export type ProseSize = "sm" | "md" | "lg";
|
|
13
18
|
export declare const prose: (size: ProseSize) => {
|
package/dist/tokens/prose.js
CHANGED
|
@@ -4,36 +4,100 @@ import { memoize } from "../utils/memoize";
|
|
|
4
4
|
import { colors } from "./colors";
|
|
5
5
|
import { motionEasing, motionStreamDurationMs } from "./motion";
|
|
6
6
|
import { fontFamily, monoFontStyle } from "./text";
|
|
7
|
+
/** A block size that steps up with the platform data scale. */
|
|
8
|
+
const scaledPx = (normal, large) => defineVars({
|
|
9
|
+
px: { default: `${normal}px`, [LARGE_SCALE]: `${large}px` },
|
|
10
|
+
}).px;
|
|
7
11
|
const metrics = {
|
|
8
12
|
sm: {
|
|
9
13
|
blockGap: 16,
|
|
10
|
-
paragraph: { fontSize: 14, lineHeight: 24 },
|
|
11
|
-
h1: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
paragraph: { fontSize: scaledPx(14, 17), lineHeight: scaledPx(24, 29) },
|
|
15
|
+
h1: {
|
|
16
|
+
fontSize: scaledPx(30, 36),
|
|
17
|
+
lineHeight: scaledPx(36, 43),
|
|
18
|
+
marginTop: 32,
|
|
19
|
+
marginBottom: 24,
|
|
20
|
+
},
|
|
21
|
+
h2: {
|
|
22
|
+
fontSize: scaledPx(20, 24),
|
|
23
|
+
lineHeight: scaledPx(28, 34),
|
|
24
|
+
marginTop: 32,
|
|
25
|
+
marginBottom: 16,
|
|
26
|
+
},
|
|
27
|
+
h3: {
|
|
28
|
+
fontSize: scaledPx(18, 22),
|
|
29
|
+
lineHeight: scaledPx(28, 34),
|
|
30
|
+
marginTop: 28,
|
|
31
|
+
marginBottom: 8,
|
|
32
|
+
},
|
|
33
|
+
h4: {
|
|
34
|
+
fontSize: scaledPx(14, 17),
|
|
35
|
+
lineHeight: scaledPx(20, 24),
|
|
36
|
+
marginTop: 20,
|
|
37
|
+
marginBottom: 8,
|
|
38
|
+
},
|
|
15
39
|
listPadding: 22,
|
|
16
40
|
listItemGap: 4,
|
|
17
41
|
listNestedGap: 8,
|
|
18
42
|
},
|
|
19
43
|
md: {
|
|
20
44
|
blockGap: 20,
|
|
21
|
-
paragraph: { fontSize: 16, lineHeight: 28 },
|
|
22
|
-
h1: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
paragraph: { fontSize: scaledPx(16, 19), lineHeight: scaledPx(28, 34) },
|
|
46
|
+
h1: {
|
|
47
|
+
fontSize: scaledPx(36, 43),
|
|
48
|
+
lineHeight: scaledPx(40, 48),
|
|
49
|
+
marginTop: 48,
|
|
50
|
+
marginBottom: 32,
|
|
51
|
+
},
|
|
52
|
+
h2: {
|
|
53
|
+
fontSize: scaledPx(24, 29),
|
|
54
|
+
lineHeight: scaledPx(32, 38),
|
|
55
|
+
marginTop: 48,
|
|
56
|
+
marginBottom: 24,
|
|
57
|
+
},
|
|
58
|
+
h3: {
|
|
59
|
+
fontSize: scaledPx(20, 24),
|
|
60
|
+
lineHeight: scaledPx(32, 38),
|
|
61
|
+
marginTop: 32,
|
|
62
|
+
marginBottom: 12,
|
|
63
|
+
},
|
|
64
|
+
h4: {
|
|
65
|
+
fontSize: scaledPx(16, 19),
|
|
66
|
+
lineHeight: scaledPx(24, 29),
|
|
67
|
+
marginTop: 24,
|
|
68
|
+
marginBottom: 8,
|
|
69
|
+
},
|
|
26
70
|
listPadding: 26,
|
|
27
71
|
listItemGap: 8,
|
|
28
72
|
listNestedGap: 12,
|
|
29
73
|
},
|
|
30
74
|
lg: {
|
|
31
75
|
blockGap: 24,
|
|
32
|
-
paragraph: { fontSize: 18, lineHeight: 32 },
|
|
33
|
-
h1: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
76
|
+
paragraph: { fontSize: scaledPx(18, 22), lineHeight: scaledPx(32, 38) },
|
|
77
|
+
h1: {
|
|
78
|
+
fontSize: scaledPx(48, 58),
|
|
79
|
+
lineHeight: scaledPx(48, 58),
|
|
80
|
+
marginTop: 56,
|
|
81
|
+
marginBottom: 40,
|
|
82
|
+
},
|
|
83
|
+
h2: {
|
|
84
|
+
fontSize: scaledPx(30, 36),
|
|
85
|
+
lineHeight: scaledPx(40, 48),
|
|
86
|
+
marginTop: 56,
|
|
87
|
+
marginBottom: 32,
|
|
88
|
+
},
|
|
89
|
+
h3: {
|
|
90
|
+
fontSize: scaledPx(24, 29),
|
|
91
|
+
lineHeight: scaledPx(36, 43),
|
|
92
|
+
marginTop: 40,
|
|
93
|
+
marginBottom: 16,
|
|
94
|
+
},
|
|
95
|
+
h4: {
|
|
96
|
+
fontSize: scaledPx(18, 22),
|
|
97
|
+
lineHeight: scaledPx(28, 34),
|
|
98
|
+
marginTop: 32,
|
|
99
|
+
marginBottom: 8,
|
|
100
|
+
},
|
|
37
101
|
listPadding: 28,
|
|
38
102
|
listItemGap: 12,
|
|
39
103
|
listNestedGap: 16,
|
|
@@ -52,8 +116,8 @@ const bodyColor = colors.gray[12];
|
|
|
52
116
|
const quoteColor = colors.gray[11];
|
|
53
117
|
const px = (value) => `${value}px`;
|
|
54
118
|
const block = (metric) => ({
|
|
55
|
-
fontSize:
|
|
56
|
-
lineHeight:
|
|
119
|
+
fontSize: metric.fontSize,
|
|
120
|
+
lineHeight: metric.lineHeight,
|
|
57
121
|
fontFamily,
|
|
58
122
|
});
|
|
59
123
|
export const prose = memoize((size) => {
|
package/dist/tokens/sizing.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare const controlSize: import("purse-styles").VariableGroup<{
|
|
|
13
13
|
readonly default: "28px";
|
|
14
14
|
readonly ":root[data-scale=\"large\"]": "40px";
|
|
15
15
|
};
|
|
16
|
+
readonly smHeight: {
|
|
17
|
+
readonly default: "24px";
|
|
18
|
+
readonly ":root[data-scale=\"large\"]": "40px";
|
|
19
|
+
};
|
|
16
20
|
readonly minTarget: {
|
|
17
21
|
readonly default: "0px";
|
|
18
22
|
readonly ":root[data-scale=\"large\"]": "40px";
|
package/dist/tokens/sizing.js
CHANGED
|
@@ -16,6 +16,7 @@ export const iconSizeValues = defineVars({
|
|
|
16
16
|
/** Shared control height and minimum touch target. */
|
|
17
17
|
export const controlSize = defineVars({
|
|
18
18
|
height: { default: "28px", [LARGE_SCALE]: "40px" },
|
|
19
|
+
smHeight: { default: "24px", [LARGE_SCALE]: "40px" },
|
|
19
20
|
minTarget: { default: "0px", [LARGE_SCALE]: "40px" },
|
|
20
21
|
});
|
|
21
22
|
/** Reading measure for long-form columns. */
|
package/package.json
CHANGED
package/skills/maui/SKILL.md
CHANGED
|
@@ -23,6 +23,7 @@ Read the reference for the work you are doing:
|
|
|
23
23
|
| Avatar | [avatar.md](references/components/avatar.md) |
|
|
24
24
|
| Badge | [badge.md](references/components/badge.md) |
|
|
25
25
|
| Buttons | [buttons.md](references/components/buttons.md) (`Button`, `Overlay`, `Dialog`) |
|
|
26
|
+
| Tab bar | [tab-bar.md](references/components/tab-bar.md) |
|
|
26
27
|
| Drawer | [drawer.md](references/components/drawer.md) |
|
|
27
28
|
| Prose | [prose.md](references/components/prose.md) (`Prose`, `H1`–`H4`, `P`, lists, `Label`, `Link`) |
|
|
28
29
|
| Editor / MarkdownEditor | [editor.md](references/components/editor.md) (optional TipTap peers; separate imports) |
|
|
@@ -4,7 +4,7 @@ Gallery: `/components/buttons`. Import from `"maui"`. Overlay and Dialog live on
|
|
|
4
4
|
|
|
5
5
|
## `Button`
|
|
6
6
|
|
|
7
|
-
Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`, so it works as a `MenuTrigger`, `Select`, `DialogTrigger`, `ComboBox`, or `DatePicker` child.
|
|
7
|
+
Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`, so it works as a `MenuTrigger`, `Select`, `DialogTrigger`, `ComboBox`, or `DatePicker` child. Default height 28px, `shadow.control`, `radius` 4px.
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
10
|
<Button>Save</Button>
|
|
@@ -12,6 +12,7 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
12
12
|
<Button variant="quiet" variantColor="accent">Quiet accent</Button>
|
|
13
13
|
<Button variant="primary" variantColor="blue">Create</Button>
|
|
14
14
|
<Button variant="primary" variantColor="#6366f1">Indigo</Button>
|
|
15
|
+
<Button size="sm">Compact</Button>
|
|
15
16
|
<Button isDisabled>Wait</Button>
|
|
16
17
|
<Button aria-label="Search"><Search size="sm" /></Button>
|
|
17
18
|
<Button>
|
|
@@ -22,6 +23,7 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
22
23
|
|
|
23
24
|
| Prop | Notes |
|
|
24
25
|
| --- | --- |
|
|
26
|
+
| `size` | `"default"` (28px) \| `"sm"` (24px with 10px text and 14px icons). Large/touch scale keeps both at a 40px target |
|
|
25
27
|
| `variant` | `"default"` (raised element) \| `"quiet"` (no shadow, transparent) \| `"primary"` (solid fill) |
|
|
26
28
|
| `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary default is `"accent"`. Quiet ignores color unless set; when set, tints the label with no fill |
|
|
27
29
|
| `isDisabled` | React Aria name. Maps to native `disabled`. **No `disabled` React prop** |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Tab bar
|
|
2
|
+
|
|
3
|
+
Gallery: `/components/tab-bar`. Import from `"maui"`.
|
|
4
|
+
|
|
5
|
+
`TabBar` is a controlled, horizontally scrollable workspace tab toolbar. The selected tab uses Maui’s default outlined `Button` treatment. Unselected tabs are quiet and separated by hairlines. Optional icons, closing, and adding are built in.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
const [selectedId, setSelectedId] = useState("changes")
|
|
9
|
+
|
|
10
|
+
<TabBar
|
|
11
|
+
aria-label="Workspace tabs"
|
|
12
|
+
items={[
|
|
13
|
+
{ id: "changes", label: "Changes", icon: <FileText /> },
|
|
14
|
+
{ id: "pull-request", label: "PR #34", icon: <GitPullRequest />, isClosable: true },
|
|
15
|
+
]}
|
|
16
|
+
selectedId={selectedId}
|
|
17
|
+
onSelectionChange={setSelectedId}
|
|
18
|
+
onClose={(id) => closeTab(id)}
|
|
19
|
+
onAdd={() => addTab()}
|
|
20
|
+
addLabel="New session"
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Prop | Notes |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `items` | `{ id, label, icon?, isClosable? }[]` |
|
|
27
|
+
| `selectedId` | Controlled selected item ID |
|
|
28
|
+
| `onSelectionChange` | Called when a tab is pressed or selected with the keyboard |
|
|
29
|
+
| `onClose` | Enables a close control for every item where `isClosable` is true. Selected tabs show it persistently; unselected tabs reveal it on hover |
|
|
30
|
+
| `onAdd` | Shows an icon-only add button |
|
|
31
|
+
| `addLabel` | Accessible add-button label; defaults to `"New tab"` |
|
|
32
|
+
| `aria-label` | Required accessible label for the tab toolbar |
|
|
33
|
+
|
|
34
|
+
Left/Right arrows wrap through tabs (and respect RTL). Home/End select the first/last tab. Delete or Backspace calls `onClose` for a selected closable tab.
|
|
@@ -113,11 +113,13 @@ text({
|
|
|
113
113
|
| `lg` | 16px | 24px |
|
|
114
114
|
| `xl` | 22px | 30px |
|
|
115
115
|
|
|
116
|
+
Sizes step up on the **Large platform scale** (`:root[data-scale="large"]`, driven by `(pointer: coarse)` under the system preference): `md` is 14px/22px on desktop and 17px/26px on coarse pointers. See `useScale` / `ScaleProvider`.
|
|
117
|
+
|
|
116
118
|
- `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white.
|
|
117
119
|
- UI sans: `fontFamily` (system ui-sans-serif stack). Mono: Commit Mono with `ss05` smart kerning (`monoFontStyle` / `monospace`).
|
|
118
120
|
- `tabular: true` → `font-variant-numeric: tabular-nums`. NumberField, TableHead, and TableCell turn this on by default. Body copy and Prose stay off.
|
|
119
121
|
- `baseTextStyle` is md / 400 / highContrast — already on `html, body`.
|
|
120
|
-
- Inside `Prose`, `H1`–`H4` / `P` / lists switch to the **prose** scale (`sm` 14px, `md` 16px, `lg` 18px), which is larger and has reading rhythm. Do not put app chrome inside `Prose`.
|
|
122
|
+
- Inside `Prose`, `H1`–`H4` / `P` / lists switch to the **prose** scale (`sm` 14px, `md` 16px, `lg` 18px), which is larger and has reading rhythm. Prose block sizes and line heights also step up ~1.2x on the Large platform scale. Do not put app chrome inside `Prose`.
|
|
121
123
|
|
|
122
124
|
`proseContainerStyle` is the composable column (`maxWidth: proseMaxWidth`, `"80ch"`). `sizing.contentWidth` is that same style. `sizing.fullWidth` is `width: 100%`.
|
|
123
125
|
|
|
@@ -349,7 +349,7 @@ const paneClass = style(border([], "outline"), radius.lg, {
|
|
|
349
349
|
minHeight: 0,
|
|
350
350
|
minWidth: 0,
|
|
351
351
|
overflow: "hidden",
|
|
352
|
-
backgroundColor: backgroundColor.
|
|
352
|
+
backgroundColor: backgroundColor.app,
|
|
353
353
|
})
|
|
354
354
|
|
|
355
355
|
const paneHeaderClass = style(
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
TableRow,
|
|
35
35
|
} from "../../components/Table"
|
|
36
36
|
import { Text } from "../../components/Text"
|
|
37
|
+
import { TabBar } from "../../components/TabBar"
|
|
37
38
|
import { Thinking } from "../../components/Thinking"
|
|
38
39
|
import { Crossfade } from "../../components/Crossfade"
|
|
39
40
|
import { LoadingScreen } from "../../components/LoadingScreen"
|
|
@@ -248,6 +249,7 @@ export const catalog: CatalogComponent[] = [
|
|
|
248
249
|
info: "Button. default is raised, quiet is borderless, primary fills with accent 9. Works as a MenuTrigger child.",
|
|
249
250
|
html: true,
|
|
250
251
|
attributes: [
|
|
252
|
+
{ name: "size", values: ["default", "sm"] },
|
|
251
253
|
{ name: "variant", values: ["default", "quiet", "primary"] },
|
|
252
254
|
{
|
|
253
255
|
name: "variantColor",
|
|
@@ -259,6 +261,18 @@ export const catalog: CatalogComponent[] = [
|
|
|
259
261
|
{ name: "isDisabled", boolean: true },
|
|
260
262
|
],
|
|
261
263
|
},
|
|
264
|
+
{
|
|
265
|
+
name: "TabBar",
|
|
266
|
+
info: "Controlled workspace tab bar. Selected tab uses the default outlined button; quiet tabs have separators.",
|
|
267
|
+
html: true,
|
|
268
|
+
attributes: [
|
|
269
|
+
{ name: "items", object: true },
|
|
270
|
+
{ name: "selectedId" },
|
|
271
|
+
{ name: "aria-label" },
|
|
272
|
+
{ name: "addLabel" },
|
|
273
|
+
{ name: "className" },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
262
276
|
{
|
|
263
277
|
name: "Drawer",
|
|
264
278
|
info: "Start/end edge modal overlay. RAC isOpen / onOpenChange. Swipe toward the edge, tap scrim, or Escape to dismiss. Not a Sidebar prop.",
|
|
@@ -534,6 +548,7 @@ export const previewScope: Record<string, unknown> = {
|
|
|
534
548
|
TableHeader,
|
|
535
549
|
TableRow,
|
|
536
550
|
Text,
|
|
551
|
+
TabBar,
|
|
537
552
|
Thinking,
|
|
538
553
|
Crossfade,
|
|
539
554
|
LoadingScreen,
|
|
@@ -23,6 +23,13 @@ describe("collectJsxDiagnosticsFromSource", () => {
|
|
|
23
23
|
expect(diagnostics).toEqual([])
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
test("accepts a small Button", () => {
|
|
27
|
+
const diagnostics = collectJsxDiagnosticsFromSource(
|
|
28
|
+
`<Button size="sm">Compact</Button>`,
|
|
29
|
+
)
|
|
30
|
+
expect(diagnostics).toEqual([])
|
|
31
|
+
})
|
|
32
|
+
|
|
26
33
|
test("accepts a braced string hex variantColor", () => {
|
|
27
34
|
const diagnostics = collectJsxDiagnosticsFromSource(
|
|
28
35
|
`<Button variant="primary" variantColor={"#1A73E8"}>Connect</Button>`,
|
|
@@ -213,6 +220,12 @@ describe("JSX editor catalog", () => {
|
|
|
213
220
|
).toBe(true)
|
|
214
221
|
})
|
|
215
222
|
|
|
223
|
+
test("catalog exposes TabBar", async () => {
|
|
224
|
+
const { catalog, previewScope } = await import("./catalog")
|
|
225
|
+
expect(catalog.some((entry) => entry.name === "TabBar")).toBe(true)
|
|
226
|
+
expect(previewScope).toHaveProperty("TabBar")
|
|
227
|
+
})
|
|
228
|
+
|
|
216
229
|
test("accepts Crossfade without direction", () => {
|
|
217
230
|
const diagnostics = collectJsxDiagnosticsFromSource(
|
|
218
231
|
`<Crossfade contentKey="inbox"><Text>Inbox</Text></Crossfade>`,
|