@tanishqkancharla/maui 0.0.67 → 0.0.72
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 +1 -1
- package/dist/components/Button.js +15 -5
- package/dist/components/Editor.js +3 -3
- package/dist/components/Input.js +0 -6
- package/dist/components/ListBox.js +4 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/Slider.js +1 -1
- package/dist/components/Table.js +4 -3
- package/dist/components/navigationItem.js +4 -1
- package/dist/tokens/prose.d.ts +4 -4
- package/dist/tokens/prose.js +16 -24
- package/package.json +1 -1
- package/skills/maui/references/components/buttons.md +8 -5
- package/skills/maui/references/components/form-controls.md +2 -2
- package/skills/maui/references/components/menu.md +1 -1
- package/skills/maui/references/design_conventions.md +50 -9
- package/skills/maui/references/tokens.md +18 -21
- package/src/apps/Calendar/Calendar.tsx +1 -1
- package/src/apps/JsxEditor/catalog.ts +2 -2
- package/src/apps/JsxEditor/lint.test.ts +10 -3
- package/src/components/Button.tsx +28 -7
- package/src/components/Editor.tsx +7 -3
- package/src/components/Input.tsx +0 -6
- package/src/components/ListBox.tsx +4 -1
- package/src/components/Select.tsx +1 -1
- package/src/components/Slider.tsx +1 -1
- package/src/components/Table.tsx +7 -2
- package/src/components/navigationItem.ts +4 -1
- package/src/pages/BordersTokenPage.tsx +57 -24
- package/src/pages/ButtonsPage.tsx +30 -1
- package/src/patterns/Sidebar.tsx +1 -1
- package/src/tokens/prose.ts +19 -28
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ButtonProps as RACButtonProps } from "react-aria-components";
|
|
3
3
|
import { type ColorName } from "../tokens/colors";
|
|
4
|
-
export type ButtonVariant = "default" | "quiet" | "primary";
|
|
4
|
+
export type ButtonVariant = "default" | "quiet" | "primary" | "text";
|
|
5
5
|
export type ButtonSize = "default" | "sm";
|
|
6
6
|
/**
|
|
7
7
|
* @internal Shared shell under every Button variant (no fill, no hover).
|
|
@@ -132,6 +132,11 @@ const buttonClass = style(buttonBaseClass, {
|
|
|
132
132
|
backgroundColor: backgroundColor.elementHover,
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
|
+
const textButtonSpacingClass = style({
|
|
136
|
+
"&, &:has(> svg), &:has([data-button-content] > svg), &:has(> svg:first-child:not(:only-child)), &:has([data-button-content] > svg:first-child:not(:only-child)), &:has(> svg:last-child:not(:only-child)), &:has([data-button-content] > svg:last-child:not(:only-child)), &:has(> svg:only-child), &:has([data-button-content] > svg:only-child), &[data-pending]": {
|
|
137
|
+
padding: 0,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
135
140
|
function paletteAlpha(name) {
|
|
136
141
|
return colors[`${name}Alpha`];
|
|
137
142
|
}
|
|
@@ -141,7 +146,9 @@ const quietClass = memoize((color, hoverColor, wash) => style(buttonBaseClass, f
|
|
|
141
146
|
boxShadow: "none",
|
|
142
147
|
"&:hover:not(:disabled):not([data-pending])": {
|
|
143
148
|
color: hoverColor,
|
|
144
|
-
backgroundColor:
|
|
149
|
+
backgroundColor: wash
|
|
150
|
+
? surfaceWash(wash, surfaceMixPercent.hover)
|
|
151
|
+
: "transparent",
|
|
145
152
|
},
|
|
146
153
|
"&:disabled": disabledQuiet,
|
|
147
154
|
}));
|
|
@@ -214,13 +221,13 @@ const coloredButtonClass = memoize((variant, color) => {
|
|
|
214
221
|
// No alpha scale for a custom fill; approximate step A6 with the fill at 35%.
|
|
215
222
|
return primaryClass(fill, onSolidText(fill), fillShade(fill, 0, 0.35));
|
|
216
223
|
}
|
|
217
|
-
return quietClass(fill, darkerFill(fill), fill);
|
|
224
|
+
return quietClass(fill, darkerFill(fill), variant === "quiet" ? fill : undefined);
|
|
218
225
|
}
|
|
219
226
|
const scale = paletteFill(color);
|
|
220
227
|
if (variant === "primary") {
|
|
221
|
-
return primaryClass(scale[9], darkTextOnSolid.has(color) ? scale[
|
|
228
|
+
return primaryClass(scale[9], darkTextOnSolid.has(color) ? onSolidText(scale[9]) : "white", paletteAlpha(color)[6]);
|
|
222
229
|
}
|
|
223
|
-
return quietClass(scale[11], scale[12], paletteAlpha(color)[9]);
|
|
230
|
+
return quietClass(scale[11], scale[12], variant === "quiet" ? paletteAlpha(color)[9] : undefined);
|
|
224
231
|
});
|
|
225
232
|
export function useButton(props, forwardedRef) {
|
|
226
233
|
const ref = useObjectRef(forwardedRef);
|
|
@@ -243,7 +250,7 @@ export function useButton(props, forwardedRef) {
|
|
|
243
250
|
}
|
|
244
251
|
/** `root` goes on the RAC `Button` (native `:hover` / `:focus-visible` / `:disabled`; icons are direct `> svg` children); `label` goes on a `<span>` wrapping each text run. */
|
|
245
252
|
export const buttonStyles = {
|
|
246
|
-
root: memoize(({ variant = "default", size = "default", variantColor, } = {}) => style(buttonVariantClass(variant, variantColor), size === "sm" && smallButtonClass)),
|
|
253
|
+
root: memoize(({ variant = "default", size = "default", variantColor, } = {}) => style(buttonVariantClass(variant, variantColor), size === "sm" && smallButtonClass, variant === "text" && textButtonSpacingClass)),
|
|
247
254
|
label: buttonTextClass,
|
|
248
255
|
};
|
|
249
256
|
export const Button = React.forwardRef(function Button(props, forwardedRef) {
|
|
@@ -282,6 +289,9 @@ function buttonVariantClass(variant, variantColor) {
|
|
|
282
289
|
if (variant === "primary") {
|
|
283
290
|
return coloredButtonClass("primary", variantColor ?? "accent");
|
|
284
291
|
}
|
|
292
|
+
if (variant === "text") {
|
|
293
|
+
return coloredButtonClass("text", variantColor ?? "gray");
|
|
294
|
+
}
|
|
285
295
|
if (variant === "quiet" && variantColor) {
|
|
286
296
|
return coloredButtonClass("quiet", variantColor);
|
|
287
297
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { EditorContent } from "@tiptap/react";
|
|
3
3
|
import { style, useStyles } from "purse-styles";
|
|
4
4
|
import { colors } from "../tokens/colors";
|
|
5
|
-
import { proseHtml } from "../tokens/prose";
|
|
5
|
+
import { proseHtml, proseRhythm } from "../tokens/prose";
|
|
6
6
|
import { cls } from "../utils/cls";
|
|
7
7
|
/**
|
|
8
8
|
* Maui-styled TipTap surface. Create the editor with TipTap's useEditor;
|
|
@@ -11,7 +11,7 @@ import { cls } from "../utils/cls";
|
|
|
11
11
|
* through TipTap's editorProps.attributes.
|
|
12
12
|
*/
|
|
13
13
|
export function Editor({ size = "md", className, ...props }) {
|
|
14
|
-
const surfaceClassName = useStyles(editorClass, proseHtml(size));
|
|
14
|
+
const surfaceClassName = useStyles(editorClass, proseHtml(size), proseRhythm(size, "& > .ProseMirror"));
|
|
15
15
|
return (_jsx(EditorContent, { ...props, className: cls("maui-editor-prose", surfaceClassName, className) }));
|
|
16
16
|
}
|
|
17
17
|
const editorClass = style({
|
|
@@ -23,7 +23,7 @@ const editorClass = style({
|
|
|
23
23
|
// proseHtml styles descendants; its block rhythm belongs on the document.
|
|
24
24
|
display: "flex",
|
|
25
25
|
flexDirection: "column",
|
|
26
|
-
gap:
|
|
26
|
+
gap: 0,
|
|
27
27
|
},
|
|
28
28
|
"& .ProseMirror p.is-editor-empty:first-child::before": {
|
|
29
29
|
color: colors.gray[9],
|
package/dist/components/Input.js
CHANGED
|
@@ -24,9 +24,6 @@ const inputClass = style(inputText, focusRing("&:focus-visible", shadowVars.cont
|
|
|
24
24
|
color: colors.gray[12],
|
|
25
25
|
border: "none",
|
|
26
26
|
background: backgroundColor.element,
|
|
27
|
-
"&:hover:not(:disabled)": {
|
|
28
|
-
background: backgroundColor.elementHover,
|
|
29
|
-
},
|
|
30
27
|
"&:disabled": {
|
|
31
28
|
color: colors.gray[8],
|
|
32
29
|
background: colors.gray[2],
|
|
@@ -228,9 +225,6 @@ const numberInputClass = style(text({ size: "sm", fontWeight: 400, color: "highC
|
|
|
228
225
|
border: "none",
|
|
229
226
|
outline: "none",
|
|
230
227
|
background: "transparent",
|
|
231
|
-
"&:hover:not(:disabled)": {
|
|
232
|
-
background: backgroundColor.elementHover,
|
|
233
|
-
},
|
|
234
228
|
"&::placeholder": {
|
|
235
229
|
fontStyle: "italic",
|
|
236
230
|
color: colors.gray[8],
|
|
@@ -32,9 +32,12 @@ const item = style(spacing.padding({ x: 4, y: 2 }), text({ size: "sm", fontWeigh
|
|
|
32
32
|
background: backgroundColor.elementHover,
|
|
33
33
|
},
|
|
34
34
|
"&[data-selected]": {
|
|
35
|
-
color: colors.accent[
|
|
35
|
+
color: colors.accent[11],
|
|
36
36
|
fontWeight: 500,
|
|
37
37
|
},
|
|
38
|
+
"&[data-selected]:not([data-disabled]):is(:hover, [data-hovered], [data-focused])": {
|
|
39
|
+
color: colors.accent[12],
|
|
40
|
+
},
|
|
38
41
|
"&[data-selected]::after": {
|
|
39
42
|
content: '"✓"',
|
|
40
43
|
position: "absolute",
|
|
@@ -28,7 +28,7 @@ const trigger = style(text({ size: "sm", fontWeight: 400, color: "highContrast"
|
|
|
28
28
|
border: "none",
|
|
29
29
|
textAlign: "left",
|
|
30
30
|
background: backgroundColor.element,
|
|
31
|
-
"
|
|
31
|
+
"&[data-pressed]": {
|
|
32
32
|
background: backgroundColor.elementHover,
|
|
33
33
|
},
|
|
34
34
|
"&[aria-expanded='true']": {
|
|
@@ -44,7 +44,7 @@ const thumb = style(focusRing("&[data-focus-visible]", shadowVars.control), {
|
|
|
44
44
|
width: sliderSize.thumb,
|
|
45
45
|
height: sliderSize.thumb,
|
|
46
46
|
borderRadius: "100%",
|
|
47
|
-
background:
|
|
47
|
+
background: "#ffffff",
|
|
48
48
|
boxShadow: shadowVars.control,
|
|
49
49
|
"&[data-dragging]": {
|
|
50
50
|
background: colors.accent[11],
|
package/dist/components/Table.js
CHANGED
|
@@ -69,7 +69,7 @@ const row = style({
|
|
|
69
69
|
"&[data-selection-mode][data-selected] td": {
|
|
70
70
|
backgroundColor: colors.accent[3],
|
|
71
71
|
},
|
|
72
|
-
"&[data-disabled]": {
|
|
72
|
+
"&[data-disabled] td": {
|
|
73
73
|
color: colors.gray[8],
|
|
74
74
|
},
|
|
75
75
|
});
|
|
@@ -105,11 +105,11 @@ const selectionCheck = style({
|
|
|
105
105
|
height: "10px",
|
|
106
106
|
paddingTop: "0.5px",
|
|
107
107
|
paddingLeft: "0.5px",
|
|
108
|
-
fill:
|
|
108
|
+
fill: "white",
|
|
109
109
|
opacity: 0,
|
|
110
110
|
transform: "scale(0)",
|
|
111
111
|
});
|
|
112
|
-
const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "center" }), focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control), {
|
|
112
|
+
const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "center" }), focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control), focusRing(`&[data-selected][data-focus-visible] ${selectionBox}, &[data-indeterminate][data-focus-visible] ${selectionBox}`, shadowVars.selectedControl), {
|
|
113
113
|
position: "relative",
|
|
114
114
|
width: "fit-content",
|
|
115
115
|
cursor: "default",
|
|
@@ -129,6 +129,7 @@ const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "ce
|
|
|
129
129
|
},
|
|
130
130
|
[`&[data-selected] ${selectionBox}, &[data-indeterminate] ${selectionBox}`]: {
|
|
131
131
|
backgroundColor: colors.accent[9],
|
|
132
|
+
boxShadow: shadowVars.selectedControl,
|
|
132
133
|
},
|
|
133
134
|
[`&[data-hovered][data-selected] ${selectionBox}, &[data-hovered][data-indeterminate] ${selectionBox}`]: {
|
|
134
135
|
backgroundColor: surfaceWash(colors.gray[12], surfaceMixPercent.hover, colors.accent[9]),
|
|
@@ -13,7 +13,10 @@ export const navigationItem = style(spacing.padding({ x: 4, y: 2 }), text({ size
|
|
|
13
13
|
background: backgroundColor.elementHover,
|
|
14
14
|
},
|
|
15
15
|
"&[aria-current='page']": {
|
|
16
|
-
color: colors.accent[
|
|
16
|
+
color: colors.accent[11],
|
|
17
17
|
fontWeight: 500,
|
|
18
18
|
},
|
|
19
|
+
"&[aria-current='page']:hover": {
|
|
20
|
+
color: colors.accent[12],
|
|
21
|
+
},
|
|
19
22
|
});
|
package/dist/tokens/prose.d.ts
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* rhythm tuned for sustained reading. The `Prose` container picks a size and
|
|
5
5
|
* typography components inside it opt into the matching prose treatment.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Type sizes and heading spacing are ported from the Tailwind Typography plugin
|
|
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
11
|
*
|
|
12
12
|
* On the Large platform scale (coarse pointers) block font sizes and line
|
|
13
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.
|
|
15
|
-
*
|
|
14
|
+
* content grows with the rest of the UI on touch devices. Paragraph gaps stay
|
|
15
|
+
* at 10px on both scales.
|
|
16
16
|
*/
|
|
17
17
|
export type ProseSize = "sm" | "md" | "lg";
|
|
18
18
|
export declare const prose: (size: ProseSize) => {
|
|
@@ -32,7 +32,7 @@ export declare const prose: (size: ProseSize) => {
|
|
|
32
32
|
* `marginBottom` becomes the "hug" gap after a heading; heading `marginTop`
|
|
33
33
|
* becomes the larger break before it.
|
|
34
34
|
*/
|
|
35
|
-
export declare const proseRhythm: (size: ProseSize) => import("purse-styles").StyleElement;
|
|
35
|
+
export declare const proseRhythm: (size: ProseSize, selector?: unknown) => import("purse-styles").StyleElement;
|
|
36
36
|
/**
|
|
37
37
|
* Element styles + vertical rhythm for HTML rendered outside React typography
|
|
38
38
|
* components (TipTap's ProseMirror tree, Streamdown markdown output).
|
package/dist/tokens/prose.js
CHANGED
|
@@ -10,7 +10,6 @@ const scaledPx = (normal, large) => defineVars({
|
|
|
10
10
|
}).px;
|
|
11
11
|
const metrics = {
|
|
12
12
|
sm: {
|
|
13
|
-
blockGap: 16,
|
|
14
13
|
paragraph: { fontSize: scaledPx(14, 17), lineHeight: scaledPx(24, 29) },
|
|
15
14
|
h1: {
|
|
16
15
|
fontSize: scaledPx(30, 36),
|
|
@@ -36,11 +35,10 @@ const metrics = {
|
|
|
36
35
|
marginTop: 20,
|
|
37
36
|
marginBottom: 8,
|
|
38
37
|
},
|
|
39
|
-
listPadding:
|
|
38
|
+
listPadding: 20,
|
|
40
39
|
listItemGap: 4,
|
|
41
40
|
},
|
|
42
41
|
md: {
|
|
43
|
-
blockGap: 20,
|
|
44
42
|
paragraph: { fontSize: scaledPx(16, 19), lineHeight: scaledPx(28, 34) },
|
|
45
43
|
h1: {
|
|
46
44
|
fontSize: scaledPx(36, 43),
|
|
@@ -66,11 +64,10 @@ const metrics = {
|
|
|
66
64
|
marginTop: 24,
|
|
67
65
|
marginBottom: 8,
|
|
68
66
|
},
|
|
69
|
-
listPadding:
|
|
67
|
+
listPadding: 24,
|
|
70
68
|
listItemGap: 8,
|
|
71
69
|
},
|
|
72
70
|
lg: {
|
|
73
|
-
blockGap: 24,
|
|
74
71
|
paragraph: { fontSize: scaledPx(18, 22), lineHeight: scaledPx(32, 38) },
|
|
75
72
|
h1: {
|
|
76
73
|
fontSize: scaledPx(48, 58),
|
|
@@ -96,16 +93,11 @@ const metrics = {
|
|
|
96
93
|
marginTop: 32,
|
|
97
94
|
marginBottom: 8,
|
|
98
95
|
},
|
|
99
|
-
listPadding:
|
|
96
|
+
listPadding: 26,
|
|
100
97
|
listItemGap: 12,
|
|
101
98
|
},
|
|
102
99
|
};
|
|
103
|
-
|
|
104
|
-
const blockGaps = defineVars({
|
|
105
|
-
sm: { default: `${metrics.sm.blockGap}px`, [LARGE_SCALE]: "12px" },
|
|
106
|
-
md: { default: `${metrics.md.blockGap}px`, [LARGE_SCALE]: "16px" },
|
|
107
|
-
lg: { default: `${metrics.lg.blockGap}px`, [LARGE_SCALE]: "20px" },
|
|
108
|
-
});
|
|
100
|
+
const blockGap = "10px";
|
|
109
101
|
// Heading weights follow Tailwind's descending scale, capped at Maui's
|
|
110
102
|
// heaviest weight (700).
|
|
111
103
|
const headingColor = colors.gray[12];
|
|
@@ -163,23 +155,23 @@ const notHeading = ":not(h1):not(h2):not(h3):not(h4)";
|
|
|
163
155
|
* `marginBottom` becomes the "hug" gap after a heading; heading `marginTop`
|
|
164
156
|
* becomes the larger break before it.
|
|
165
157
|
*/
|
|
166
|
-
export const proseRhythm = memoize((size) => {
|
|
158
|
+
export const proseRhythm = memoize((size, selector = "&") => {
|
|
167
159
|
const m = metrics[size];
|
|
168
160
|
return style({
|
|
169
|
-
|
|
170
|
-
marginTop:
|
|
161
|
+
[`${selector} > * + *`]: {
|
|
162
|
+
marginTop: blockGap,
|
|
171
163
|
},
|
|
172
164
|
// Gap after a heading: hug it to the content that follows.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
[`${selector} > h1 + *`]: { marginTop: px(m.h1.marginBottom) },
|
|
166
|
+
[`${selector} > h2 + *`]: { marginTop: px(m.h2.marginBottom) },
|
|
167
|
+
[`${selector} > h3 + *`]: { marginTop: px(m.h3.marginBottom) },
|
|
168
|
+
[`${selector} > h4 + *`]: { marginTop: px(m.h4.marginBottom) },
|
|
177
169
|
// Break before a heading (only when it opens a new section, i.e. it
|
|
178
170
|
// isn't already hugging a preceding heading).
|
|
179
|
-
[
|
|
180
|
-
[
|
|
181
|
-
[
|
|
182
|
-
[
|
|
171
|
+
[`${selector} > ${notHeading} + h1`]: { marginTop: px(m.h1.marginTop) },
|
|
172
|
+
[`${selector} > ${notHeading} + h2`]: { marginTop: px(m.h2.marginTop) },
|
|
173
|
+
[`${selector} > ${notHeading} + h3`]: { marginTop: px(m.h3.marginTop) },
|
|
174
|
+
[`${selector} > ${notHeading} + h4`]: { marginTop: px(m.h4.marginTop) },
|
|
183
175
|
});
|
|
184
176
|
});
|
|
185
177
|
/**
|
|
@@ -194,7 +186,7 @@ export const proseHtml = memoize((size) => {
|
|
|
194
186
|
return style({
|
|
195
187
|
display: "flex",
|
|
196
188
|
flexDirection: "column",
|
|
197
|
-
gap:
|
|
189
|
+
gap: blockGap,
|
|
198
190
|
"& p": {
|
|
199
191
|
...block(m.paragraph),
|
|
200
192
|
fontWeight: 400,
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
10
10
|
<Button>Save</Button>
|
|
11
11
|
<Button variant="quiet">Cancel</Button>
|
|
12
12
|
<Button variant="quiet" variantColor="accent">Quiet accent</Button>
|
|
13
|
+
<Button variant="text">Text action</Button>
|
|
13
14
|
<Button variant="primary" variantColor="blue">Create</Button>
|
|
14
15
|
<Button variant="primary" variantColor="#6366f1">Indigo</Button>
|
|
15
16
|
<Button size="sm">Compact</Button>
|
|
@@ -25,17 +26,19 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
|
|
|
25
26
|
| Prop | Notes |
|
|
26
27
|
| --- | --- |
|
|
27
28
|
| `size` | `"default"` (28px) \| `"sm"` (24px with 10px text and 14px icons). Large/touch scale keeps both at a 40px target |
|
|
28
|
-
| `variant` | `"default"` (raised element) \| `"quiet"` (
|
|
29
|
-
| `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary
|
|
29
|
+
| `variant` | `"default"` (raised element) \| `"quiet"` (transparent, hover wash) \| `"text"` (transparent, color-only hover) \| `"primary"` (solid fill) |
|
|
30
|
+
| `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary defaults to `"accent"`; quiet and text default to gray and tint the label/icon when set |
|
|
30
31
|
| `isDisabled` | React Aria name. Maps to native `disabled`. **No `disabled` React prop** |
|
|
31
|
-
| `isPending` | Shows a current-color indicator beside visible content
|
|
32
|
+
| `isPending` | Shows a current-color indicator beside visible content. Blocks activation and suppresses hover fill while retaining focusability and variant color. Controlled by the caller, not by returned promises |
|
|
32
33
|
| `pendingLabel` | Optional visible string replacing children while pending (e.g. `"Saving…"`). Omit to keep children; provide it to replace an icon with progress text |
|
|
33
34
|
| `onClick` | Still supported. React Aria also exposes `onPress` |
|
|
34
35
|
| `children` | Text is wrapped for cap-height trim; SVG icons sit beside text. Icon-only needs `aria-label` |
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
Use primary for an emphasized action, default for a raised neutral control, quiet for a low-emphasis action, and text for an inline-style action without a hover fill. Let Button manage its palette and state colors rather than rebuilding the fill or shadow. For exact treatment, inspect `src/components/Button.tsx` in the installed package as described in [design conventions](../design_conventions.md).
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Prefer plain labels (optionally alongside icons) so Button controls their foreground; see the [composition rule](../design_conventions.md#composition).
|
|
40
|
+
|
|
41
|
+
The text variant removes horizontal spacing and surface chrome while keeping the control height and focus ring. Use it when an action should align directly with surrounding text; hover changes the foreground, not the fill.
|
|
39
42
|
|
|
40
43
|
`useButton(props)` is exported for custom focus-tracked buttons; prefer `Button`.
|
|
41
44
|
|
|
@@ -64,7 +64,7 @@ Controlled: `selected` + `onChange`. **`label` is required.**
|
|
|
64
64
|
|
|
65
65
|
**TextField** — `textFieldStyles`
|
|
66
66
|
- `root` — field wrapper around the input and error.
|
|
67
|
-
- `input({ size, keyboardHint })` — RAC `Input`; `:
|
|
67
|
+
- `input({ size, keyboardHint })` — RAC `Input`; `:disabled`, `aria-invalid`, `:focus-visible`, `::placeholder`.
|
|
68
68
|
- `frame` — relative wrapper around the input when a keyboard hint is shown.
|
|
69
69
|
- `keyboardHint` — Maui `Kbd variant="quiet"` inside `frame`.
|
|
70
70
|
- `error` — RAC `FieldError`.
|
|
@@ -76,7 +76,7 @@ Controlled: `selected` + `onChange`. **`label` is required.**
|
|
|
76
76
|
|
|
77
77
|
**NumberField** — `numberFieldStyles`
|
|
78
78
|
- `root` — RAC `Group`; `:has(:focus-visible)`, `:has(input:disabled)`, `:has(input[aria-invalid='true'])`; styles the stepper `button`s inside a `.number-stepper` wrapper, so it needs that markup.
|
|
79
|
-
- `input` — RAC `Input`; `:
|
|
79
|
+
- `input` — RAC `Input`; `:disabled`, `::placeholder`.
|
|
80
80
|
|
|
81
81
|
**QuietTextField** — `quietTextFieldStyles`
|
|
82
82
|
- `input({ keyboardHint })` — RAC `Input`; `:hover`, `:disabled`, `aria-invalid`, `:focus-visible`, `::placeholder`.
|
|
@@ -23,7 +23,7 @@ Exactly two children on `MenuTrigger`: **trigger**, then **menu**. Use Maui `But
|
|
|
23
23
|
</MenuTrigger>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`isDisabled` on the trigger button blocks open. Escape closes and focus returns to the trigger (`aria-expanded` tracks open state).
|
|
26
|
+
`isDisabled` on the trigger button blocks open. Escape closes and focus returns to the trigger (`aria-expanded` tracks open state).
|
|
27
27
|
|
|
28
28
|
`placement` defaults to `"bottom start"`. Popover uses `shadow.strong` via `CollectionPopover`.
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Maui design conventions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use the matching skill references for Maui's design choices, composition rules, and reusable token patterns. Inspect the installed package's source for tactical questions about props, defaults, state styling, or exact values. Source ships under `node_modules/maui/src/` with the documented npm alias, or `node_modules/@tanishqkancharla/maui/src/` without it. Follow components into their token definitions as needed; current implementation is not a guarantee of contrast or design intent.
|
|
4
4
|
|
|
5
5
|
Patterns, demo apps, and the gallery `Panel` preview frame are not package components. `Panel` is not on the `"maui"` barrel, in `src/components`, or in the JSX editor catalog. Rebuild reference recipes with barrel exports such as `Button`, `Flex`, and `text(...)`.
|
|
6
6
|
|
|
@@ -24,32 +24,73 @@ State styles key off React Aria attributes (`data-hovered`, `data-selected`, `da
|
|
|
24
24
|
|
|
25
25
|
## Composition
|
|
26
26
|
|
|
27
|
-
- Use Maui's `Text` component
|
|
27
|
+
- Use Maui's `Text` component for standalone inline typography; see [Text](components/text.md). `Text` and `text(...)` set an explicit color by default, overriding inherited variant, selected, or disabled colors. Prefer plain Button labels so the Button controls its foreground. Keep semantic elements such as headings, paragraphs, labels, and links for their respective roles.
|
|
28
28
|
- Hover backgrounds have no transitions. Hover fills such as `backgroundColor.elementHover`, quiet-button washes, and list or row highlights snap instantly. Do not animate `background` or `background-color` on hover with `motion.standard(...)` or a CSS `transition`. Other motion, including tooltips and transforms, is allowed.
|
|
29
29
|
- Simple apps default to a prose column: `style(proseContainerStyle, { width: "100%", marginInline: "auto" })`. `sizing.contentWidth` is that same style. Use it for single-column tools, settings, forms, and reading layouts. Multi-pane or full-bleed apps such as inboxes, calendars, and IDEs are exceptions.
|
|
30
30
|
- Design an intentional empty composition for every list, inbox, search result, or collection. Include copy and, when useful, an action. Do not leave a blank panel.
|
|
31
31
|
|
|
32
|
+
## Decorative edge strips
|
|
33
|
+
|
|
34
|
+
When a rounded surface uses a colored edge for emphasis, use a full-height rectangular strip inside a `position: relative; overflow: hidden` container. Keep the radius on the container, not the strip: a CSS left border follows the corners and curls inward. This is a composition recipe for custom UI, not a new component or a change to existing component borders. Keep using `border()` for outlines and dividers.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const surface = style(radius.md, spacing.padding({ all: 6 }), {
|
|
38
|
+
position: "relative",
|
|
39
|
+
overflow: "hidden",
|
|
40
|
+
backgroundColor: colors.orange[3],
|
|
41
|
+
})
|
|
42
|
+
const strip = style({
|
|
43
|
+
position: "absolute",
|
|
44
|
+
insetBlock: 0,
|
|
45
|
+
left: 0,
|
|
46
|
+
width: "3px",
|
|
47
|
+
backgroundColor: colors.orange[9],
|
|
48
|
+
pointerEvents: "none",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function DecoratedSurface() {
|
|
52
|
+
const surfaceClass = useStyles(surface)
|
|
53
|
+
const stripClass = useStyles(strip)
|
|
54
|
+
return (
|
|
55
|
+
<div className={surfaceClass}>
|
|
56
|
+
<div aria-hidden="true" className={stripClass} />
|
|
57
|
+
<Text>Surface content</Text>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
32
63
|
## Shadows
|
|
33
64
|
|
|
34
|
-
Use `shadow.control` for interactive control surfaces.
|
|
35
|
-
highlights with a compact outer edge. Use the three-level elevation scale for
|
|
36
|
-
other raised surfaces:
|
|
65
|
+
Use `shadow.control` for interactive control surfaces. Use the three-level elevation scale for other raised surfaces:
|
|
37
66
|
|
|
38
67
|
- `shadow.subtle` — cards and ordinary raised surfaces
|
|
39
|
-
- `shadow.medium` —
|
|
68
|
+
- `shadow.medium` — larger floating panels
|
|
40
69
|
- `shadow.strong` — dropdowns, popovers, and dominant overlays
|
|
41
70
|
|
|
42
|
-
|
|
71
|
+
These shadows include their own edge or ring. Do not also apply `border()`, `borderColor.outline`, or another ring to the same element. Not every floating component uses elevation: reuse its exported styles rather than inferring a shadow from its placement.
|
|
43
72
|
|
|
44
73
|
Buttons and form-control surfaces use `shadow.control` by default. For compound controls, apply it once to the outer control boundary instead of to every internal button or segment.
|
|
45
74
|
|
|
46
75
|
## Focus
|
|
47
76
|
|
|
48
|
-
`focusRing()`
|
|
77
|
+
Use `focusRing()` for Maui's theme-aware keyboard-focus treatment rather than adding a competing outline or box shadow.
|
|
78
|
+
|
|
79
|
+
**Control safe area.** Input and form-control shadows paint outside the component's layout box; reserve room for the resting, hovered, and keyboard-focused states, even when hover does not change the background. Use `spacing.padding(4)` as a starting inset between controls and clipping edges, and leave gaps between neighboring controls. Keep overflow visible on immediate wrappers; when a panel, rounded container, or scroll viewport must clip, put the safe inset inside that clipping boundary, not outside it as margin. Check the first and last controls at scroll edges, including checkbox, radio, switch, and slider focus states. Do not remove shadows or focus rings to fit a tight layout.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// Keep the control's visual overflow inside the scrolling boundary.
|
|
83
|
+
<div style={{ overflow: "auto" }}>
|
|
84
|
+
<Flex column padding={4} gap={4}>
|
|
85
|
+
<TextField aria-label="Name" />
|
|
86
|
+
<Select aria-label="Plan">{/* options */}</Select>
|
|
87
|
+
</Flex>
|
|
88
|
+
</div>
|
|
89
|
+
```
|
|
49
90
|
|
|
50
91
|
## Layout utilities
|
|
51
92
|
|
|
52
|
-
`Flex` and `Gap` take spacing scale steps
|
|
93
|
+
`Flex` and `Gap` take spacing scale steps, not raw pixels. For example, `<Flex row gap={4} padding={4}>` uses the same token for its gap and inset.
|
|
53
94
|
|
|
54
95
|
Put padding on `Flex` with `padding` or `p`, `px`, `py`, `pt`, `pr`, `pb`, and `pl`; there is no `Padding` component. `alignItems` and `justifyContent` use the same tokens as `flex()` (`start`, `center`, `end`, and others; `justifyContent` also supports `between`, `around`, and `evenly`). `background` is a surface token such as `app`, `element`, or `accent`. `Spacer` grows to fill leftover flex space. `Divider` is a horizontal rule.
|
|
55
96
|
|
|
@@ -56,12 +56,14 @@ function Row({ children }: { children: React.ReactNode }) {
|
|
|
56
56
|
|
|
57
57
|
`colors` is Radix Scales with a brand `accent` (teal in light, violet in dark). Each scale has steps **1–12**. Alpha scales are `colors.grayAlpha`, `colors.accentAlpha`, `colors.blueAlpha`, …
|
|
58
58
|
|
|
59
|
+
The scale roles below are a starting point, not a contrast guarantee or a state recipe for every component. Use text roles for labels rather than copying a solid fill's step. Check the actual foreground/background pair in light and dark themes, including selected and disabled states; preserve non-color cues for state and status.
|
|
60
|
+
|
|
59
61
|
| Steps | Use |
|
|
60
62
|
| --- | --- |
|
|
61
63
|
| 1–2 | App / subtle surfaces |
|
|
62
64
|
| 3–5 | Interactive washes |
|
|
63
65
|
| 6–8 | Borders / strong lines |
|
|
64
|
-
| 9–10 | Solid fills
|
|
66
|
+
| 9–10 | Solid fills |
|
|
65
67
|
| 11–12 | Text (11 secondary, 12 primary) |
|
|
66
68
|
|
|
67
69
|
```ts
|
|
@@ -74,6 +76,8 @@ colors.blueAlpha[8]
|
|
|
74
76
|
|
|
75
77
|
`colorNames` is `"accent"` plus every Radix palette name. `paletteNames` omits `"accent"`. `"blue"` as a `variantColor` is the Radix blue scale, not CSS `blue`.
|
|
76
78
|
|
|
79
|
+
Brand accent, semantic status, and decorative identity serve different roles. Prefer a component's variant or tone before assembling custom colors; follow the matching component's source when composing similar UI. For example, [Banner](components/banner.md) owns status tones, while [Avatar](components/avatar.md) uses paired decorative colors, not status meanings.
|
|
80
|
+
|
|
77
81
|
Prefer semantic surfaces over raw scale steps for chrome:
|
|
78
82
|
|
|
79
83
|
```ts
|
|
@@ -81,14 +85,14 @@ import { background, backgroundColor } from "maui"
|
|
|
81
85
|
|
|
82
86
|
backgroundColor.app // page
|
|
83
87
|
backgroundColor.element // raised control / card
|
|
84
|
-
backgroundColor.elementHover //
|
|
85
|
-
backgroundColor.elementActive //
|
|
88
|
+
backgroundColor.elementHover // hovered element surface
|
|
89
|
+
backgroundColor.elementActive // active element surface
|
|
86
90
|
background.app // style objects of the same values
|
|
87
91
|
background.element
|
|
88
92
|
background.accent // accent[9]
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
Alpha washes depend on their backdrop. A quiet control's transparent overlay is not interchangeable with `elementHover`, which is precomposed over the element surface. Reuse the matching component styles rather than recreating its wash. Hover fills snap instantly; see [design conventions](design_conventions.md#composition).
|
|
92
96
|
|
|
93
97
|
## Text
|
|
94
98
|
|
|
@@ -115,7 +119,7 @@ text({
|
|
|
115
119
|
|
|
116
120
|
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
121
|
|
|
118
|
-
- `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white.
|
|
122
|
+
- `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white. `onAccent` is literal white, not an adaptive foreground for every solid fill.
|
|
119
123
|
- UI sans: `fontFamily` (system ui-sans-serif stack). Mono: Commit Mono with `ss05` smart kerning (`monoFontStyle` / `monospace`).
|
|
120
124
|
- `tabular: true` → `font-variant-numeric: tabular-nums`. NumberField, TableHead, and TableCell turn this on by default. Body copy and Prose stay off.
|
|
121
125
|
- `baseTextStyle` is md / 400 / highContrast — already on `html, body`.
|
|
@@ -178,13 +182,12 @@ shadowVars.control | .subtle | .medium | .strong // raw box-shadow strings
|
|
|
178
182
|
tintedSubtle(color) // subtle ring+blur tinted from a fill
|
|
179
183
|
```
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
already includes a 1px ring. **Do not also `border()` the same element.**
|
|
185
|
+
For elevation choice, compound control boundaries, and avoiding doubled rings, follow the [shadow conventions](design_conventions.md#shadows).
|
|
183
186
|
|
|
184
187
|
`border(sides, color)`:
|
|
185
188
|
|
|
186
189
|
- `sides`: `[]` for all sides, or `"top" | "right" | "bottom" | "left"`
|
|
187
|
-
- `color`: `"border"` (
|
|
190
|
+
- `color`: `"border"` (subtle separator) | `"outline"` (stronger edge) | `"accent"` (accent edge)
|
|
188
191
|
|
|
189
192
|
`borderColor.border` / `borderColor.outline` are the raw color strings (hairlines, `borderBottom`).
|
|
190
193
|
|
|
@@ -195,20 +198,14 @@ already includes a 1px ring. **Do not also `border()` the same element.**
|
|
|
195
198
|
## Motion
|
|
196
199
|
|
|
197
200
|
```ts
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
overlayMotion //
|
|
201
|
+
motion.standard("opacity", "transform") // shared transition style
|
|
202
|
+
motionDurationMs // standard duration
|
|
203
|
+
motionStreamDurationMs // streaming text duration
|
|
204
|
+
motionEasing // standard easing
|
|
205
|
+
overlayMotion // overlay and reduced-motion presets
|
|
203
206
|
```
|
|
204
207
|
|
|
205
|
-
Use `motion.standard` for opacity, transform, box-shadow, color
|
|
206
|
-
|
|
207
|
-
`overlayMotion` defines tooltip enter (120ms entrance curve), tooltip exit
|
|
208
|
-
(80ms ease-in), dialog enter (200ms entrance curve), dialog exit (120ms
|
|
209
|
-
ease-in), backdrop (100ms), and opacity-only reduced motion (80ms). Crossfade
|
|
210
|
-
enter is a 0.3s spring (`bounce` 0.2); exit uses `motionDurationMs` /
|
|
211
|
-
ease-in-out. Streaming markdown uses `motionStreamDurationMs`.
|
|
208
|
+
Use `motion.standard` for opacity, transform, box-shadow, and color, not hover fills; see [design conventions](design_conventions.md#composition). Reuse `overlayMotion` for custom overlay enter/exit and reduced-motion handling; inspect the matching component for how it applies those presets.
|
|
212
209
|
|
|
213
210
|
## Sizing and icons
|
|
214
211
|
|
|
@@ -229,7 +226,7 @@ Stroke/fill is `currentColor`. Pass `size` on the icon component; optional `widt
|
|
|
229
226
|
|
|
230
227
|
## Avatar tokens
|
|
231
228
|
|
|
232
|
-
`avatar.green` / `avatar.orange` / `avatar.pink` each
|
|
229
|
+
`avatar.green` / `avatar.orange` / `avatar.pink` each provide a paired `background` and `foreground`; keep the pair together when using these decorative tokens. Prefer `Avatar` for identity UI rather than choosing its colors manually.
|
|
233
230
|
|
|
234
231
|
## Prose tokens
|
|
235
232
|
|
|
@@ -1463,7 +1463,7 @@ const miniDayClass = style(
|
|
|
1463
1463
|
text({ size: "xs", fontWeight: 400, color: "highContrast" }),
|
|
1464
1464
|
radius.circle,
|
|
1465
1465
|
focusRing(),
|
|
1466
|
-
motion.standard("
|
|
1466
|
+
motion.standard("color"),
|
|
1467
1467
|
{
|
|
1468
1468
|
display: "grid",
|
|
1469
1469
|
placeItems: "center",
|
|
@@ -264,11 +264,11 @@ export const catalog: CatalogComponent[] = [
|
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
name: "Button",
|
|
267
|
-
info: "Button. default is raised, quiet
|
|
267
|
+
info: "Button. default is raised, quiet adds a hover wash, text changes only color on hover, primary fills with accent 9. Works as a MenuTrigger child.",
|
|
268
268
|
html: true,
|
|
269
269
|
attributes: [
|
|
270
270
|
{ name: "size", values: ["default", "sm"] },
|
|
271
|
-
{ name: "variant", values: ["default", "quiet", "primary"] },
|
|
271
|
+
{ name: "variant", values: ["default", "quiet", "primary", "text"] },
|
|
272
272
|
{
|
|
273
273
|
name: "variantColor",
|
|
274
274
|
info: "Palette name (colors[name], primary step 9) or an opaque hex / rgb() fill. Quiet stays transparent; color tints the label. Hover mixes Alpha[9] at 6%/9%.",
|
|
@@ -23,6 +23,13 @@ describe("collectJsxDiagnosticsFromSource", () => {
|
|
|
23
23
|
expect(diagnostics).toEqual([])
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
test("accepts a text Button", () => {
|
|
27
|
+
const diagnostics = collectJsxDiagnosticsFromSource(
|
|
28
|
+
`<Button variant="text" variantColor="blue">Connect</Button>`,
|
|
29
|
+
)
|
|
30
|
+
expect(diagnostics).toEqual([])
|
|
31
|
+
})
|
|
32
|
+
|
|
26
33
|
test("accepts a small Button", () => {
|
|
27
34
|
const diagnostics = collectJsxDiagnosticsFromSource(
|
|
28
35
|
`<Button size="sm">Compact</Button>`,
|
|
@@ -54,7 +61,7 @@ describe("collectJsxDiagnosticsFromSource", () => {
|
|
|
54
61
|
)
|
|
55
62
|
expect(diagnostics).toHaveLength(1)
|
|
56
63
|
expect(diagnostics[0]?.message).toBe(
|
|
57
|
-
`Type '"solid"' is not assignable to type '"default" | "quiet" | "primary"'.`,
|
|
64
|
+
`Type '"solid"' is not assignable to type '"default" | "quiet" | "primary" | "text"'.`,
|
|
58
65
|
)
|
|
59
66
|
})
|
|
60
67
|
|
|
@@ -66,9 +73,9 @@ describe("collectJsxDiagnosticsFromSource", () => {
|
|
|
66
73
|
</Flex>`,
|
|
67
74
|
)
|
|
68
75
|
expect(formatTypeErrorBanners(diagnostics)).toEqual([
|
|
69
|
-
`Line 2: Type '"solid"' is not assignable to type '"default" | "quiet" | "primary"'.`,
|
|
76
|
+
`Line 2: Type '"solid"' is not assignable to type '"default" | "quiet" | "primary" | "text"'.`,
|
|
70
77
|
`Line 2: Property 'disabled' does not exist on Button.`,
|
|
71
|
-
`Line 3: Type '"ghost"' is not assignable to type '"default" | "quiet" | "primary"'.`,
|
|
78
|
+
`Line 3: Type '"ghost"' is not assignable to type '"default" | "quiet" | "primary" | "text"'.`,
|
|
72
79
|
])
|
|
73
80
|
})
|
|
74
81
|
|
|
@@ -30,7 +30,7 @@ import { spacing } from "../tokens/spacing"
|
|
|
30
30
|
import { text } from "../tokens/text"
|
|
31
31
|
import { memoize } from "../utils/memoize"
|
|
32
32
|
|
|
33
|
-
export type ButtonVariant = "default" | "quiet" | "primary"
|
|
33
|
+
export type ButtonVariant = "default" | "quiet" | "primary" | "text"
|
|
34
34
|
export type ButtonSize = "default" | "sm"
|
|
35
35
|
|
|
36
36
|
const buttonShadow = shadowVars.control
|
|
@@ -173,18 +173,27 @@ const buttonClass = style(buttonBaseClass, {
|
|
|
173
173
|
},
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
+
const textButtonSpacingClass = style({
|
|
177
|
+
"&, &:has(> svg), &:has([data-button-content] > svg), &:has(> svg:first-child:not(:only-child)), &:has([data-button-content] > svg:first-child:not(:only-child)), &:has(> svg:last-child:not(:only-child)), &:has([data-button-content] > svg:last-child:not(:only-child)), &:has(> svg:only-child), &:has([data-button-content] > svg:only-child), &[data-pending]":
|
|
178
|
+
{
|
|
179
|
+
padding: 0,
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
176
183
|
function paletteAlpha(name: ColorName): ColorScale {
|
|
177
184
|
return colors[`${name}Alpha`]
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
const quietClass = memoize((color: string, hoverColor: string, wash
|
|
187
|
+
const quietClass = memoize((color: string, hoverColor: string, wash?: string) =>
|
|
181
188
|
style(buttonBaseClass, focusRing("&:focus-visible"), {
|
|
182
189
|
color,
|
|
183
190
|
backgroundColor: "transparent",
|
|
184
191
|
boxShadow: "none",
|
|
185
192
|
"&:hover:not(:disabled):not([data-pending])": {
|
|
186
193
|
color: hoverColor,
|
|
187
|
-
backgroundColor:
|
|
194
|
+
backgroundColor: wash
|
|
195
|
+
? surfaceWash(wash, surfaceMixPercent.hover)
|
|
196
|
+
: "transparent",
|
|
188
197
|
},
|
|
189
198
|
"&:disabled": disabledQuiet,
|
|
190
199
|
}),
|
|
@@ -275,7 +284,7 @@ const primaryClass = memoize(
|
|
|
275
284
|
)
|
|
276
285
|
|
|
277
286
|
const coloredButtonClass = memoize(
|
|
278
|
-
(variant: "primary" | "quiet", color: ButtonVariantColor) => {
|
|
287
|
+
(variant: "primary" | "quiet" | "text", color: ButtonVariantColor) => {
|
|
279
288
|
if (isCssColor(color)) {
|
|
280
289
|
const fill = opaqueColor(color)
|
|
281
290
|
if (variant === "primary") {
|
|
@@ -283,19 +292,27 @@ const coloredButtonClass = memoize(
|
|
|
283
292
|
return primaryClass(fill, onSolidText(fill), fillShade(fill, 0, 0.35))
|
|
284
293
|
}
|
|
285
294
|
|
|
286
|
-
return quietClass(
|
|
295
|
+
return quietClass(
|
|
296
|
+
fill,
|
|
297
|
+
darkerFill(fill),
|
|
298
|
+
variant === "quiet" ? fill : undefined,
|
|
299
|
+
)
|
|
287
300
|
}
|
|
288
301
|
|
|
289
302
|
const scale = paletteFill(color)
|
|
290
303
|
if (variant === "primary") {
|
|
291
304
|
return primaryClass(
|
|
292
305
|
scale[9],
|
|
293
|
-
darkTextOnSolid.has(color) ? scale[
|
|
306
|
+
darkTextOnSolid.has(color) ? onSolidText(scale[9]) : "white",
|
|
294
307
|
paletteAlpha(color)[6],
|
|
295
308
|
)
|
|
296
309
|
}
|
|
297
310
|
|
|
298
|
-
return quietClass(
|
|
311
|
+
return quietClass(
|
|
312
|
+
scale[11],
|
|
313
|
+
scale[12],
|
|
314
|
+
variant === "quiet" ? paletteAlpha(color)[9] : undefined,
|
|
315
|
+
)
|
|
299
316
|
},
|
|
300
317
|
)
|
|
301
318
|
|
|
@@ -365,6 +382,7 @@ export const buttonStyles = {
|
|
|
365
382
|
style(
|
|
366
383
|
buttonVariantClass(variant, variantColor),
|
|
367
384
|
size === "sm" && smallButtonClass,
|
|
385
|
+
variant === "text" && textButtonSpacingClass,
|
|
368
386
|
),
|
|
369
387
|
),
|
|
370
388
|
label: buttonTextClass,
|
|
@@ -510,6 +528,9 @@ function buttonVariantClass(
|
|
|
510
528
|
if (variant === "primary") {
|
|
511
529
|
return coloredButtonClass("primary", variantColor ?? "accent")
|
|
512
530
|
}
|
|
531
|
+
if (variant === "text") {
|
|
532
|
+
return coloredButtonClass("text", variantColor ?? "gray")
|
|
533
|
+
}
|
|
513
534
|
if (variant === "quiet" && variantColor) {
|
|
514
535
|
return coloredButtonClass("quiet", variantColor)
|
|
515
536
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EditorContent, type EditorContentProps } from "@tiptap/react"
|
|
2
2
|
import { style, useStyles } from "purse-styles"
|
|
3
3
|
import { colors } from "../tokens/colors"
|
|
4
|
-
import { proseHtml, type ProseSize } from "../tokens/prose"
|
|
4
|
+
import { proseHtml, proseRhythm, type ProseSize } from "../tokens/prose"
|
|
5
5
|
import { cls } from "../utils/cls"
|
|
6
6
|
|
|
7
7
|
export type EditorProps = Omit<EditorContentProps, "size"> & {
|
|
@@ -15,7 +15,11 @@ export type EditorProps = Omit<EditorContentProps, "size"> & {
|
|
|
15
15
|
* through TipTap's editorProps.attributes.
|
|
16
16
|
*/
|
|
17
17
|
export function Editor({ size = "md", className, ...props }: EditorProps) {
|
|
18
|
-
const surfaceClassName = useStyles(
|
|
18
|
+
const surfaceClassName = useStyles(
|
|
19
|
+
editorClass,
|
|
20
|
+
proseHtml(size),
|
|
21
|
+
proseRhythm(size, "& > .ProseMirror"),
|
|
22
|
+
)
|
|
19
23
|
return (
|
|
20
24
|
<EditorContent
|
|
21
25
|
{...props}
|
|
@@ -33,7 +37,7 @@ const editorClass = style({
|
|
|
33
37
|
// proseHtml styles descendants; its block rhythm belongs on the document.
|
|
34
38
|
display: "flex",
|
|
35
39
|
flexDirection: "column",
|
|
36
|
-
gap:
|
|
40
|
+
gap: 0,
|
|
37
41
|
},
|
|
38
42
|
"& .ProseMirror p.is-editor-empty:first-child::before": {
|
|
39
43
|
color: colors.gray[9],
|
package/src/components/Input.tsx
CHANGED
|
@@ -46,9 +46,6 @@ const inputClass = style(
|
|
|
46
46
|
color: colors.gray[12],
|
|
47
47
|
border: "none",
|
|
48
48
|
background: backgroundColor.element,
|
|
49
|
-
"&:hover:not(:disabled)": {
|
|
50
|
-
background: backgroundColor.elementHover,
|
|
51
|
-
},
|
|
52
49
|
"&:disabled": {
|
|
53
50
|
color: colors.gray[8],
|
|
54
51
|
background: colors.gray[2],
|
|
@@ -376,9 +373,6 @@ const numberInputClass = style(
|
|
|
376
373
|
border: "none",
|
|
377
374
|
outline: "none",
|
|
378
375
|
background: "transparent",
|
|
379
|
-
"&:hover:not(:disabled)": {
|
|
380
|
-
background: backgroundColor.elementHover,
|
|
381
|
-
},
|
|
382
376
|
"&::placeholder": {
|
|
383
377
|
fontStyle: "italic",
|
|
384
378
|
color: colors.gray[8],
|
|
@@ -41,9 +41,12 @@ const item = style(
|
|
|
41
41
|
background: backgroundColor.elementHover,
|
|
42
42
|
},
|
|
43
43
|
"&[data-selected]": {
|
|
44
|
-
color: colors.accent[
|
|
44
|
+
color: colors.accent[11],
|
|
45
45
|
fontWeight: 500,
|
|
46
46
|
},
|
|
47
|
+
"&[data-selected]:not([data-disabled]):is(:hover, [data-hovered], [data-focused])": {
|
|
48
|
+
color: colors.accent[12],
|
|
49
|
+
},
|
|
47
50
|
"&[data-selected]::after": {
|
|
48
51
|
content: '"✓"',
|
|
49
52
|
position: "absolute",
|
|
@@ -63,7 +63,7 @@ const thumb = style(focusRing("&[data-focus-visible]", shadowVars.control), {
|
|
|
63
63
|
width: sliderSize.thumb,
|
|
64
64
|
height: sliderSize.thumb,
|
|
65
65
|
borderRadius: "100%",
|
|
66
|
-
background:
|
|
66
|
+
background: "#ffffff",
|
|
67
67
|
boxShadow: shadowVars.control,
|
|
68
68
|
"&[data-dragging]": {
|
|
69
69
|
background: colors.accent[11],
|
package/src/components/Table.tsx
CHANGED
|
@@ -111,7 +111,7 @@ const row = style({
|
|
|
111
111
|
"&[data-selection-mode][data-selected] td": {
|
|
112
112
|
backgroundColor: colors.accent[3],
|
|
113
113
|
},
|
|
114
|
-
"&[data-disabled]": {
|
|
114
|
+
"&[data-disabled] td": {
|
|
115
115
|
color: colors.gray[8],
|
|
116
116
|
},
|
|
117
117
|
})
|
|
@@ -172,7 +172,7 @@ const selectionCheck = style({
|
|
|
172
172
|
height: "10px",
|
|
173
173
|
paddingTop: "0.5px",
|
|
174
174
|
paddingLeft: "0.5px",
|
|
175
|
-
fill:
|
|
175
|
+
fill: "white",
|
|
176
176
|
opacity: 0,
|
|
177
177
|
transform: "scale(0)",
|
|
178
178
|
})
|
|
@@ -180,6 +180,10 @@ const selectionCheck = style({
|
|
|
180
180
|
const selectionCheckbox = style(
|
|
181
181
|
flex({ alignItems: "center", justifyContent: "center" }),
|
|
182
182
|
focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control),
|
|
183
|
+
focusRing(
|
|
184
|
+
`&[data-selected][data-focus-visible] ${selectionBox}, &[data-indeterminate][data-focus-visible] ${selectionBox}`,
|
|
185
|
+
shadowVars.selectedControl,
|
|
186
|
+
),
|
|
183
187
|
{
|
|
184
188
|
position: "relative",
|
|
185
189
|
width: "fit-content",
|
|
@@ -201,6 +205,7 @@ const selectionCheckbox = style(
|
|
|
201
205
|
[`&[data-selected] ${selectionBox}, &[data-indeterminate] ${selectionBox}`]:
|
|
202
206
|
{
|
|
203
207
|
backgroundColor: colors.accent[9],
|
|
208
|
+
boxShadow: shadowVars.selectedControl,
|
|
204
209
|
},
|
|
205
210
|
[`&[data-hovered][data-selected] ${selectionBox}, &[data-hovered][data-indeterminate] ${selectionBox}`]:
|
|
206
211
|
{
|
|
@@ -18,8 +18,11 @@ export const navigationItem = style(
|
|
|
18
18
|
background: backgroundColor.elementHover,
|
|
19
19
|
},
|
|
20
20
|
"&[aria-current='page']": {
|
|
21
|
-
color: colors.accent[
|
|
21
|
+
color: colors.accent[11],
|
|
22
22
|
fontWeight: 500,
|
|
23
23
|
},
|
|
24
|
+
"&[aria-current='page']:hover": {
|
|
25
|
+
color: colors.accent[12],
|
|
26
|
+
},
|
|
24
27
|
},
|
|
25
28
|
)
|
|
@@ -36,11 +36,11 @@ const borderExamples = [
|
|
|
36
36
|
use: "Pane headers and horizontal section breaks.",
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
label:
|
|
39
|
+
label: "Clipped left accent",
|
|
40
40
|
sides: ["left"] as BorderSide[],
|
|
41
41
|
color: "accent" as const,
|
|
42
42
|
edgeWidth: "2px",
|
|
43
|
-
use: "
|
|
43
|
+
use: "Decorative emphasis on rounded surfaces.",
|
|
44
44
|
},
|
|
45
45
|
] as const
|
|
46
46
|
|
|
@@ -119,17 +119,46 @@ export function BordersTokenPage() {
|
|
|
119
119
|
</TableBody>
|
|
120
120
|
</Table>
|
|
121
121
|
|
|
122
|
-
<CodeBlock lang="
|
|
122
|
+
<CodeBlock lang="tsx">{`const panel = style(
|
|
123
123
|
background.element,
|
|
124
124
|
border([], "outline"),
|
|
125
125
|
radius.md,
|
|
126
126
|
)
|
|
127
127
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
const accentedSurface = style(
|
|
129
|
+
radius.md,
|
|
130
|
+
{
|
|
131
|
+
position: "relative",
|
|
132
|
+
overflow: "hidden",
|
|
133
|
+
padding: "16px",
|
|
134
|
+
backgroundColor: colors.gray[2],
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
const strip = style({
|
|
138
|
+
position: "absolute",
|
|
139
|
+
insetBlock: 0,
|
|
140
|
+
left: 0,
|
|
141
|
+
width: "2px",
|
|
142
|
+
backgroundColor: colors.accent[8],
|
|
143
|
+
pointerEvents: "none",
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
function DecoratedSurface() {
|
|
147
|
+
const surfaceClass = useStyles(accentedSurface)
|
|
148
|
+
const stripClass = useStyles(strip)
|
|
149
|
+
return (
|
|
150
|
+
<div className={surfaceClass}>
|
|
151
|
+
<div aria-hidden="true" className={stripClass} />
|
|
152
|
+
<Text>Surface content</Text>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}`}</CodeBlock>
|
|
156
|
+
<P>
|
|
157
|
+
For left-edge emphasis, clip a full-height rectangular accent inside
|
|
158
|
+
the rounded container instead of using a left border. The container
|
|
159
|
+
keeps its rounding without curling the accent inward. This is a
|
|
160
|
+
composition recipe, not a change to existing component borders.
|
|
161
|
+
</P>
|
|
133
162
|
|
|
134
163
|
<Panel style={{ marginTop: "16px" }}>
|
|
135
164
|
<div
|
|
@@ -166,20 +195,13 @@ function BorderExample(props: {
|
|
|
166
195
|
color: BorderColor
|
|
167
196
|
edgeWidth?: string
|
|
168
197
|
}) {
|
|
198
|
+
const leftAccent = props.sides.length === 1 && props.sides[0] === "left"
|
|
169
199
|
const className = useStyles(
|
|
170
|
-
border(props.sides, props.color),
|
|
171
|
-
props.edgeWidth
|
|
172
|
-
? style(
|
|
173
|
-
Object.fromEntries(
|
|
174
|
-
props.sides.map((side) => [
|
|
175
|
-
`border${capitalize(side)}Width`,
|
|
176
|
-
props.edgeWidth,
|
|
177
|
-
]),
|
|
178
|
-
),
|
|
179
|
-
)
|
|
180
|
-
: undefined,
|
|
200
|
+
!leftAccent && border(props.sides, props.color),
|
|
181
201
|
radius.md,
|
|
182
202
|
style({
|
|
203
|
+
position: "relative",
|
|
204
|
+
overflow: "hidden",
|
|
183
205
|
background: colors.gray[2],
|
|
184
206
|
padding: "16px",
|
|
185
207
|
textAlign: "center",
|
|
@@ -190,15 +212,26 @@ function BorderExample(props: {
|
|
|
190
212
|
|
|
191
213
|
return (
|
|
192
214
|
<div>
|
|
193
|
-
<div className={className}>
|
|
215
|
+
<div className={className}>
|
|
216
|
+
{leftAccent && (
|
|
217
|
+
<div
|
|
218
|
+
aria-hidden="true"
|
|
219
|
+
style={{
|
|
220
|
+
position: "absolute",
|
|
221
|
+
insetBlock: 0,
|
|
222
|
+
left: 0,
|
|
223
|
+
width: props.edgeWidth ?? "1px",
|
|
224
|
+
backgroundColor: colors.accent[8],
|
|
225
|
+
pointerEvents: "none",
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
{props.label}
|
|
230
|
+
</div>
|
|
194
231
|
</div>
|
|
195
232
|
)
|
|
196
233
|
}
|
|
197
234
|
|
|
198
|
-
function capitalize(value: string) {
|
|
199
|
-
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`
|
|
200
|
-
}
|
|
201
|
-
|
|
202
235
|
function SingleEdgeExample() {
|
|
203
236
|
const className = useStyles(
|
|
204
237
|
border(["bottom"], "border"),
|
|
@@ -35,7 +35,7 @@ export function ButtonsPage() {
|
|
|
35
35
|
return (
|
|
36
36
|
<Prose style={{ marginBottom: "32px" }}>
|
|
37
37
|
<H2>Buttons</H2>
|
|
38
|
-
<H3>
|
|
38
|
+
<H3>Default</H3>
|
|
39
39
|
<Flex row alignItems="center" gap={4}>
|
|
40
40
|
<Button>Button</Button>
|
|
41
41
|
<Button onClick={() => setDialogOpen(true)}>Open Dialog</Button>
|
|
@@ -221,6 +221,35 @@ export function ButtonsPage() {
|
|
|
221
221
|
</Button>
|
|
222
222
|
</Flex>
|
|
223
223
|
|
|
224
|
+
<H3>Text</H3>
|
|
225
|
+
<P>
|
|
226
|
+
<Code>variant="text"</Code> shows only the label and icon, with no
|
|
227
|
+
padding, background, or shadow. Hover changes only their color. Use{" "}
|
|
228
|
+
<Code>variantColor</Code> to tint them.
|
|
229
|
+
</P>
|
|
230
|
+
<Flex row alignItems="center" gap={4} style={{ flexWrap: "wrap" }}>
|
|
231
|
+
<Button variant="text">Text button</Button>
|
|
232
|
+
<Button variant="text" variantColor="blue">
|
|
233
|
+
<Plus />
|
|
234
|
+
Create
|
|
235
|
+
</Button>
|
|
236
|
+
<Button variant="text" aria-label="More text actions">
|
|
237
|
+
<DotsHorizontal />
|
|
238
|
+
</Button>
|
|
239
|
+
<Button variant="text" variantColor="#6366f1">
|
|
240
|
+
Custom color
|
|
241
|
+
</Button>
|
|
242
|
+
<Button variant="text" size="sm">
|
|
243
|
+
Small text
|
|
244
|
+
</Button>
|
|
245
|
+
<Button variant="text" isDisabled>
|
|
246
|
+
Disabled text
|
|
247
|
+
</Button>
|
|
248
|
+
<Button variant="text" isPending>
|
|
249
|
+
Pending text
|
|
250
|
+
</Button>
|
|
251
|
+
</Flex>
|
|
252
|
+
|
|
224
253
|
<H3>Icons with text</H3>
|
|
225
254
|
<Flex row alignItems="center" gap={4}>
|
|
226
255
|
<Button>
|
package/src/patterns/Sidebar.tsx
CHANGED
|
@@ -142,7 +142,7 @@ const itemClass = style(navigationItem, focusRing("&:focus-visible"), {
|
|
|
142
142
|
backgroundColor: "transparent",
|
|
143
143
|
color: colors.gray[12],
|
|
144
144
|
"&[aria-current='page']": {
|
|
145
|
-
color: colors.accent[
|
|
145
|
+
color: colors.accent[11],
|
|
146
146
|
fontWeight: 500,
|
|
147
147
|
backgroundColor: "transparent",
|
|
148
148
|
},
|
package/src/tokens/prose.ts
CHANGED
|
@@ -11,15 +11,15 @@ import { fontFamily, monoFontStyle } from "./text"
|
|
|
11
11
|
* rhythm tuned for sustained reading. The `Prose` container picks a size and
|
|
12
12
|
* typography components inside it opt into the matching prose treatment.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Type sizes and heading spacing are ported from the Tailwind Typography plugin
|
|
15
15
|
* (https://github.com/tailwindlabs/tailwindcss-typography): `sm` -> 14px base,
|
|
16
16
|
* `md` -> Tailwind's `base` (16px), `lg` -> 18px. Tailwind's em-relative values
|
|
17
17
|
* are resolved to px here so they compose with Maui's px-based tokens.
|
|
18
18
|
*
|
|
19
19
|
* On the Large platform scale (coarse pointers) block font sizes and line
|
|
20
20
|
* heights step up ~1.2x — the same idea as the `text` tokens — so reading
|
|
21
|
-
* content grows with the rest of the UI on touch devices.
|
|
22
|
-
*
|
|
21
|
+
* content grows with the rest of the UI on touch devices. Paragraph gaps stay
|
|
22
|
+
* at 10px on both scales.
|
|
23
23
|
*/
|
|
24
24
|
export type ProseSize = "sm" | "md" | "lg"
|
|
25
25
|
|
|
@@ -37,7 +37,6 @@ type HeadingMetric = BlockMetric & {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
type ProseMetric = {
|
|
40
|
-
blockGap: number
|
|
41
40
|
paragraph: BlockMetric
|
|
42
41
|
h1: HeadingMetric
|
|
43
42
|
h2: HeadingMetric
|
|
@@ -55,7 +54,6 @@ const scaledPx = (normal: number, large: number) =>
|
|
|
55
54
|
|
|
56
55
|
const metrics: Record<ProseSize, ProseMetric> = {
|
|
57
56
|
sm: {
|
|
58
|
-
blockGap: 16,
|
|
59
57
|
paragraph: { fontSize: scaledPx(14, 17), lineHeight: scaledPx(24, 29) },
|
|
60
58
|
h1: {
|
|
61
59
|
fontSize: scaledPx(30, 36),
|
|
@@ -81,11 +79,10 @@ const metrics: Record<ProseSize, ProseMetric> = {
|
|
|
81
79
|
marginTop: 20,
|
|
82
80
|
marginBottom: 8,
|
|
83
81
|
},
|
|
84
|
-
listPadding:
|
|
82
|
+
listPadding: 20,
|
|
85
83
|
listItemGap: 4,
|
|
86
84
|
},
|
|
87
85
|
md: {
|
|
88
|
-
blockGap: 20,
|
|
89
86
|
paragraph: { fontSize: scaledPx(16, 19), lineHeight: scaledPx(28, 34) },
|
|
90
87
|
h1: {
|
|
91
88
|
fontSize: scaledPx(36, 43),
|
|
@@ -111,11 +108,10 @@ const metrics: Record<ProseSize, ProseMetric> = {
|
|
|
111
108
|
marginTop: 24,
|
|
112
109
|
marginBottom: 8,
|
|
113
110
|
},
|
|
114
|
-
listPadding:
|
|
111
|
+
listPadding: 24,
|
|
115
112
|
listItemGap: 8,
|
|
116
113
|
},
|
|
117
114
|
lg: {
|
|
118
|
-
blockGap: 24,
|
|
119
115
|
paragraph: { fontSize: scaledPx(18, 22), lineHeight: scaledPx(32, 38) },
|
|
120
116
|
h1: {
|
|
121
117
|
fontSize: scaledPx(48, 58),
|
|
@@ -141,17 +137,12 @@ const metrics: Record<ProseSize, ProseMetric> = {
|
|
|
141
137
|
marginTop: 32,
|
|
142
138
|
marginBottom: 8,
|
|
143
139
|
},
|
|
144
|
-
listPadding:
|
|
140
|
+
listPadding: 26,
|
|
145
141
|
listItemGap: 12,
|
|
146
142
|
},
|
|
147
143
|
}
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
const blockGaps = defineVars({
|
|
151
|
-
sm: { default: `${metrics.sm.blockGap}px`, [LARGE_SCALE]: "12px" },
|
|
152
|
-
md: { default: `${metrics.md.blockGap}px`, [LARGE_SCALE]: "16px" },
|
|
153
|
-
lg: { default: `${metrics.lg.blockGap}px`, [LARGE_SCALE]: "20px" },
|
|
154
|
-
})
|
|
145
|
+
const blockGap = "10px"
|
|
155
146
|
|
|
156
147
|
// Heading weights follow Tailwind's descending scale, capped at Maui's
|
|
157
148
|
// heaviest weight (700).
|
|
@@ -216,24 +207,24 @@ const notHeading = ":not(h1):not(h2):not(h3):not(h4)"
|
|
|
216
207
|
* `marginBottom` becomes the "hug" gap after a heading; heading `marginTop`
|
|
217
208
|
* becomes the larger break before it.
|
|
218
209
|
*/
|
|
219
|
-
export const proseRhythm = memoize((size: ProseSize) => {
|
|
210
|
+
export const proseRhythm = memoize((size: ProseSize, selector = "&") => {
|
|
220
211
|
const m = metrics[size]
|
|
221
212
|
|
|
222
213
|
return style({
|
|
223
|
-
|
|
224
|
-
marginTop:
|
|
214
|
+
[`${selector} > * + *`]: {
|
|
215
|
+
marginTop: blockGap,
|
|
225
216
|
},
|
|
226
217
|
// Gap after a heading: hug it to the content that follows.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
218
|
+
[`${selector} > h1 + *`]: { marginTop: px(m.h1.marginBottom) },
|
|
219
|
+
[`${selector} > h2 + *`]: { marginTop: px(m.h2.marginBottom) },
|
|
220
|
+
[`${selector} > h3 + *`]: { marginTop: px(m.h3.marginBottom) },
|
|
221
|
+
[`${selector} > h4 + *`]: { marginTop: px(m.h4.marginBottom) },
|
|
231
222
|
// Break before a heading (only when it opens a new section, i.e. it
|
|
232
223
|
// isn't already hugging a preceding heading).
|
|
233
|
-
[
|
|
234
|
-
[
|
|
235
|
-
[
|
|
236
|
-
[
|
|
224
|
+
[`${selector} > ${notHeading} + h1`]: { marginTop: px(m.h1.marginTop) },
|
|
225
|
+
[`${selector} > ${notHeading} + h2`]: { marginTop: px(m.h2.marginTop) },
|
|
226
|
+
[`${selector} > ${notHeading} + h3`]: { marginTop: px(m.h3.marginTop) },
|
|
227
|
+
[`${selector} > ${notHeading} + h4`]: { marginTop: px(m.h4.marginTop) },
|
|
237
228
|
})
|
|
238
229
|
})
|
|
239
230
|
|
|
@@ -250,7 +241,7 @@ export const proseHtml = memoize((size: ProseSize) => {
|
|
|
250
241
|
return style({
|
|
251
242
|
display: "flex",
|
|
252
243
|
flexDirection: "column",
|
|
253
|
-
gap:
|
|
244
|
+
gap: blockGap,
|
|
254
245
|
"& p": {
|
|
255
246
|
...block(m.paragraph),
|
|
256
247
|
fontWeight: 400,
|