@tanishqkancharla/maui 0.0.43 → 0.0.45

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.
@@ -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";
@@ -1,7 +1,7 @@
1
1
  export declare const shadowVars: import("purse-styles").VariableGroup<{
2
2
  readonly control: {
3
- readonly default: "inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 1px #0001, 0 0 4px -1px #0001";
4
- readonly ":root[data-theme=\"dark\"]": "inset 0 1px #ffffff08, inset 0 0 1px .5px #fff1, 0 1px .5px #00000018, 0 0 3px -1px #000a";
3
+ readonly default: "inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 0 .5px rgb(0 0 0 / 8%), 0 0 4px -1px #0001";
4
+ readonly ":root[data-theme=\"dark\"]": "inset 0 1px #ffffff08, 0 0 0 .5px rgb(255 255 255 / 8%), 0 1px .5px #00000018, 0 0 3px -1px #000a";
5
5
  };
6
6
  readonly subtle: `rgba(var(--${string}), 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, var(--${string})) 0px 1px 1px -0.5px, rgba(0, 0, 0, var(--${string})) 0px 3px 3px -1.5px`;
7
7
  readonly medium: `rgba(var(--${string}), 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, var(--${string})) 0px 1px 1px -0.5px, rgba(0, 0, 0, var(--${string})) 0px 3px 3px -1.5px, rgba(0, 0, 0, var(--${string})) 0px 6px 6px -3px`;
@@ -14,8 +14,8 @@ const rgb = shadowParams.foregroundRgb;
14
14
  const blur = shadowParams.blurOpacity;
15
15
  export const shadowVars = defineVars({
16
16
  control: {
17
- default: `inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 1px #0001, 0 0 4px -1px #0001`,
18
- [DARK_THEME]: `inset 0 1px #ffffff08, inset 0 0 1px .5px #fff1, 0 1px .5px #00000018, 0 0 3px -1px #000a`,
17
+ default: `inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 0 .5px rgb(0 0 0 / 8%), 0 0 4px -1px #0001`,
18
+ [DARK_THEME]: `inset 0 1px #ffffff08, 0 0 0 .5px rgb(255 255 255 / 8%), 0 1px .5px #00000018, 0 0 3px -1px #000a`,
19
19
  },
20
20
  subtle: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px`,
21
21
  medium: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px`,
@@ -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";
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/maui",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "Maui design system",
5
5
  "type": "module",
6
6
  "main": "./dist/maui.js",
@@ -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. Height 28px, `shadow.control`, `radius` 4px.
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.
@@ -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.element,
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>`,
@@ -16,12 +16,13 @@ import { colors, type ColorName, type ColorScale } from "../tokens/colors"
16
16
  import { focusRing } from "../tokens/focusRing"
17
17
  import { radius } from "../tokens/radius"
18
18
  import { shadowVars } from "../tokens/shadow"
19
- import { controlSize } from "../tokens/sizing"
19
+ import { controlSize, iconSizeValues } from "../tokens/sizing"
20
20
  import { spacing } from "../tokens/spacing"
21
21
  import { text } from "../tokens/text"
22
22
  import { memoize } from "../utils/memoize"
23
23
 
24
24
  export type ButtonVariant = "default" | "quiet" | "primary"
25
+ export type ButtonSize = "default" | "sm"
25
26
 
26
27
  const buttonShadow = shadowVars.control
27
28
 
@@ -84,6 +85,33 @@ const buttonTextClass = style({
84
85
  textBox: "trim-both cap alphabetic",
85
86
  })
86
87
 
88
+ const smallButtonClass = style(
89
+ text({ size: "2xs", fontWeight: 400, color: "highContrast" }),
90
+ {
91
+ height: controlSize.smHeight,
92
+ gap: spacing.value(1),
93
+ paddingInline: spacing.value(4),
94
+ paddingBlock: spacing.value(2),
95
+ "& > svg": {
96
+ width: iconSizeValues.xs,
97
+ height: iconSizeValues.xs,
98
+ },
99
+ "&:has(> svg)": {
100
+ paddingBlock: spacing.value(2),
101
+ },
102
+ "&:has(> svg:first-child:not(:only-child))": {
103
+ paddingInlineStart: spacing.value(3),
104
+ },
105
+ "&:has(> svg:last-child:not(:only-child))": {
106
+ paddingInlineEnd: spacing.value(3),
107
+ },
108
+ "&:has(> svg:only-child)": {
109
+ width: controlSize.smHeight,
110
+ paddingInline: 0,
111
+ },
112
+ },
113
+ )
114
+
87
115
  const buttonClass = style(buttonBaseClass, {
88
116
  backgroundColor: backgroundColor.element,
89
117
  "&:hover:not(:disabled)": {
@@ -114,6 +142,13 @@ const quietButtonClass = quietClass(
114
142
  colors.grayAlpha[9],
115
143
  )
116
144
 
145
+ /** Internal style primitives for controls that compose Button visuals. */
146
+ export const buttonStyles = {
147
+ base: buttonBaseClass,
148
+ quiet: quietButtonClass,
149
+ text: buttonTextClass,
150
+ } as const
151
+
117
152
  const darkTextOnSolid: ReadonlySet<ColorName> = new Set([
118
153
  "amber",
119
154
  "lime",
@@ -225,6 +260,7 @@ type ButtonData = {
225
260
  export type ButtonProps = Omit<RACButtonProps, "children" | "className"> & {
226
261
  children: React.ReactNode
227
262
  className?: string
263
+ size?: ButtonSize
228
264
  variant?: ButtonVariant
229
265
  variantColor?: ButtonVariantColor
230
266
  }
@@ -274,13 +310,17 @@ export const Button = React.forwardRef(function Button(
274
310
  onClick,
275
311
  onFocus,
276
312
  type = "button",
313
+ size = "default",
277
314
  variant = "default",
278
315
  variantColor,
279
316
  isDisabled,
280
317
  ...buttonProps
281
318
  } = props
282
319
  const [, attributes] = useButton({ onClick, onFocus }, forwardedRef)
283
- const className = useStyles(buttonVariantClass(variant, variantColor))
320
+ const className = useStyles(
321
+ buttonVariantClass(variant, variantColor),
322
+ size === "sm" && smallButtonClass,
323
+ )
284
324
  const textClassName = useStyles(buttonTextClass)
285
325
  const mergedClassName = [className, classNameProp].filter(Boolean).join(" ")
286
326
 
@@ -0,0 +1,289 @@
1
+ import React, { useLayoutEffect, useRef } from "react"
2
+ import { useLocale } from "react-aria"
3
+ import { Button as RACButton } from "react-aria-components"
4
+ import { style, useStyles } from "purse-styles"
5
+ import {
6
+ backgroundColor,
7
+ surfaceMixPercent,
8
+ surfaceWash,
9
+ } from "../tokens/background"
10
+ import { colors } from "../tokens/colors"
11
+ import { controlSize } from "../tokens/sizing"
12
+ import { spacing } from "../tokens/spacing"
13
+ import { Button, buttonStyles } from "./Button"
14
+ import { Icons } from "./Icons"
15
+
16
+ export type TabBarItem = {
17
+ id: string
18
+ label: string
19
+ icon?: React.ReactNode
20
+ isClosable?: boolean
21
+ }
22
+
23
+ export type TabBarProps = {
24
+ items: readonly TabBarItem[]
25
+ selectedId: string
26
+ onSelectionChange: (id: string) => void
27
+ onClose?: (id: string) => void
28
+ onAdd?: () => void
29
+ addLabel?: string
30
+ "aria-label": string
31
+ className?: string
32
+ }
33
+
34
+ const tabBarClass = style({
35
+ display: "flex",
36
+ alignItems: "center",
37
+ height: `calc(${controlSize.height} + ${spacing.value(3)} * 2)`,
38
+ minWidth: 0,
39
+ paddingInline: spacing.value(4),
40
+ backgroundColor: backgroundColor.app,
41
+ borderBottom: `1px solid ${colors.gray[6]}`,
42
+ })
43
+
44
+ const tabListClass = style({
45
+ display: "flex",
46
+ alignItems: "center",
47
+ flex: "0 1 auto",
48
+ minWidth: 0,
49
+ paddingBlock: spacing.value(3),
50
+ overflowX: "auto",
51
+ overflowY: "hidden",
52
+ scrollbarWidth: "none",
53
+ "&::-webkit-scrollbar": {
54
+ display: "none",
55
+ },
56
+ "& > div:has(button[data-hovered], button[aria-pressed='true']) + span": {
57
+ backgroundColor: "transparent",
58
+ },
59
+ "& > span:has(+ div button[data-hovered]), & > span:has(+ div button[aria-pressed='true'])": {
60
+ backgroundColor: "transparent",
61
+ },
62
+ })
63
+
64
+ const quietTabHover = {
65
+ color: colors.gray[12],
66
+ backgroundColor: surfaceWash(
67
+ colors.grayAlpha[9],
68
+ surfaceMixPercent.hover,
69
+ ),
70
+ } as const
71
+
72
+ const tabClass = style({
73
+ position: "relative",
74
+ display: "flex",
75
+ alignItems: "center",
76
+ flex: "0 1 140px",
77
+ width: "140px",
78
+ minWidth: "56px",
79
+ maxWidth: "140px",
80
+ "&:not([data-selected='true']) > [data-close-control]": {
81
+ opacity: 0,
82
+ pointerEvents: "none",
83
+ },
84
+ "&:not([data-selected='true']):hover > [data-close-control]": {
85
+ opacity: 1,
86
+ pointerEvents: "auto",
87
+ },
88
+ "&: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]":
89
+ {
90
+ opacity: 1,
91
+ pointerEvents: "auto",
92
+ },
93
+ "&:not([data-selected='true']):hover > button:first-child": quietTabHover,
94
+ "&: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":
95
+ quietTabHover,
96
+ "&[data-selected='true'] > [data-close-control]:hover, &[data-selected='true'] > [data-close-control][data-hovered]":
97
+ {
98
+ backgroundColor: backgroundColor.elementHover,
99
+ },
100
+ })
101
+
102
+ const tabButtonClass = style({
103
+ width: "100%",
104
+ minWidth: 0,
105
+ justifyContent: "flex-start",
106
+ "& > span": {
107
+ minWidth: 0,
108
+ overflow: "hidden",
109
+ textOverflow: "ellipsis",
110
+ textBox: "normal",
111
+ whiteSpace: "nowrap",
112
+ },
113
+ })
114
+
115
+ const selectedTabButtonClass = style({
116
+ backgroundColor: backgroundColor.element,
117
+ })
118
+
119
+ const closeButtonClass = style({
120
+ position: "absolute",
121
+ top: "50%",
122
+ insetInlineEnd: spacing.value(1),
123
+ zIndex: 1,
124
+ width: controlSize.smHeight,
125
+ minWidth: controlSize.smHeight,
126
+ maxWidth: controlSize.smHeight,
127
+ flexShrink: 0,
128
+ transform: "translateY(-50%)",
129
+ })
130
+
131
+ const separatorClass = style({
132
+ flex: "0 0 auto",
133
+ width: "0.5px",
134
+ height: "14px",
135
+ backgroundColor: colors.gray[6],
136
+ })
137
+
138
+ const addButtonClass = style({
139
+ flex: "0 0 auto",
140
+ marginInlineStart: spacing.value(2),
141
+ })
142
+
143
+ export function TabBar({
144
+ items,
145
+ selectedId,
146
+ onSelectionChange,
147
+ onClose,
148
+ onAdd,
149
+ addLabel = "New tab",
150
+ "aria-label": ariaLabel,
151
+ className,
152
+ }: TabBarProps) {
153
+ const { direction } = useLocale()
154
+ const buttons = useRef(new Map<string, HTMLButtonElement>())
155
+ const focusSelectedAfterClose = useRef(false)
156
+ const tabBarClassName = useStyles(tabBarClass)
157
+ const tabListClassName = useStyles(tabListClass)
158
+ const tabClassName = useStyles(tabClass)
159
+ const selectedTabButtonClassName = useStyles(
160
+ buttonStyles.base,
161
+ selectedTabButtonClass,
162
+ tabButtonClass,
163
+ )
164
+ const quietTabButtonClassName = useStyles(
165
+ buttonStyles.quiet,
166
+ tabButtonClass,
167
+ )
168
+ const tabTextClassName = useStyles(buttonStyles.text)
169
+ const closeButtonClassName = useStyles(closeButtonClass)
170
+ const separatorClassName = useStyles(separatorClass)
171
+ const addButtonClassName = useStyles(addButtonClass)
172
+
173
+ useLayoutEffect(() => {
174
+ if (!focusSelectedAfterClose.current) return
175
+ focusSelectedAfterClose.current = false
176
+ buttons.current.get(selectedId)?.focus()
177
+ }, [items, selectedId])
178
+
179
+ function closeTab(id: string) {
180
+ if (!onClose) return
181
+ focusSelectedAfterClose.current = true
182
+ onClose(id)
183
+ }
184
+
185
+ function selectAt(index: number) {
186
+ const item = items[index]
187
+ if (!item) return
188
+ onSelectionChange(item.id)
189
+ buttons.current.get(item.id)?.focus()
190
+ }
191
+
192
+ function onTabKeyDown(
193
+ event: React.KeyboardEvent<HTMLButtonElement>,
194
+ index: number,
195
+ item: TabBarItem,
196
+ ) {
197
+ let nextIndex: number | undefined
198
+ if (event.key === "Home") nextIndex = 0
199
+ if (event.key === "End") nextIndex = items.length - 1
200
+ if (event.key === "ArrowRight") {
201
+ nextIndex =
202
+ (index + (direction === "rtl" ? -1 : 1) + items.length) % items.length
203
+ }
204
+ if (event.key === "ArrowLeft") {
205
+ nextIndex =
206
+ (index + (direction === "rtl" ? 1 : -1) + items.length) % items.length
207
+ }
208
+ if (
209
+ (event.key === "Delete" || event.key === "Backspace") &&
210
+ item.id === selectedId &&
211
+ item.isClosable &&
212
+ onClose
213
+ ) {
214
+ event.preventDefault()
215
+ closeTab(item.id)
216
+ return
217
+ }
218
+ if (nextIndex === undefined) return
219
+ event.preventDefault()
220
+ selectAt(nextIndex)
221
+ }
222
+
223
+ return (
224
+ <div className={[tabBarClassName, className].filter(Boolean).join(" ")}>
225
+ <div className={tabListClassName} role="toolbar" aria-label={ariaLabel}>
226
+ {items.map((item, index) => {
227
+ const selected = item.id === selectedId
228
+ const reserveClose = Boolean(item.isClosable && onClose)
229
+
230
+ return (
231
+ <React.Fragment key={item.id}>
232
+ <div
233
+ className={tabClassName}
234
+ data-selected={selected ? "true" : undefined}
235
+ >
236
+ <RACButton
237
+ ref={(element) => {
238
+ if (element) buttons.current.set(item.id, element)
239
+ else buttons.current.delete(item.id)
240
+ }}
241
+ aria-pressed={selected}
242
+ excludeFromTabOrder={!selected}
243
+ className={
244
+ selected
245
+ ? selectedTabButtonClassName
246
+ : quietTabButtonClassName
247
+ }
248
+ style={reserveClose ? { paddingInlineEnd: "32px" } : undefined}
249
+ onPress={() => onSelectionChange(item.id)}
250
+ onKeyDown={(event) => onTabKeyDown(event, index, item)}
251
+ >
252
+ {item.icon}
253
+ <span className={tabTextClassName}>{item.label}</span>
254
+ </RACButton>
255
+ {reserveClose && (
256
+ <Button
257
+ size="sm"
258
+ variant="quiet"
259
+ aria-label={`Close ${item.label}`}
260
+ data-close-control
261
+ excludeFromTabOrder={!selected}
262
+ className={closeButtonClassName}
263
+ onPress={() => closeTab(item.id)}
264
+ >
265
+ <Icons.Close />
266
+ </Button>
267
+ )}
268
+ </div>
269
+ {index < items.length - 1 && (
270
+ <span className={separatorClassName} aria-hidden="true" />
271
+ )}
272
+ </React.Fragment>
273
+ )
274
+ })}
275
+ </div>
276
+ {onAdd && (
277
+ <Button
278
+ size="sm"
279
+ variant="quiet"
280
+ aria-label={addLabel}
281
+ className={addButtonClassName}
282
+ onPress={onAdd}
283
+ >
284
+ <Icons.Plus />
285
+ </Button>
286
+ )}
287
+ </div>
288
+ )
289
+ }
package/src/maui.ts CHANGED
@@ -61,9 +61,15 @@ export {
61
61
  useButton,
62
62
  type ButtonCssColor,
63
63
  type ButtonProps,
64
+ type ButtonSize,
64
65
  type ButtonVariant,
65
66
  type ButtonVariantColor,
66
67
  } from "./components/Button"
68
+ export {
69
+ TabBar,
70
+ type TabBarItem,
71
+ type TabBarProps,
72
+ } from "./components/TabBar"
67
73
  export { Checkbox } from "./components/Checkbox"
68
74
  export { Switch } from "./components/Switch"
69
75
  export { Slider } from "./components/Slider"
@@ -25,6 +25,27 @@ export function ButtonsPage() {
25
25
  <Button onClick={() => setOverlayOpen(true)}>Open Overlay</Button>
26
26
  </Flex>
27
27
 
28
+ <H3>Small</H3>
29
+ <P>
30
+ <Code>size="sm"</Code> creates a compact button for dense interfaces.
31
+ </P>
32
+ <Flex row alignItems="center" gap={4} style={{ flexWrap: "wrap" }}>
33
+ <Button size="sm">Button</Button>
34
+ <Button size="sm" variant="primary">
35
+ Save
36
+ </Button>
37
+ <Button size="sm" variant="quiet">
38
+ Quiet
39
+ </Button>
40
+ <Button size="sm">
41
+ <Plus />
42
+ Create
43
+ </Button>
44
+ <Button size="sm" aria-label="Search">
45
+ <Search />
46
+ </Button>
47
+ </Flex>
48
+
28
49
  <H3>Disabled</H3>
29
50
  <P>
30
51
  <Code>isDisabled</Code> is React Aria’s disabled prop. It sets the
@@ -69,6 +69,7 @@ import { SpacingTokenPage } from "./SpacingTokenPage"
69
69
  import { TextPage } from "./TextPage"
70
70
  import { TextTokenPage } from "./TextTokenPage"
71
71
  import { TablePage } from "./TablePage"
72
+ import { TabBarPage } from "./TabBarPage"
72
73
  import { TooltipPage } from "./TooltipPage"
73
74
 
74
75
  export function Maui() {
@@ -139,6 +140,7 @@ const navigation: NavEntry[] = [
139
140
  { label: "Avatar", path: "/components/avatar", page: AvatarPage },
140
141
  { label: "Badge", path: "/components/badge", page: BadgePage },
141
142
  { label: "Buttons", path: "/components/buttons", page: ButtonsPage },
143
+ { label: "Tab bar", path: "/components/tab-bar", page: TabBarPage },
142
144
  { label: "Drawer", path: "/components/drawer", page: DrawerPage },
143
145
  {
144
146
  label: "Prose",
@@ -0,0 +1,90 @@
1
+ import { useRef, useState } from "react"
2
+ import { TabBar, type TabBarItem } from "../components/TabBar"
3
+ import { Code } from "../components/Code"
4
+ import { Icons } from "../components/Icons"
5
+ import { Prose } from "../components/Prose"
6
+ import { H2, P } from "../components/Typography"
7
+ import { Panel } from "./Panel"
8
+
9
+ const initialTabs: TabBarItem[] = [
10
+ {
11
+ id: "changes",
12
+ label: "Changes",
13
+ icon: <Icons.FileText />,
14
+ isClosable: true,
15
+ },
16
+ {
17
+ id: "pr-339",
18
+ label: "PR #339",
19
+ icon: <Icons.GitPullRequest />,
20
+ isClosable: true,
21
+ },
22
+ {
23
+ id: "desktop",
24
+ label: "Desktop",
25
+ icon: <Icons.World />,
26
+ isClosable: true,
27
+ },
28
+ {
29
+ id: "pr-34",
30
+ label: "PR #34",
31
+ icon: <Icons.GitPullRequest />,
32
+ isClosable: true,
33
+ },
34
+ {
35
+ id: "github",
36
+ label: "Use sandbox",
37
+ icon: <Icons.Github />,
38
+ isClosable: true,
39
+ },
40
+ ]
41
+
42
+ export function TabBarPage() {
43
+ const [tabs, setTabs] = useState(initialTabs)
44
+ const [selectedId, setSelectedId] = useState("pr-34")
45
+ const nextTabId = useRef(1)
46
+
47
+ function closeTab(id: string) {
48
+ setTabs((current) => {
49
+ const index = current.findIndex((tab) => tab.id === id)
50
+ const next = current.filter((tab) => tab.id !== id)
51
+ if (id === selectedId && next.length > 0) {
52
+ setSelectedId(next[Math.min(index, next.length - 1)]!.id)
53
+ }
54
+ return next
55
+ })
56
+ }
57
+
58
+ function addTab() {
59
+ const id = `new-${nextTabId.current++}`
60
+ setTabs((current) => [
61
+ ...current,
62
+ { id, label: "New session", isClosable: true },
63
+ ])
64
+ setSelectedId(id)
65
+ }
66
+
67
+ return (
68
+ <Prose style={{ maxWidth: "1200px", paddingBottom: "32px" }}>
69
+ <H2>Tab bar</H2>
70
+ <P>
71
+ A horizontally scrollable workspace tab bar. The selected tab uses the
72
+ default outlined <Code>Button</Code> treatment; quiet tabs are separated
73
+ by hairlines. Arrow keys, Home, and End move selection. Delete or
74
+ Backspace closes a selected closable tab.
75
+ </P>
76
+ <Panel style={{ padding: 0, overflow: "hidden" }}>
77
+ <TabBar
78
+ aria-label="Workspace tabs"
79
+ items={tabs}
80
+ selectedId={selectedId}
81
+ onSelectionChange={setSelectedId}
82
+ onClose={closeTab}
83
+ onAdd={addTab}
84
+ addLabel="New session"
85
+ />
86
+ <div style={{ minHeight: "120px" }} />
87
+ </Panel>
88
+ </Prose>
89
+ )
90
+ }
@@ -17,8 +17,8 @@ const blur = shadowParams.blurOpacity
17
17
 
18
18
  export const shadowVars = defineVars({
19
19
  control: {
20
- default: `inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 1px #0001, 0 0 4px -1px #0001`,
21
- [DARK_THEME]: `inset 0 1px #ffffff08, inset 0 0 1px .5px #fff1, 0 1px .5px #00000018, 0 0 3px -1px #000a`,
20
+ default: `inset 0 1px #fff8, inset 0 0 1px 1px #fff8, 0 .5px 1px #00000018, 0 0 0 .5px rgb(0 0 0 / 8%), 0 0 4px -1px #0001`,
21
+ [DARK_THEME]: `inset 0 1px #ffffff08, 0 0 0 .5px rgb(255 255 255 / 8%), 0 1px .5px #00000018, 0 0 3px -1px #000a`,
22
22
  },
23
23
  subtle: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px`,
24
24
  medium: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px`,
@@ -22,6 +22,7 @@ export const iconSizeValues: Record<IconSize, string> = defineVars({
22
22
  /** Shared control height and minimum touch target. */
23
23
  export const controlSize = defineVars({
24
24
  height: { default: "28px", [LARGE_SCALE]: "40px" },
25
+ smHeight: { default: "24px", [LARGE_SCALE]: "40px" },
25
26
  minTarget: { default: "0px", [LARGE_SCALE]: "40px" },
26
27
  })
27
28