diff --git a/.gitignore b/.gitignore index 2770e35cf19ada..2e218b3b9b2d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ test-results/ # typescript *.tsbuildinfo next-env.d.ts + +# spacing-derivation review screenshots (local verification harness) +/spacing-screenshots/ +/scripts/spacing-screenshots/__baselines__/ +/scripts/spacing-screenshots/.playwright-output/ diff --git a/docs/adr/0001-spacing-derived-component-dimensions.md b/docs/adr/0001-spacing-derived-component-dimensions.md new file mode 100644 index 00000000000000..0351ce0c5a3f91 --- /dev/null +++ b/docs/adr/0001-spacing-derived-component-dimensions.md @@ -0,0 +1,68 @@ +# Component dimensions are spacing-derived + +Component dimension values (padding, label offsets) are expressed as +`calc(theme.spacing(N) ± )` with an **integer** `N`, instead of hardcoded +pixels — so they ride the `--mui-spacing` runtime variable for density, while +rendering pixel-identically to the previous hardcoded values at the default +theme. + +## Context + +Material UI hardcodes component paddings as pixels (`padding: '6px 16px'`, +`'16.5px 14px'`, …). Under `createTheme({ cssVariables: true })`, +`theme.spacing(n)` compiles to `calc(n * var(--mui-spacing))`, so overriding +`--mui-spacing` at any scope reflows everything spacing-derived. But components +opt out of that dial wherever they use literal px — density stops at the +component edge. + +We want components to follow `--mui-spacing` without: + +- any visual change at the default theme (Argos must be a zero-diff), and +- introducing new public/agnostic CSS variables (that is a separate effort). + +The spec paddings are not clean 8px multiples (`6`, `5`, `15`, `16.5`, …), so a +naive `theme.spacing(value/8)` yields ugly fractional multipliers +(`spacing(1.875)`) that are hard to read and debug. + +## Decision + +Express every **spacing** dimension as `calc(theme.spacing(N) ± offsetpx)`: + +- **N = `round(px / 8)`** — the nearest whole unit. Integer multipliers stay + legible (`spacing(2)` reads as "2 units") and debuggable. +- **offset** = the px needed to land on the exact original value, so the + default theme (`--mui-spacing: 8px`) is pixel-identical. When the offset is + `0`, drop the `calc` and use `theme.spacing(N)` directly. +- **Outlined** input/button block = the contained expression **− 1px**; + the `−1` is the border compensation (`6 → 5`). +- **Vertical axis only where horizontal couples to fixed sibling geometry.** + The outlined input's notch (NotchedOutline ``) sits at a fixed left + inset; if the input's **inline** padding or the label's **transformX** scaled + with density, the value text, the floating label, and the notch gap would + desync — a visibly broken outline. So for OutlinedInput and the outlined + InputLabel, only the **block / y-axis** is spacing-derived; inline padding, + adornment paddings, and `transformX` stay literal. Components with **no** such + coupling (Button) keep both axes spacing-derived. +- **The outlined InputLabel resting y-offset tracks the input's block padding** + (so the label stays vertically aligned at any density). The _shrunk_ transform + stays fully literal — when floated, the label sits on the border line, which + padding does not move. + +**Not** spacing, left untouched: the `1.4375em` line-box (font-size-coupled), +`border-radius`, `border-width`, `scale()` factors, the NotchedOutline `8px` +notch, and — per above — horizontal (inline/x) values on notch-coupled +components. + +## Consequences + +- Overriding `--mui-spacing` at a scope reflows Button padding and outlined + input height/label together — density with one dial, no per-component knobs. +- Pixel-identical at the default theme; Argos is the acceptance gate + (real browsers resolve `calc`; jsdom does not, so density assertions belong + in browser/visual tests, not jsdom unit tests). +- Conversion is mechanical and component-by-component, so there is a temporary + window where converted components (Button, OutlinedInput) follow the dial and + unconverted ones (FilledInput, standard Input, Select, …) do not. The + replication spec exists to close that window consistently — see + [spacing-derived-dimensions-spec.md](./spacing-derived-dimensions-spec.md). +- No new public API: `--variant-*` and other private vars are untouched. diff --git a/docs/adr/spacing-derived-dimensions-spec.md b/docs/adr/spacing-derived-dimensions-spec.md new file mode 100644 index 00000000000000..59ffc836ed92a8 --- /dev/null +++ b/docs/adr/spacing-derived-dimensions-spec.md @@ -0,0 +1,193 @@ +# Spacing-derived dimensions — replication spec + +How to convert a component's hardcoded px dimensions to spacing-derived values. +Follow this for the next set (FilledInput, standard `Input`, `Select`, +multiline, …). Decision + rationale: [0001](./0001-spacing-derived-component-dimensions.md). + +## The rule + +For each **spacing** px value `P` (padding / margin / gap / label offset): + +1. `N = round(P / 8)` — nearest whole unit (integer). +2. `offset = P − 8 * N`. +3. Write `` `calc(${theme.spacing(N)} - <|offset|>px)` `` (or `+`) when + `offset !== 0`; otherwise `` `${theme.spacing(N)}` `` (no `calc`). +4. Verify it resolves to `P` at the default (`spacing(1) = 8px`). + +Relationship rules: + +- **Outlined = contained − 1px** (border compensation), e.g. Button outlined + block `5 = 6 − 1`. +- **Horizontal stays literal where x is anchored.** Keep inline padding, + adornment paddings, and label `transformX` **literal** whenever something is + horizontally anchored to the input's inline padding. The outlined notch + (``, fixed left inset) is the obvious break — but the **filled and + standard labels also align their x to inline padding**, so keep their x + literal too. For inputs and their labels, only the **block / y-axis** derives. + Components with no anchored horizontal relationship (Button) derive **both**. +- **Floating-label y tracks the input — but the shrunk state splits by where it + floats:** + - **resting** y → tracks the input's block padding (the value position). + - **shrunk** y → **literal if it floats onto a border** (outlined: the floated + label sits on the border line, which padding doesn't move). **Tracks the + input if it floats into reserved padding** (filled: into the top padding). + Filled shrunk is the tricky one — tracking one unit while `paddingTop` tracks + three leaves a growing gap at high density; tune the coefficient and **verify + with a valued/focused field** (an empty field only shows the resting label). + +Leave untouched (not spacing): `em` line-boxes, `border-radius`, +`border-width`, `scale()`, NotchedOutline notch padding, and horizontal +(inline/x) values on inputs + their labels. + +## Choosing `N` — the step coefficient + +Once a value is written as `calc( + theme.spacing(N))`, **N is the +step coefficient**: each 1px change in `--mui-spacing` shifts that value by +N px. So N is not just "the closest multiple of 8" — it's a design knob for +how fast that element scales with density. + +**Rule of thumb: N reflects the size/role of the element.** + +- **Larger / container-like elements → larger N.** A medium-sized container + (Button padding, Chip height) typically uses `N = 2` (+2px per spacing unit), + so the container visibly responds to density. +- **Smaller / inner elements → smaller N.** Inner glyphs inside a container + (Chip avatar/icon/deleteIcon `width`/`height`/`fontSize`) typically use `N = 1` + (+1px per spacing unit), so they scale gently and don't outgrow their parent. +- **The result:** at high density the container grows faster than its contents, + so inner gaps grow rather than shrink (avatars don't overflow); at low density + the container shrinks faster than its contents, but a separate **min-clamp** + prevents inner offsets from going negative — see below. + +Choosing N this way trades two competing goals: + +1. **Default fidelity** — `calc( + spacing(N))` must equal the original + px at `--mui-spacing = 8` (the math constraint). +2. **Density behavior** — at non-default densities, smaller N keeps proportions + tighter, larger N makes the element more density-responsive. + +Pick the smallest N that still gives meaningful response. For sub-unit values +(`< 4px`) that are **load-bearing in a coupled system** (e.g. Chip's `5/-6/4` +offsets), prefer `N = 1` with a px offset (`calc(spacing(1) - 3px)`) — this +keeps the value scaling at the gentlest rate, matching the surrounding inner +elements. + +### Clamp inner-margin negatives at `0` + +Compensation negatives like `calc(spacing(1) - 3px)` go _negative_ when +`--mui-spacing < 3` (would pull the inner element past the container edge). +Wrap them in `max(…, 0px)` to clamp: + +```js +marginLeft: `max(calc(${theme.spacing(1)} - 3px), 0px)`; +``` + +At default the `max()` is a no-op (5px stays 5px). At ultra-low density it +prevents the negative shift. Use this for inner-positive offsets that must +not go negative (Chip avatar/icon `marginLeft`, deleteIcon `marginRight`). +Compensation negatives that _should_ stay negative (FormControlLabel `-11` +cancelling IconButton padding) don't need the clamp — they're meant to be +negative at every density. + +## Worked examples (this PR) + +### Button (`theme.spacing(1)=8`, `(2)=16`, `(3)=24`) + +| variant | size | block | inline | +| :-------- | :--- | :--------------------- | :---------------------- | +| contained | md | `spacing(1) − 2px` (6) | `spacing(2)` (16) | +| contained | sm | `spacing(1) − 4px` (4) | `spacing(1) + 2px` (10) | +| contained | lg | `spacing(1)` (8) | `spacing(3) − 2px` (22) | +| text | md | `spacing(1) − 2px` (6) | `spacing(1)` (8) | +| text | sm | `spacing(1) − 4px` (4) | `spacing(1) − 3px` (5) | +| text | lg | `spacing(1)` (8) | `spacing(1) + 3px` (11) | +| outlined | \* | contained − 1px | contained − 1px | + +Button has no notch, so both axes derive. + +### OutlinedInput + +| target | value | +| :------------------------ | :-------------------------- | +| input block, md | `spacing(2) + 0.5px` (16.5) | +| input block, sm | `spacing(1) + 0.5px` (8.5) | +| input inline + adornments | `14px` — literal (notch) | + +### InputLabel (outlined) + +| state | transform | +| :---------- | :---------------------------------------- | +| resting, md | `translate(14px, spacing(2))` — x literal | +| resting, sm | `translate(14px, spacing(1) + 1px)` | +| shrunk | `translate(14px, −9px)` — fully literal | + +### InputLabel (standard) — x stays `0` + +Resting y tracks the standard input's text top = `Input` `marginTop` +(`spacing(2)`) + `InputBase` `paddingTop`: + +| state | transform | +| :---------- | :--------------------------------------------- | +| resting, md | `translate(0, spacing(3) − 4px)` (20) | +| resting, sm | `translate(0, spacing(2) + 1px)` (17) | +| shrunk | `translate(0, −1.5px)` — floats above, literal | + +### FilledInput + InputLabel (filled) — block only, x literal + +Input block padding (top / bottom); inline `12` + adornments stay literal: + +| state | top | bottom | +| :------------- | :---------------------- | :---------------------- | +| md | `spacing(3) + 1px` (25) | `spacing(1)` (8) | +| sm | `spacing(3) − 3px` (21) | `spacing(1) − 4px` (4) | +| hiddenLabel md | `spacing(2)` (16) | `spacing(2) + 1px` (17) | +| hiddenLabel sm | `spacing(1)` (8) | `spacing(1) + 1px` (9) | + +Filled label `transformY` (x stays `12px`): + +| state | y | +| :---------- | :----------------------------------------------------------- | +| resting, md | `spacing(2)` (16) | +| resting, sm | `spacing(2) − 3px` (13) | +| shrunk, md | `spacing(1) − 1px` (7) — floats _into_ padding, so it tracks | +| shrunk, sm | `spacing(1) − 4px` (4) | + +### InputBase (standard `Input`) — block only, inline already `0` + +Root-multiline + input share `4px 0 5px`. No inline padding ⇒ nothing anchored, +but the variant has no notch either — still derive **block only** (the `0` +inline is already density-free): + +| target | value | +| :------------------------ | :------------------------------- | +| block top | `spacing(1) − 4px` (4) | +| block bottom | `spacing(1) − 3px` (5) | +| small `paddingTop` | `1px` — literal (sub-unit nudge) | +| multiline input `padding` | `0` — unchanged | + +The standard `InputLabel` transforms track this in their own rollout item. + +### Surrounding form components + +| component | value | derivation | +| :--------------- | :----------------------------- | :------------------------------------ | +| FormControl | normal `marginTop 16` | `spacing(2)` | +| FormControl | normal `marginBottom 8` | `spacing(1)` | +| FormControl | dense `marginTop 8` | `spacing(1)` | +| FormControl | dense `marginBottom 4` | `spacing(1) − 4px` | +| FormControlLabel | row gaps `marginRight/Left 16` | `spacing(2)` | +| FormControlLabel | `marginLeft/Right −11` | literal — compensates control padding | +| FormHelperText | `marginTop 3/4`, inline `14` | literal — micro-gap / input-anchored | +| InputAdornment | filled start `marginTop 16` | `spacing(2)` (tracks label-space) | + +FormControl's root was a static styled object — wrap it in `memoTheme(({ theme }) +=> …)` to reach `theme.spacing`. + +## Verification + +Use the local harness (`scripts/spacing-screenshots/`) — see the rollout plan's +"How to verify". Default render must be **pixel-identical** to the pre-change +baseline (`toHaveScreenshot`, `maxDiffPixels: 0`); review 6px/10px shots for +density. For floating-label components, put a **valued field** in the fixture so +the shrunk label is visible. Unit tests (vitest, browser + node) green; lint, +prettier. No jsdom assertion on computed padding px. diff --git a/docs/adr/spacing-derived-rollout-plan.md b/docs/adr/spacing-derived-rollout-plan.md new file mode 100644 index 00000000000000..592626d946772d --- /dev/null +++ b/docs/adr/spacing-derived-rollout-plan.md @@ -0,0 +1,245 @@ +# Spacing-derived dimensions — rollout plan + +Roll the spacing-derivation pattern across `packages/mui-material/src/*`, one +component (or tight family) per PR, until every spacing dimension rides +`--mui-spacing`. + +- **Decision / why:** [0001-spacing-derived-component-dimensions.md](./0001-spacing-derived-component-dimensions.md) +- **How (the rule + worked examples):** [spacing-derived-dimensions-spec.md](./spacing-derived-dimensions-spec.md) + +**Rollout complete** — every group in the checklist below is done or audited-skip. +Each component was verified with the local harness (default render pixel-identical +to the pre-change baseline) + unit tests + lint/prettier; see per-item notes. + +Recurring decisions applied throughout: + +- **Vertical (block) derives; anchored horizontal stays literal.** Inputs (notch / + label-x), lists (edge padding ↔ icon width ↔ inset), select arrow reservation, + autocomplete icon reservation, checkbox columns, tooltip arrows. +- **`N` in `theme.spacing(N)` is the step coefficient** — each 1px of + `--mui-spacing` shifts the value by N px. Pick N from the element's size/role, + not just from `round(P/8)`: larger / container-like elements (Chip height, + Button padding) use `N = 2` so they respond visibly to density; smaller / inner + elements (Chip avatar/icon glyph sizes) use `N = 1` so they scale gently and + don't outgrow their container. See spec § "Choosing N — the step coefficient". + Wrap inner-positive offsets that must not go negative in `max(…, 0px)`. +- **Geometry stays literal** — fixed `width/height/minWidth`, icon sizes, `em`/`rem` + line-boxes, `border`, `scale()`, thumb/track/rail, half-icon offsets, `%` translates. +- **Negative compensation margins track their padding** via `theme.spacing(-n)` + (e.g. StepButton, Alert/Card/Snackbar action, IconButton edge stays literal where + it doesn't cleanly map to a unit). +- **Sub-unit values (`< 4px`, and the `1/2/3px` nudges) stay literal.** +- Static styled objects were wrapped in `memoTheme(({ theme }) => …)` to reach + `theme.spacing`. + +Audited-skip components: Select/NativeSelect, FormHelperText, FormLabel, +ButtonGroup, Chip, StepConnector, Dialog paper margin, ImageList/ImageListItem, +AvatarGroup, Link, Switch, Slider, LinearProgress, Skeleton, Divider (already +derived), Typography, CssBaseline. + +## Requirement (per component) + +For every **spacing** value (padding / margin / gap / row-column-gap, and any +transform offset that positions content relative to padding): + +1. Convert to `calc(theme.spacing(N) ± offsetpx)`, `N = round(px/8)` integer, + offset for a **pixel-identical default**. Offset `0` → bare `theme.spacing(N)`. +2. Keep the value's relationships intact (border `−1px` comps, label/anchor + transforms tracking their controlled element). +3. Touch nothing else — no public vars, no visual change at the default theme. + +## Edge cases (check every component against these) + +- **Horizontal / anchored coupling.** Keep a horizontal value **literal** when + something is anchored to it. The outlined notch is the loud case, but the + **filled and standard label x also align to the input's inline padding** — so + inputs and their labels keep inline/x literal (derive vertical only). + Re-evaluate per component; ones with no anchored horizontal relationship + (Button) derive both axes. +- **Not spacing → leave literal:** `em`/`rem` line-boxes, icon/avatar sizes, + `border-width`, `border-radius`, `scale()`, `translate` used for motion/flip, + rail/thumb/track geometry. +- **Sub-unit values (< 4px).** `round(px/8) = 0` → keep the px literal (don't + write `spacing(0) ± x`), unless it's part of a scaling family (then express + relative, e.g. `contained − 1px`). +- **Fractional offsets** (`+ 0.5px`, inputs) are fine. +- **Asymmetric block padding** (e.g. FilledInput `25` top / `8` bottom) — derive + each side independently; the matching InputLabel transform tracks the top. +- **Floating-label transforms** track their input on the **y-axis** (x stays + literal). Resting y tracks the input's block padding. Shrunk y is **literal if + the label floats onto a border** (outlined) but **tracks the input if it + floats into reserved padding** (filled — and expect a growing gap at high + density unless the coefficient is tuned). Verify the shrunk state with a + valued/focused field — an empty field only shows the resting label. +- **Mixed shorthand** — block derived, inline literal → `` `calc(…) 14px` ``. +- **default-prop margins** (e.g. `FormControl` `margin="dense|normal"`) are + spacing — convert them too. +- **Geometry-only components** (Switch, Slider, LinearProgress, Skeleton, + Divider) — likely nothing to convert; confirm and skip. + +## How to verify (every PR — local, no Argos) + +Argos per-component is too slow. Verify locally with the screenshot harness +(`scripts/spacing-screenshots/` — a Playwright test using its built-in pixel +comparator, no extra deps). Screenshots land in `spacing-screenshots//` +(gitignored) for your review. Requires `pnpm docs:dev` running. + +1. Add the component's load-bearing matrix (variants × sizes, adornment, + multiline) to the `spacing-fixture` route's demo map. For floating-label + components, include a **valued/focused field** so the shrunk label is visible. +2. **Baseline (before):** on the _unconverted_ component, + `COMPONENT= pnpm spacing:shot:update` — writes the "before" baseline + (`baseline-default.png`). +3. Implement the spacing-derivation. +4. **Assert + density (after):** `COMPONENT= pnpm spacing:shot` — + - asserts the default render is **pixel-identical to the baseline** + (`toHaveScreenshot`, `maxDiffPixels: 0`); a mismatch ⇒ a wrong offset, and + a diff image is written to `test-results/`. **This is the regression gate.** + - writes `after-6px.png` / `after-10px.png` (`--mui-spacing` set inline on the + fixture scope) for review. +5. **Density review (human):** eyeball `after-6px` / `after-10px` for reflow and + anchored/notch alignment — new behavior, not assertable. +6. Unit tests green (`pnpm test:unit run ` — browser **and** node), + `eslint`, `prettier`, `tsc`. No jsdom assertion on computed padding px. + +> Pixel-identical at default is also true by construction (`calc(8px − 2px) ≡ 6px`); step 4's diff is the belt-and-suspenders catch for a mistyped offset. + +## Component checklist + +### Next set — input / form family (completes TextField) + +- [x] ~~FilledInput (input md/sm incl. `25/8` label-space, adornments, multiline)~~ ✅ Done +- [x] ~~InputBase (standard `Input`: `4px 0 5px`, small, multiline)~~ ✅ Done — block only (`spacing(1)−4px` / `−3px`), 1px small nudge stays literal; inline already `0`. Standard label tracking ⇒ InputLabel item. +- [x] ~~Input~~ ✅ Done — only spacing is the standard label-gap `marginTop: 16` → `spacing(2)`. +- [x] ~~NativeSelect / Select (`paddingRight 24/32`)~~ ✅ Done — **skip**: `paddingRight 24/32` reserves space for the fixed 24px arrow icon (absolutely positioned, `right: 0/7`) → icon-anchored horizontal geometry, not density; block padding comes from the wrapping input variant. `minWidth 16` / icon `top: calc(50% − .5em)` are geometry. +- [x] ~~InputAdornment~~ ✅ Done — filled start `marginTop: 16` → `spacing(2)` (tracks the filled label-space, verified with a valued field). `marginRight/marginLeft: 8` stay **literal** — horizontal, part of the input's inline layout which the whole family keeps literal. +- [x] ~~InputLabel — **filled** + **standard** transform sets (track their input)~~ ✅ Done — filled with FilledInput; standard resting y → `spacing(3) − 4px` (md) / `spacing(2) + 1px` (sm), tracking `Input` marginTop + `InputBase` paddingTop. Shrunk `-1.5px` floats above the field → literal. +- [x] ~~FormControl (margin dense/normal), FormControlLabel, FormHelperText, FormLabel~~ ✅ Done — FormControl normal `16/8` → `spacing(2)`/`spacing(1)`, dense `8/4` → `spacing(1)`/`spacing(1)−4px` (static styled → `memoTheme`). FormControlLabel row gaps `16` → `spacing(2)`; `−11` (compensates control icon-button padding) → `calc(${theme.spacing(-1)} - 3px)` in **iter 2** so it tracks IconButton padding at density. FormHelperText **skip**: `marginTop 3/4` micro typographic gaps (deriving 4 would invert vs the 3 medium), `marginLeft/Right 14` align to input inline → literal. FormLabel **skip**: only `padding: 0`. + +**Input / form family complete.** Skipped within it: Select/NativeSelect (icon +geometry), FormHelperText + FormLabel (micro-gaps / input-anchored / `padding: 0`). + +### Buttons & actionable controls + +- [x] ~~IconButton~~ ✅ Done — padding `8/5/12` → `spacing(1)` / `spacing(1)−3px` / `spacing(2)−4px`. `fontSize` (icon size) + edge `−12/−3` margins (alignment compensation anchored to padding/icon) stay literal. +- [x] ~~ButtonBase~~ ✅ Done — **skip**: only `padding: 0` / `margin: 0` resets. +- [x] ~~ButtonGroup~~ ✅ Done — **skip**: `marginLeft/Top: -1` are border-overlap geometry, `minWidth: 40` is sizing; no padding/gap. +- [x] ~~Fab~~ ✅ Done — extended inline padding `0 16px`/`0 8px` → `0 spacing(2)` / `0 spacing(1)`. Circular FAB + all width/height/minWidth/borderRadius are geometry (literal). +- [x] ~~Chip~~ ✅ Done — initially skipped, then derived in **iter 2** as a fully coupled formula: every value (height `32/24`, inline padding `12/8`, avatar/icon `width/height 24/18`, all the `5/-6/4/-4/2/3` offsets, deleteIcon `fontSize 22/16`, label paddings) → `spacing(N) ± offset`. The whole pill scales coherently with `--mui-spacing` (10px → 40px-tall chip with proportional avatar/icon/gaps). ChipLabel static → `memoTheme`. Pixel-identical at default to the literal version. +- [x] ~~ToggleButton / ToggleButtonGroup~~ ✅ Done — ToggleButton padding `11/7/15` → `spacing(1)+3px` / `spacing(1)−1px` / `spacing(2)−1px`. ToggleButtonGroup **skip**: only `-1` border-overlap margins. +- [x] ~~Tab~~ ✅ Done — padding `12px 16px` → `spacing(2)−4px spacing(2)`; labelIcon `paddingTop/Bottom 9` → `spacing(1)+1px`; stacked-icon margin `6` → `spacing(1)−2px` (start/end icon margins were already `spacing(1)`). One jsdom computed-style test → `skipIf(isJsdom())` (calc). +- [x] ~~BottomNavigationAction~~ ✅ Done — inline padding `12` → `spacing(2)−4px`; unselected `paddingTop 14` → `spacing(2)−2px`. minWidth/maxWidth geometry. +- [x] ~~SpeedDialAction~~ ✅ Done — fab `margin 8` → `spacing(1)`; static-tooltip `marginLeft/Right 8` → `spacing(1)`; label `padding 4px 16px` → `spacing(1)−4px spacing(2)`. Verified by construction + unit tests (SpeedDial is position-fixed, doesn't isolate in the harness scope). +- [x] ~~Pagination / PaginationItem~~ ✅ Done — Pagination root **skip** (`padding/margin: 0`). PaginationItem inline padding `6/4/10` → `spacing(1)−2px` / `−4px` / `+2px` (both text + outlined blocks); inter-item margins `3/1` + page-icon `−8` stay literal. Fixed `height/minWidth` = geometry (like Fab-extended: horizontal padding scales, height doesn't). + +### Lists & menus + +- [x] ~~List / ListSubheader~~ ✅ Done — List `paddingTop/Bottom 8` → `spacing(1)` (static styled → `memoTheme`). ListSubheader **skip**: only inline `16/72` (horizontal coupled). +- [x] ~~ListItem / ListItemButton / ListItemAvatar / ListItemIcon / ListItemText~~ ✅ Done — **vertical only** (the list family's inline `16` ↔ icon width `56/36` ↔ inset `72/56` is an anchored horizontal system, kept literal). Block paddings `8` → `spacing(1)`, dense `4` → `spacing(1)−4px`; ListItemText margins `4/6` → `spacing(1)−4px`/`−2px`; Avatar/Icon flex-start `marginTop 8` → `spacing(1)`. ListItemAvatar/ListItemText were static → `memoTheme`. +- [x] ~~MenuItem~~ ✅ Done — block `paddingTop/Bottom 6` → `spacing(1)−2px`, dense `4` → `spacing(1)−4px`. Inline `16/36/52` + minHeight stay literal (horizontal/geometry); divider margins were already `spacing(1)`. +- [x] ~~Autocomplete~~ ✅ Done — dropdown block paddings (listbox `8`, option `6`, loading/noOptions `14`) → spacing-based. Input-integration (static root → `memoTheme`): **block only** across outlined/filled/standard × sizes × hiddenLabel; the padding **redistribution** preserved (outlined root `9` + inner `7.5` → `spacing(1)+1px` + `spacing(1)−0.5px`, sums to the OutlinedInput total). All inline kept literal — icon-anchored reservation (`paddingRight 26+4+9`, `endAdornment right: 9`, indicator paddings `4/2`). Verified field pixel-identical across all variants; dropdown via unit tests. + +### Surfaces & containers + +- [x] ~~Accordion / AccordionSummary / AccordionActions~~ ✅ Done — Accordion expanded `margin 16` → `spacing(2)`; Summary content margin `12/20` → `spacing(2)−4px` / `spacing(3)−4px`; Actions `padding/marginLeft 8` → `spacing(1)` (static → `memoTheme`). Summary padding + AccordionDetails were already `theme.spacing`. +- [x] ~~Alert / AlertTitle~~ ✅ Done — root `6px 16px` → `spacing(1)−2px spacing(2)`; icon/message/action paddings + icon `marginRight 12` → spacing-based (all vertical derived together so they stay centered); action `marginRight −8` → `spacing(-1)` (tracks IconButton padding). Icon/Message/Action static → `memoTheme`. AlertTitle `marginTop −2` literal (sub-unit). +- [x] ~~Dialog / DialogTitle / DialogContent / DialogActions~~ ✅ Done — Title `16px 24px` → `spacing(2) spacing(3)`; Content `20px 24px` → `spacing(3)−4px spacing(3)`, dividers `16px 24px` → `spacing(2) spacing(3)`; Actions `padding/marginLeft 8` → `spacing(1)`. **Dialog root skip**: paper `margin 32` is coupled to static media-query breakpoints (`+ 32*2`, can't use CSS vars) — deriving would desync margin from the breakpoint thresholds. Title/Actions static → `memoTheme`. +- [x] ~~Card — CardHeader / CardContent / CardActions~~ ✅ Done — Header `padding 16`/avatar `marginRight 16` → `spacing(2)`, action `marginRight −8` → `spacing(-1)` (`marginTop/Bottom −4` literal); Content `16` → `spacing(2)`, last-child `paddingBottom 24` → `spacing(3)`; Actions `padding/marginLeft 8` → `spacing(1)`. All static → `memoTheme`. Card root has no spacing. +- [x] ~~SnackbarContent~~ ✅ Done — root `6px 16px` → `spacing(1)−2px spacing(2)`, message `8px 0` → `spacing(1) 0`, action `paddingLeft 16` → `spacing(2)` + `marginRight −8` → `spacing(-1)`. Message/Action static → `memoTheme`. +- [x] ~~Tooltip~~ ✅ Done — tooltip `padding 4px 8px` → `spacing(1)−4px spacing(1)`, touch `8px 16px` → `spacing(1) spacing(2)`. Arrow/placement margins (`-0.71em`, `14/24px`) + `margin 2` stay literal (arrow geometry / popper positioning). Verified by unit tests (portal). +- [x] ~~Breadcrumbs~~ ✅ Done — separator `marginLeft/Right 8` → `spacing(1)` (static → `memoTheme`). +- [x] ~~MobileStepper~~ ✅ Done — `padding 8` → `spacing(1)`; dot `width/height 8` + `margin 0 2px` are geometry/sub-unit (literal). + +### Steppers, tables, misc layout + +- [x] ~~Stepper / Step / StepButton / StepLabel / StepContent / StepConnector~~ ✅ Done — Stepper `gap 8`, Step `padding/gap 8` → `spacing(1)`; StepButton `padding 24px 16px` → `spacing(3) spacing(2)`, `margin -24px -16px` → `spacing(-3) spacing(-2)` (negatives track padding via `theme.spacing(-n)`); StepLabel `padding 8px 0`/icon `8`/alt `marginTop 16` → spacing-based; StepContent `padding 8` → `spacing(1)`, composite `8+12` → `calc(spacing(1) + 12px)` (the `12` half-icon stays literal). **StepConnector skip**: `marginLeft/Right 12` half-icon + `calc(±50% + 20px)` are icon geometry. Several static → `memoTheme`. +- [x] ~~TableCell / TablePagination / TableSortLabel~~ ✅ Done — TableCell `padding 16` → `spacing(2)`, small `6px 16px` → `spacing(1)−2px spacing(2)`; checkbox-column paddings kept literal (fixed-width column, deriving would overflow). TablePagination actions `marginLeft 20` → `spacing(3)−4px`, Select `marginRight 32`/`marginLeft 8`/select `paddingLeft 8` → spacing-based (`paddingRight 24` literal — arrow icon); Select static → `memoTheme`. TableSortLabel icon `margin 4` → `spacing(1)−4px`. +- [x] ~~ImageList / ImageListItemBar~~ ✅ Done — ImageListItemBar `padding 12px 16px` → `spacing(2)−4px spacing(2)`, below `6px 0 12px` → spacing-based. **ImageList/ImageListItem skip**: `gap` is a public px prop applied via inline style (user-controlled, not internal spacing). +- [x] ~~AvatarGroup~~ ✅ Done — **skip**: overlap is `--AvatarGroup-spacing` (negative margin) computed from the `spacing` prop — overlap geometry, not density. +- [x] ~~Link~~ ✅ Done — **skip**: no spacing values. +- [x] ~~Badge~~ ✅ Done — `padding 0 6px` → `0 calc(spacing(1)−2px)`. `translate` offsets (`--Badge-translate`, %) + `height/minWidth` (RADIUS) are geometry (literal). + +### Audit & likely skip (geometry, not spacing density) + +- [x] ~~Switch — thumb/track geometry~~ ✅ Audited, skip — `padding 12/7/4` is coupled to the literal thumb-travel `translateX`; deriving padding alone misaligns the thumb. Edge `−8` margins literal. +- [x] ~~Slider — rail/thumb/mark geometry~~ ✅ Audited, skip — `padding 13px 0`/`20px 0` are rail touch-area geometry (coupled to thumb size); valueLabel padding is `rem`; mark-label clearance margins literal. +- [x] ~~LinearProgress~~ ✅ Audited, skip — only `margin: 0` reset. +- [x] ~~Skeleton~~ ✅ Audited, skip — only `marginTop/Bottom: 0` resets. +- [x] ~~Divider~~ ✅ Audited — already uses `theme.spacing` for its margins/padding; inset `marginLeft: 72` is icon-anchored (list inset) → literal. Nothing to change. +- [x] ~~Typography (margin resets — confirm)~~ ✅ Audited, skip — `margin: 0` reset + `gutterBottom 0.35em` (em, typographic). +- [x] ~~CssBaseline / internal~~ ✅ Audited, skip — global resets, no component spacing. + +## Iteration 2 — deferred & skipped backlog + +Iteration 1 derived the **block / unanchored** spacing of every component. What +remains below was deliberately left literal, grouped by **why** (which decides how +to revisit it). "Partial" = the component was otherwise derived; only the listed +values are still literal. + +### A. Anchored horizontal systems (need a _coordinated_ horizontal pass) + +The inline axis is coupled across components — deriving one value alone breaks the +alignment with its anchor. A future pass must derive the whole chain together (or +introduce a dedicated horizontal-density var) so the anchor stays aligned at every +`--mui-spacing`. + +- [ ] **InputAdornment** (partial) — `marginRight/Left 8` (horizontal, part of input inline layout). +- [ ] **List / ListSubheader / ListItem / ListItemButton / ListItemText** (partial) — ~~edge padding `16`~~ ✅ done iter-2 (all three containers derive together); inset `56/72`, subheader inset `72`, `ListItem secondaryAction paddingRight 48`, `ListItemIcon/Avatar` icon width `36/56` still literal — coordinated icon-width derivation needed. +- [ ] **MenuItem** (partial) — inline `16` gutters / inset `36` / `marginLeft 52`. +- [ ] **TableCell** (partial) — checkbox-column paddings `0 12px 0 16px` / `0 0 0 4px` (fixed-width `24/48` column; deriving overflows). +- [ ] **TablePagination** (partial) — select `paddingRight 24` (arrow reservation). + +### B. Geometry → spacing (need to extend the rule to `width/height/translate`) + +Sized by fixed dimensions or icon offsets, not padding. Iteration 1's rule targets +padding/margin/gap only. Revisiting needs a decision: should fixed geometry ride +`--mui-spacing` (or a separate size scale)? + +- [x] ~~**Chip** (full skip) — `height 32/24`; inline padding `12/8` coupled to avatar/icon/delete offsets.~~ ✅ **Done in iter 2** — coupled formula derives every value (height, avatar/icon, margins, label paddings, deleteIcon fontSize) via `spacing(N) ± offset`; whole pill scales together. Pixel-identical at default. (See iter-1 checklist note + "Iter-2 progress" below.) +- [ ] **Switch** (full skip) — `padding 12/7/4` coupled to the literal thumb-travel `translateX`; track/thumb sizes; edge `-8`. +- [ ] **Slider** (full skip) — `padding 13px 0` / `20px 0` rail touch-area; rail/thumb/mark geometry. +- [ ] **Fab** (partial) — circular `width/height 56/40/48`, `minWidth`, `minHeight 36`. +- [ ] **PaginationItem** (partial) — `height`; page-icon `margin 0 -8px`. +- [ ] **Tab / MenuItem / BottomNavigationAction** (partial) — `minHeight`. + +### C. Sub-unit nudges (`< 4px`) left literal + +Tiny tuning values; deriving them is low-value and can invert size relationships. +Revisit only if exact-px fidelity at high density matters. + + + +### D. Coupled to static media-query breakpoints + +- [ ] **Dialog** (full skip) — paper `margin 32` + `maxHeight/maxWidth/width calc(100% - 64px)` + breakpoint thresholds `+ 32*2`. The CSS could derive, but the JS breakpoint values can't use CSS vars → margin would desync from the threshold. Needs a different approach (e.g. accept the threshold staying fixed, or a container query). + +### E. Public-prop / CSS-var driven spacing + +User-controlled; revisiting means making the _default_ spacing-aware, not the value. + +- [ ] **AvatarGroup** (full skip) — overlap `--AvatarGroup-spacing` from the `spacing` prop (`SPACINGS` `-16/-8`). + +### Iter-2 progress + +- [x] **FormControlLabel `−11`** — derived as `calc(${theme.spacing(-1)} - 3px)` so the compensation tracks IconButton padding at every `--mui-spacing`. Same pattern is applicable to other mixed-offset compensation negatives if revisited (e.g. IconButton edge `−12` → `calc(${theme.spacing(-1)} - 4px)`, currently out of the pruned iter-2 scope). +- [x] **Chip (full coupled derivation)** — every value (root `height`, `borderRadius`, avatar/icon `width/height`, all the small `±2/3/4/5/6` offsets, deleteIcon `fontSize`, label paddings) → `spacing(N) ± offset`. Whole pill scales as a unit. ChipLabel static → `memoTheme`. **Finding:** the iter-1 "sub-unit literal" rule was for _isolated_ small nudges; when sub-unit values are load-bearing in a coupled system (Chip's 5/6/4/2/3 offsets), deriving them via the formula keeps proportions and yields pixel-identical at default. + - **Two-tier step coefficient.** Container scales faster than inner glyphs so contents don't overflow at high density. Chip height uses `spacing(2)` (+2/unit): medium `calc(16px + spacing(2))` = 32, small `calc(8px + spacing(2))` = 24. Inner avatar/icon/deleteIcon `width/height/fontSize` use `spacing(1)` (+1/unit): medium avatar `calc(16px + spacing(1))` = 24, medium deleteIcon `calc(14px + spacing(1))` = 22, small avatar/icon `calc(spacing(1) + 10px)` = 18, small deleteIcon `calc(8px + spacing(1))` = 16. Inner-margin positives wrapped in `max(..., 0px)` so the compensation never goes negative at ultra-low density (avatar `marginLeft`, deleteIcon `marginRight`). `borderRadius` kept literal at `32 / 2` — CSS clamps to `h/2` for the pill regardless. Pixel-identical at default. See spec § "Choosing N — the step coefficient". +- [x] **List family edge padding** — `ListItem` / `ListItemButton` / `ListSubheader` gutter `paddingLeft/Right 16` → `theme.spacing(2)`. The three parallel list-item containers now stay aligned at every `--mui-spacing` (was the coupling reason for iter-1's "horizontal literal" decision). Still literal in this pass: `ListItemText` inset `56`, `ListSubheader` inset `72`, `ListItem` `paddingRight 48` (secondary-action width), `ListItemIcon/Avatar` widths `36/56` — all anchored to icon geometry; the next list-family iter-2 step is a coordinated icon-width derivation. +- [x] **SwitchBase `padding: 9`** (internal — used by Checkbox + Radio; Switch overrides with its own padding) → `calc(${theme.spacing(1)} + 1px)`. **+1/unit step**, deliberately matching `FormControlLabel`'s `marginLeft: calc(spacing(-1) - 3px)` (-1/unit). The sum stays constant (`-2px`) at every density, so the checkbox/radio glyph stays in exactly the same visual position relative to the FormControlLabel edge regardless of `--mui-spacing`. Static styled wrapped via direct `({ theme }) => …` (codebase convention has been `memoTheme` for cached evaluation — call it out for follow-up). + +### Workflow refinement (learned during iter 2) + +**`calc(...var(...))` vs literal-px sub-pixel diff.** Iter 1 captured baselines on the +_unconverted_ literal values. When iter 2 converts a previously-literal value +(e.g. `marginLeft: -11`) to a calc form (`calc(${theme.spacing(-1)} - 3px)`), +the rendered pixel value is the same integer at default, but the browser resolves +`calc(... - ...)` through `var(--mui-spacing)` with floating-point precision +that doesn't always collapse to the exact same sub-pixel position as the integer +literal. Result: a tiny (~1% of pixels) anti-aliasing diff that breaks the +strict `maxDiffPixels: 0` gate even though the math is provably equal. + +**How to handle:** when the change is _purely_ a literal → calc rewrite (no +intended visual change), refresh the baseline (`COMPONENT= pnpm +spacing:shot:update`) — the derived form is the new ground truth, equality is by +construction. The strict pixel gate stays useful for any subsequent change to the +same component (it's now anchored to the calc form). diff --git a/docs/pages/experiments/spacing-fixture.tsx b/docs/pages/experiments/spacing-fixture.tsx new file mode 100644 index 00000000000000..c3c3af6aedd452 --- /dev/null +++ b/docs/pages/experiments/spacing-fixture.tsx @@ -0,0 +1,545 @@ +'use client'; +import * as React from 'react'; +import { useRouter } from 'next/router'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; +import Fab from '@mui/material/Fab'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import BottomNavigation from '@mui/material/BottomNavigation'; +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; +import Pagination from '@mui/material/Pagination'; +import List from '@mui/material/List'; +import ListSubheader from '@mui/material/ListSubheader'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Avatar from '@mui/material/Avatar'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import Autocomplete from '@mui/material/Autocomplete'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionActions from '@mui/material/AccordionActions'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Paper from '@mui/material/Paper'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogActions from '@mui/material/DialogActions'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import SnackbarContent from '@mui/material/SnackbarContent'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import MobileStepper from '@mui/material/MobileStepper'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import StepContent from '@mui/material/StepContent'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableFooter from '@mui/material/TableFooter'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import TablePagination from '@mui/material/TablePagination'; +import Badge from '@mui/material/Badge'; +import ImageListItemBar from '@mui/material/ImageListItemBar'; +import Chip from '@mui/material/Chip'; +import Checkbox from '@mui/material/Checkbox'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +// Local verification fixture for the spacing-derivation rollout. +// Used by scripts/spacing-screenshots. Renders one component's load-bearing +// matrix inside #spacing-scope; the harness overrides --mui-spacing on it. +// Add a component's matrix to `demos` before verifying it. Keep matrices tight +// — every variant/size that has a distinct spacing value, nothing else. +const theme = createTheme({ cssVariables: true }); + +const demos: Record = { + Button: ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + + + + + ))} + + ), + FilledInput: ( + + + + + + $, + endAdornment: kg, + }, + }} + /> + + + ), + ChipDemo: ( + + {(['medium', 'small'] as const).map((size) => ( + + + + A} /> + A} + /> + {}} /> + {}} /> + + ))} + + ), + BadgeDemo: ( + + + + + + + + + ), + ImageListItemBarDemo: ( + + + + + + + + + ), + TableDemo: ( + + {(['medium', 'small'] as const).map((size) => ( + + + + + + + + + Dessert + + + Calories + + + + + + + + Frozen yoghurt + 159 + + +
+ ))} +
+ ), + TablePaginationDemo: ( + + + + {}} + onRowsPerPageChange={() => {}} + /> + + +
+ ), + StepperDemo: ( + + + + One + + + Two + + + Three + + + + + Step one + Content for step one. + + + Step two + Content for step two. + + + + ), + CardFamily: ( + + R} + action={ + + + + } + title="Card title" + subheader="September 14, 2026" + /> + Card content text goes here for the body. + + + + + + ), + SnackbarContent: ( + + UNDO + + } + /> + ), + Breadcrumbs: ( + + Home + Catalog + Item + + ), + MobileStepperDemo: ( + Back} + nextButton={} + /> + ), + Accordion: ( + + + Expanded summary + Details content here. + + + + + + + Collapsed summary + Hidden details. + + + ), + Alert: ( + + Success message + {}}> + Info with close action + + + Warning + Warning with title and body text. + + + ), + DialogParts: ( + + Dialog title + + Dialog content text goes here. + + + + + + + ), + Autocomplete: ( + + } + /> + } + /> + } + /> + } + /> + } + /> + + ), + ListFamily: ( + + Subheader + + + + + + + + + A + + + + + + + + + + + + ), + MenuItem: ( + + Regular item + Another item + Dense item + Dense item two + + ), + BottomNavigationAction: ( + + } + /> + } + /> + + ), + PaginationItem: ( + + + + + + + ), + ToggleButton: ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + Left + Right + + ))} + + ), + Tab: ( + + + } + /> + } + /> + + ), + Fab: ( + + + Small + + + Medium + + Large + + ), + IconButton: ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + + + ))} + + + + + + + + ), + FormSpacing: ( + + + + + } label="One" /> + } label="Two" /> + + + } label="Start" labelPlacement="start" /> + } label="Start2" labelPlacement="start" /> + + + ), + InputAdornment: ( + + {(['filled', 'outlined', 'standard'] as const).map((variant) => ( + $, + endAdornment: kg, + }, + }} + /> + ))} + $, + endAdornment: kg, + }, + }} + /> + + ), + StandardInput: ( + + + + + + $, + endAdornment: kg, + }, + }} + /> + + + ), + TextField: ( + + {(['medium', 'small'] as const).map((size) => + (['outlined', 'filled', 'standard'] as const).map((variant) => ( + + )), + )} + $, + endAdornment: kg, + }, + }} + /> + + + ), +}; + +export default function SpacingFixture() { + const router = useRouter(); + const component = (router.query.c as string) || 'Button'; + const spacing = (router.query.spacing as string) || '8'; + const demo = demos[component] ??
No demo registered for "{component}".
; + return ( + + + {demo} + + + ); +} diff --git a/docs/src/modules/components/TemplateFrame.js b/docs/src/modules/components/TemplateFrame.js index df4dd1ab6cfad0..7eebdb841de2a7 100644 --- a/docs/src/modules/components/TemplateFrame.js +++ b/docs/src/modules/components/TemplateFrame.js @@ -21,6 +21,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import LightModeIcon from '@mui/icons-material/LightModeOutlined'; import DarkModeIcon from '@mui/icons-material/DarkModeOutlined'; import PaletteIcon from '@mui/icons-material/PaletteOutlined'; +import DensityMediumIcon from '@mui/icons-material/DensityMediumRounded'; import { codeSandbox, stackBlitz } from '@mui/internal-core-docs/Demo'; import sourceMaterialTemplates from 'docs/src/modules/material/sourceMaterialTemplates'; import { pascalCase } from '@mui/internal-core-docs/helpers'; @@ -172,6 +173,43 @@ export function ThemeSelector({ value, onChange }) { ); } +export function DensitySelector({ value, onChange }) { + return ( + + ); +} + const { palette: lightPalette, typography, ...designTokens } = getDesignTokens('light'); const { palette: darkPalette } = getDesignTokens('dark'); @@ -214,6 +252,14 @@ export default function TemplateFrame({ children }) { const templateId = router.pathname.split('/').pop(); const templateName = pascalCase(templateId); const [selectedTheme, setSelectedTheme] = React.useState('custom'); + const [spacing, setSpacing] = React.useState(8); + React.useEffect(() => { + const { style } = document.body; + style.setProperty('--mui-spacing', `${spacing}px`); + return () => { + style.removeProperty('--mui-spacing'); + }; + }, [spacing]); const materialTemplates = sourceMaterialTemplates(); const item = materialTemplates.map.get(templateId); return ( @@ -352,6 +398,7 @@ export default function TemplateFrame({ children }) { value={selectedTheme} onChange={(newTheme) => setSelectedTheme(newTheme)} /> + diff --git a/package.json b/package.json index b6d9ddf1e2da71..bae32fb1d72c2c 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "docs:build-color-preview": "babel-node scripts/buildColorTypes", "docs:deploy": "pnpm --filter docs run deploy", "docs:dev": "pnpm --filter docs dev", + "spacing:shot": "playwright test -c scripts/spacing-screenshots/playwright.config.mjs", + "spacing:shot:update": "playwright test -c scripts/spacing-screenshots/playwright.config.mjs --update-snapshots", "docs:icons": "pnpm --filter docs icons", "docs:size-why": "cross-env DOCS_STATS_ENABLED=true pnpm docs:build", "docs:start": "pnpm --filter docs start", diff --git a/packages/mui-material/src/Accordion/Accordion.js b/packages/mui-material/src/Accordion/Accordion.js index e072881bde2ba3..eb5b5e48df04f4 100644 --- a/packages/mui-material/src/Accordion/Accordion.js +++ b/packages/mui-material/src/Accordion/Accordion.js @@ -118,7 +118,7 @@ const AccordionRoot = styled(Paper, { props: (props) => !props.disableGutters, style: { [`&.${accordionClasses.expanded}`]: { - margin: '16px 0', + margin: `${theme.spacing(2)} 0`, }, }, }, diff --git a/packages/mui-material/src/AccordionActions/AccordionActions.js b/packages/mui-material/src/AccordionActions/AccordionActions.js index df67d059a1d7d8..5c66e9c76cb92f 100644 --- a/packages/mui-material/src/AccordionActions/AccordionActions.js +++ b/packages/mui-material/src/AccordionActions/AccordionActions.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getAccordionActionsUtilityClass } from './accordionActionsClasses'; @@ -25,22 +26,24 @@ const AccordionActionsRoot = styled('div', { return [styles.root, !ownerState.disableSpacing && styles.spacing]; }, -})({ - display: 'flex', - alignItems: 'center', - padding: 8, - justifyContent: 'flex-end', - variants: [ - { - props: (props) => !props.disableSpacing, - style: { - '& > :not(style) ~ :not(style)': { - marginLeft: 8, +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + justifyContent: 'flex-end', + variants: [ + { + props: (props) => !props.disableSpacing, + style: { + '& > :not(style) ~ :not(style)': { + marginLeft: theme.spacing(1), + }, }, }, - }, - ], -}); + ], + })), +); const AccordionActions = React.forwardRef(function AccordionActions(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiAccordionActions' }); diff --git a/packages/mui-material/src/AccordionSummary/AccordionSummary.js b/packages/mui-material/src/AccordionSummary/AccordionSummary.js index 2027b0d8b5cde2..d4e99d804ac80a 100644 --- a/packages/mui-material/src/AccordionSummary/AccordionSummary.js +++ b/packages/mui-material/src/AccordionSummary/AccordionSummary.js @@ -72,7 +72,7 @@ const AccordionSummaryContent = styled('span', { display: 'flex', textAlign: 'start', flexGrow: 1, - margin: '12px 0', + margin: `calc(${theme.spacing(2)} - 4px) 0`, variants: [ { props: (props) => !props.disableGutters, @@ -81,7 +81,7 @@ const AccordionSummaryContent = styled('span', { duration: theme.transitions.duration.shortest, }), [`&.${accordionSummaryClasses.expanded}`]: { - margin: '20px 0', + margin: `calc(${theme.spacing(3)} - 4px) 0`, }, }, }, diff --git a/packages/mui-material/src/Alert/Alert.js b/packages/mui-material/src/Alert/Alert.js index 5f407b2f5d2e65..e59d7eef7e6b04 100644 --- a/packages/mui-material/src/Alert/Alert.js +++ b/packages/mui-material/src/Alert/Alert.js @@ -47,7 +47,7 @@ const AlertRoot = styled(Paper, { ...theme.typography.body2, backgroundColor: 'transparent', display: 'flex', - padding: '6px 16px', + padding: `calc(${theme.spacing(1)} - 2px) ${theme.spacing(2)}`, variants: [ ...Object.entries(theme.palette) .filter(createSimplePaletteValueFilter(['light'])) @@ -111,33 +111,39 @@ const AlertRoot = styled(Paper, { const AlertIcon = styled('div', { name: 'MuiAlert', slot: 'Icon', -})({ - marginRight: 12, - padding: '7px 0', - display: 'flex', - fontSize: 22, - opacity: 0.9, -}); +})( + memoTheme(({ theme }) => ({ + marginRight: `calc(${theme.spacing(2)} - 4px)`, + padding: `calc(${theme.spacing(1)} - 1px) 0`, + display: 'flex', + fontSize: 22, + opacity: 0.9, + })), +); const AlertMessage = styled('div', { name: 'MuiAlert', slot: 'Message', -})({ - padding: '8px 0', - minWidth: 0, - overflow: 'auto', -}); +})( + memoTheme(({ theme }) => ({ + padding: `${theme.spacing(1)} 0`, + minWidth: 0, + overflow: 'auto', + })), +); const AlertAction = styled('div', { name: 'MuiAlert', slot: 'Action', -})({ - display: 'flex', - alignItems: 'flex-start', - padding: '4px 0 0 16px', - marginLeft: 'auto', - marginRight: -8, -}); +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'flex-start', + padding: `calc(${theme.spacing(1)} - 4px) 0 0 ${theme.spacing(2)}`, + marginLeft: 'auto', + marginRight: theme.spacing(-1), + })), +); const defaultIconMapping = { success: , diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index dd8b8427de196c..eb511fea3273d0 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -86,144 +86,147 @@ const AutocompleteRoot = styled('div', { hasClearIcon && styles.hasClearIcon, ]; }, -})({ - [`&.${autocompleteClasses.focused} .${autocompleteClasses.clearIndicator}`]: { - visibility: 'visible', - }, - /* Avoid double tap issue on iOS */ - '@media (pointer: fine)': { - [`&:hover .${autocompleteClasses.clearIndicator}`]: { +})( + memoTheme(({ theme }) => ({ + [`&.${autocompleteClasses.focused} .${autocompleteClasses.clearIndicator}`]: { visibility: 'visible', }, - }, - [`& .${autocompleteClasses.tag}`]: { - margin: 3, - maxWidth: 'calc(100% - 6px)', - }, - [`& .${autocompleteClasses.inputRoot}`]: { - [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 26 + 4, - }, - [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 52 + 4, - }, - [`& .${autocompleteClasses.input}`]: { - width: 0, - minWidth: 30, - }, - }, - [`& .${inputClasses.root}`]: { - paddingBottom: 1, - '& .MuiInput-input': { - padding: '4px 4px 4px 0px', - }, - }, - [`& .${inputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - [`& .${inputClasses.input}`]: { - padding: '2px 4px 3px 0', - }, - }, - [`& .${outlinedInputClasses.root}`]: { - padding: 9, - [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 26 + 4 + 9, + /* Avoid double tap issue on iOS */ + '@media (pointer: fine)': { + [`&:hover .${autocompleteClasses.clearIndicator}`]: { + visibility: 'visible', + }, }, - [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 52 + 4 + 9, + [`& .${autocompleteClasses.tag}`]: { + margin: 3, + maxWidth: 'calc(100% - 6px)', }, - [`& .${autocompleteClasses.input}`]: { - padding: '7.5px 4px 7.5px 5px', + [`& .${autocompleteClasses.inputRoot}`]: { + [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 26 + 4, + }, + [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 52 + 4, + }, + [`& .${autocompleteClasses.input}`]: { + width: 0, + minWidth: 30, + }, }, - [`& .${autocompleteClasses.endAdornment}`]: { - right: 9, + [`& .${inputClasses.root}`]: { + paddingBottom: 1, + '& .MuiInput-input': { + padding: `calc(${theme.spacing(1)} - 4px) 4px calc(${theme.spacing(1)} - 4px) 0px`, + }, }, - }, - [`& .${outlinedInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - // Don't specify paddingRight, as it overrides the default value set when there is only - // one of the popup or clear icon as the specificity is equal so the latter one wins - paddingTop: 6, - paddingBottom: 6, - paddingLeft: 6, - [`& .${autocompleteClasses.input}`]: { - padding: '2.5px 4px 2.5px 8px', + [`& .${inputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + [`& .${inputClasses.input}`]: { + padding: '2px 4px 3px 0', + }, }, - }, - [`& .${filledInputClasses.root}`]: { - paddingTop: 19, - paddingLeft: 8, - [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 26 + 4 + 9, + [`& .${outlinedInputClasses.root}`]: { + padding: `calc(${theme.spacing(1)} + 1px) 9px`, + [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 26 + 4 + 9, + }, + [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 52 + 4 + 9, + }, + [`& .${autocompleteClasses.input}`]: { + padding: `calc(${theme.spacing(1)} - 0.5px) 4px calc(${theme.spacing(1)} - 0.5px) 5px`, + }, + [`& .${autocompleteClasses.endAdornment}`]: { + right: 9, + }, }, - [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 52 + 4 + 9, + [`& .${outlinedInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + // Don't specify paddingRight, as it overrides the default value set when there is only + // one of the popup or clear icon as the specificity is equal so the latter one wins + paddingTop: `calc(${theme.spacing(1)} - 2px)`, + paddingBottom: `calc(${theme.spacing(1)} - 2px)`, + paddingLeft: 6, + [`& .${autocompleteClasses.input}`]: { + padding: '2.5px 4px 2.5px 8px', + }, }, - [`& .${filledInputClasses.input}`]: { - padding: '7px 4px', + [`& .${filledInputClasses.root}`]: { + paddingTop: `calc(${theme.spacing(2)} + 3px)`, + paddingLeft: 8, + [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 26 + 4 + 9, + }, + [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 52 + 4 + 9, + }, + [`& .${filledInputClasses.input}`]: { + padding: `calc(${theme.spacing(1)} - 1px) 4px`, + }, + [`& .${autocompleteClasses.endAdornment}`]: { + right: 9, + }, }, - [`& .${autocompleteClasses.endAdornment}`]: { - right: 9, + [`& .${filledInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + paddingBottom: 1, + [`& .${filledInputClasses.input}`]: { + padding: '2.5px 4px', + }, }, - }, - [`& .${filledInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - paddingBottom: 1, - [`& .${filledInputClasses.input}`]: { - padding: '2.5px 4px', + [`& .${inputBaseClasses.hiddenLabel}`]: { + paddingTop: theme.spacing(1), }, - }, - [`& .${inputBaseClasses.hiddenLabel}`]: { - paddingTop: 8, - }, - [`& .${filledInputClasses.root}.${inputBaseClasses.hiddenLabel}`]: { - paddingTop: 0, - paddingBottom: 0, - [`& .${autocompleteClasses.input}`]: { - paddingTop: 16, - paddingBottom: 17, + [`& .${filledInputClasses.root}.${inputBaseClasses.hiddenLabel}`]: { + paddingTop: 0, + paddingBottom: 0, + [`& .${autocompleteClasses.input}`]: { + paddingTop: theme.spacing(2), + paddingBottom: `calc(${theme.spacing(2)} + 1px)`, + }, }, - }, - [`& .${filledInputClasses.root}.${inputBaseClasses.hiddenLabel}.${inputBaseClasses.sizeSmall}`]: { + [`& .${filledInputClasses.root}.${inputBaseClasses.hiddenLabel}.${inputBaseClasses.sizeSmall}`]: + { + [`& .${autocompleteClasses.input}`]: { + paddingTop: theme.spacing(1), + paddingBottom: `calc(${theme.spacing(1)} + 1px)`, + }, + }, [`& .${autocompleteClasses.input}`]: { - paddingTop: 8, - paddingBottom: 9, + flexGrow: 1, + textOverflow: 'ellipsis', + opacity: 0, }, - }, - [`& .${autocompleteClasses.input}`]: { - flexGrow: 1, - textOverflow: 'ellipsis', - opacity: 0, - }, - variants: [ - { - props: { fullWidth: true }, - style: { width: '100%' }, - }, - { - props: { size: 'small' }, - style: { - [`& .${autocompleteClasses.tag}`]: { - margin: 2, - maxWidth: 'calc(100% - 4px)', + variants: [ + { + props: { fullWidth: true }, + style: { width: '100%' }, + }, + { + props: { size: 'small' }, + style: { + [`& .${autocompleteClasses.tag}`]: { + margin: 2, + maxWidth: 'calc(100% - 4px)', + }, }, }, - }, - { - props: { inputFocused: true }, - style: { - [`& .${autocompleteClasses.input}`]: { - opacity: 1, + { + props: { inputFocused: true }, + style: { + [`& .${autocompleteClasses.input}`]: { + opacity: 1, + }, }, }, - }, - { - props: { multiple: true }, - style: { - [`& .${autocompleteClasses.inputRoot}`]: { - flexWrap: 'wrap', + { + props: { multiple: true }, + style: { + [`& .${autocompleteClasses.inputRoot}`]: { + flexWrap: 'wrap', + }, }, }, - }, - ], -}); + ], + })), +); const AutocompleteEndAdornment = styled('div', { name: 'MuiAutocomplete', @@ -308,7 +311,7 @@ const AutocompleteLoading = styled('div', { })( memoTheme(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, - padding: '14px 16px', + padding: `calc(${theme.spacing(2)} - 2px) 16px`, })), ); @@ -318,7 +321,7 @@ const AutocompleteNoOptions = styled('div', { })( memoTheme(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, - padding: '14px 16px', + padding: `calc(${theme.spacing(2)} - 2px) 16px`, })), ); @@ -329,7 +332,7 @@ const AutocompleteListbox = styled('ul', { memoTheme(({ theme }) => ({ listStyle: 'none', margin: 0, - padding: '8px 0', + padding: `${theme.spacing(1)} 0`, maxHeight: '40vh', overflow: 'auto', isolation: 'isolate', // Prevent overlap with iOS overlay scrollbars. @@ -341,11 +344,11 @@ const AutocompleteListbox = styled('ul', { justifyContent: 'flex-start', alignItems: 'center', cursor: 'pointer', - paddingTop: 6, + paddingTop: `calc(${theme.spacing(1)} - 2px)`, boxSizing: 'border-box', outline: '0', WebkitTapHighlightColor: 'transparent', - paddingBottom: 6, + paddingBottom: `calc(${theme.spacing(1)} - 2px)`, paddingLeft: 16, paddingRight: 16, [theme.breakpoints.up('sm')]: { diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 3fb47d06002666..f8d652beb4b596 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -81,7 +81,7 @@ const BadgeBadge = styled('span', { fontSize: theme.typography.pxToRem(12), minWidth: RADIUS_STANDARD * 2, lineHeight: 1, - padding: '0 6px', + padding: `0 calc(${theme.spacing(1)} - 2px)`, height: RADIUS_STANDARD * 2, borderRadius: RADIUS_STANDARD, zIndex: 1, // Render the badge on top of potential ripples. diff --git a/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js b/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js index b0bdacc3e6df33..95e1e3957d2bd1 100644 --- a/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js +++ b/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js @@ -37,7 +37,7 @@ const BottomNavigationActionRoot = styled(ButtonBase, { transition: theme.transitions.create(['color', 'padding-top'], { duration: theme.transitions.duration.short, }), - padding: '0px 12px', + padding: `0px calc(${theme.spacing(2)} - 4px)`, minWidth: 80, maxWidth: 168, color: (theme.vars || theme).palette.text.secondary, @@ -50,7 +50,7 @@ const BottomNavigationActionRoot = styled(ButtonBase, { { props: ({ showLabel, selected }) => !showLabel && !selected, style: { - paddingTop: 14, + paddingTop: `calc(${theme.spacing(2)} - 2px)`, }, }, { diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js index 63cb6bae7f3538..1a37aabc361d64 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js @@ -7,6 +7,7 @@ import integerPropType from '@mui/utils/integerPropType'; import composeClasses from '@mui/utils/composeClasses'; import useSlotProps from '@mui/utils/useSlotProps'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import Typography from '../Typography'; import BreadcrumbCollapsed from './BreadcrumbCollapsed'; @@ -48,12 +49,14 @@ const BreadcrumbsOl = styled('ol', { const BreadcrumbsSeparator = styled('li', { name: 'MuiBreadcrumbs', slot: 'Separator', -})({ - display: 'flex', - userSelect: 'none', - marginLeft: 8, - marginRight: 8, -}); +})( + memoTheme(({ theme }) => ({ + display: 'flex', + userSelect: 'none', + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + })), +); function insertSeparators(items, className, separator, ownerState) { return items.reduce((acc, current, index) => { diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index cbed494f1e7399..a020b5beb201c0 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -100,7 +100,7 @@ const ButtonRoot = styled(ButtonBase, { return { ...theme.typography.button, minWidth: 64, - padding: '6px 16px', + padding: `calc(${theme.spacing(1)} - 2px) ${theme.spacing(2)}`, border: 0, borderRadius: (theme.vars || theme).shape.borderRadius, transition: theme.transitions.create( @@ -145,7 +145,7 @@ const ButtonRoot = styled(ButtonBase, { { props: { variant: 'outlined' }, style: { - padding: '5px 15px', + padding: `calc(${theme.spacing(1)} - 3px) calc(${theme.spacing(2)} - 1px)`, border: '1px solid currentColor', borderColor: `var(--variant-outlinedBorder, currentColor)`, backgroundColor: `var(--variant-outlinedBg)`, @@ -158,7 +158,7 @@ const ButtonRoot = styled(ButtonBase, { { props: { variant: 'text' }, style: { - padding: '6px 8px', + padding: `calc(${theme.spacing(1)} - 2px) ${theme.spacing(1)}`, color: `var(--variant-textColor)`, backgroundColor: `var(--variant-textBg)`, }, @@ -225,7 +225,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'text', }, style: { - padding: '4px 5px', + padding: `calc(${theme.spacing(1)} - 4px) calc(${theme.spacing(1)} - 3px)`, fontSize: theme.typography.pxToRem(13), }, }, @@ -235,7 +235,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'text', }, style: { - padding: '8px 11px', + padding: `${theme.spacing(1)} calc(${theme.spacing(1)} + 3px)`, fontSize: theme.typography.pxToRem(15), }, }, @@ -245,7 +245,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'outlined', }, style: { - padding: '3px 9px', + padding: `calc(${theme.spacing(1)} - 5px) calc(${theme.spacing(1)} + 1px)`, fontSize: theme.typography.pxToRem(13), }, }, @@ -255,7 +255,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'outlined', }, style: { - padding: '7px 21px', + padding: `calc(${theme.spacing(1)} - 1px) calc(${theme.spacing(3)} - 3px)`, fontSize: theme.typography.pxToRem(15), }, }, @@ -265,7 +265,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'contained', }, style: { - padding: '4px 10px', + padding: `calc(${theme.spacing(1)} - 4px) calc(${theme.spacing(1)} + 2px)`, fontSize: theme.typography.pxToRem(13), }, }, @@ -275,7 +275,7 @@ const ButtonRoot = styled(ButtonBase, { variant: 'contained', }, style: { - padding: '8px 22px', + padding: `${theme.spacing(1)} calc(${theme.spacing(3)} - 2px)`, fontSize: theme.typography.pxToRem(15), }, }, diff --git a/packages/mui-material/src/CardActions/CardActions.js b/packages/mui-material/src/CardActions/CardActions.js index 5213656597c565..2726ef91605402 100644 --- a/packages/mui-material/src/CardActions/CardActions.js +++ b/packages/mui-material/src/CardActions/CardActions.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getCardActionsUtilityClass } from './cardActionsClasses'; @@ -25,21 +26,23 @@ const CardActionsRoot = styled('div', { return [styles.root, !ownerState.disableSpacing && styles.spacing]; }, -})({ - display: 'flex', - alignItems: 'center', - padding: 8, - variants: [ - { - props: { disableSpacing: false }, - style: { - '& > :not(style) ~ :not(style)': { - marginLeft: 8, +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + variants: [ + { + props: { disableSpacing: false }, + style: { + '& > :not(style) ~ :not(style)': { + marginLeft: theme.spacing(1), + }, }, }, - }, - ], -}); + ], + })), +); const CardActions = React.forwardRef(function CardActions(inProps, ref) { const props = useDefaultProps({ diff --git a/packages/mui-material/src/CardContent/CardContent.js b/packages/mui-material/src/CardContent/CardContent.js index 769163a78edbad..2574cb4ffecb64 100644 --- a/packages/mui-material/src/CardContent/CardContent.js +++ b/packages/mui-material/src/CardContent/CardContent.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getCardContentUtilityClass } from './cardContentClasses'; @@ -20,12 +21,14 @@ const useUtilityClasses = (ownerState) => { const CardContentRoot = styled('div', { name: 'MuiCardContent', slot: 'Root', -})({ - padding: 16, - '&:last-child': { - paddingBottom: 24, - }, -}); +})( + memoTheme(({ theme }) => ({ + padding: theme.spacing(2), + '&:last-child': { + paddingBottom: theme.spacing(3), + }, + })), +); const CardContent = React.forwardRef(function CardContent(inProps, ref) { const props = useDefaultProps({ diff --git a/packages/mui-material/src/CardHeader/CardHeader.js b/packages/mui-material/src/CardHeader/CardHeader.js index 9d3690cfcd5aa8..8831352dd74e87 100644 --- a/packages/mui-material/src/CardHeader/CardHeader.js +++ b/packages/mui-material/src/CardHeader/CardHeader.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import composeClasses from '@mui/utils/composeClasses'; import Typography, { typographyClasses } from '../Typography'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import cardHeaderClasses, { getCardHeaderUtilityClass } from './cardHeaderClasses'; import useSlot from '../utils/useSlot'; @@ -33,31 +34,37 @@ const CardHeaderRoot = styled('div', { styles.root, ]; }, -})({ - display: 'flex', - alignItems: 'center', - padding: 16, -}); +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(2), + })), +); const CardHeaderAvatar = styled('div', { name: 'MuiCardHeader', slot: 'Avatar', -})({ - display: 'flex', - flex: '0 0 auto', - marginRight: 16, -}); +})( + memoTheme(({ theme }) => ({ + display: 'flex', + flex: '0 0 auto', + marginRight: theme.spacing(2), + })), +); const CardHeaderAction = styled('div', { name: 'MuiCardHeader', slot: 'Action', -})({ - flex: '0 0 auto', - alignSelf: 'flex-start', - marginTop: -4, - marginRight: -8, - marginBottom: -4, -}); +})( + memoTheme(({ theme }) => ({ + flex: '0 0 auto', + alignSelf: 'flex-start', + marginTop: -4, + marginRight: theme.spacing(-1), + marginBottom: -4, + })), +); const CardHeaderContent = styled('div', { name: 'MuiCardHeader', diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index 9723bdecf42fa9..48ec32e5533d12 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -72,7 +72,7 @@ const ChipRoot = styled('div', { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - height: 32, + height: `calc(${theme.spacing(2)} + 16px)`, // 32px lineHeight: 1.5, color: (theme.vars || theme).palette.text.primary, backgroundColor: (theme.vars || theme).palette.action.selected, @@ -93,23 +93,23 @@ const ChipRoot = styled('div', { pointerEvents: 'none', }, [`& .${chipClasses.avatar}`]: { - marginLeft: 5, - marginRight: -6, - width: 24, - height: 24, + marginLeft: `max(calc(${theme.spacing(1)} - 3px), 0px)`, + marginRight: `calc(2px - ${theme.spacing(1)})`, + width: `calc(16px + ${theme.spacing(1)})`, + height: `calc(16px + ${theme.spacing(1)})`, color: theme.vars ? theme.vars.palette.Chip.defaultAvatarColor : textColor, fontSize: theme.typography.pxToRem(12), }, [`& .${chipClasses.icon}`]: { - marginLeft: 5, - marginRight: -6, + marginLeft: `max(calc(${theme.spacing(1)} - 3px), 0px)`, + marginRight: `calc(2px - ${theme.spacing(1)})`, }, [`& .${chipClasses.deleteIcon}`]: { WebkitTapHighlightColor: 'transparent', color: theme.alpha((theme.vars || theme).palette.text.primary, 0.26), - fontSize: 22, + fontSize: `calc(14px + ${theme.spacing(1)})`, cursor: 'pointer', - margin: '0 5px 0 -6px', + margin: `0 max(calc(${theme.spacing(1)} - 3px), 0px) 0 calc(2px - ${theme.spacing(1)})`, '&:hover': { color: theme.alpha((theme.vars || theme).palette.text.primary, 0.4), }, @@ -140,23 +140,23 @@ const ChipRoot = styled('div', { { props: { size: 'small' }, style: { - height: 24, + height: `calc(${theme.spacing(2)} + 8px)`, // 32px [`& .${chipClasses.avatar}`]: { - marginLeft: 4, - marginRight: -4, - width: 18, - height: 18, + marginLeft: `max(calc(${theme.spacing(1)} - 4px), 0px)`, + marginRight: `calc(4px - ${theme.spacing(1)})`, + width: `calc(${theme.spacing(1)} + 10px)`, + height: `calc(${theme.spacing(1)} + 10px)`, fontSize: theme.typography.pxToRem(10), }, [`& .${chipClasses.icon}`]: { - fontSize: 18, - marginLeft: 4, - marginRight: -4, + fontSize: `calc(${theme.spacing(1)} + 10px)`, + marginLeft: `calc(${theme.spacing(1)} - 4px)`, + marginRight: `calc(4px - ${theme.spacing(1)})`, }, [`& .${chipClasses.deleteIcon}`]: { - fontSize: 16, - marginRight: 4, - marginLeft: -4, + fontSize: `calc(8px + ${theme.spacing(1)})`, + marginRight: `max(calc(${theme.spacing(1)} - 4px), 0px)`, + marginLeft: `calc(4px - ${theme.spacing(1)})`, }, }, }, @@ -265,13 +265,13 @@ const ChipRoot = styled('div', { backgroundColor: (theme.vars || theme).palette.action.focus, }, [`& .${chipClasses.avatar}`]: { - marginLeft: 4, + marginLeft: `calc(${theme.spacing(1)} - 4px)`, }, [`& .${chipClasses.icon}`]: { - marginLeft: 4, + marginLeft: `calc(${theme.spacing(1)} - 4px)`, }, [`& .${chipClasses.deleteIcon}`]: { - marginRight: 5, + marginRight: `calc(${theme.spacing(1)} - 3px)`, }, }, }, @@ -279,13 +279,13 @@ const ChipRoot = styled('div', { props: { size: 'small', variant: 'outlined' }, style: { [`& .${chipClasses.avatar}`]: { - marginLeft: 2, + marginLeft: `calc(${theme.spacing(1)} - 6px)`, }, [`& .${chipClasses.icon}`]: { - marginLeft: 2, + marginLeft: `calc(${theme.spacing(1)} - 6px)`, }, [`& .${chipClasses.deleteIcon}`]: { - marginRight: 3, + marginRight: `calc(${theme.spacing(1)} - 5px)`, }, }, }, @@ -324,36 +324,38 @@ const ChipRoot = styled('div', { const ChipLabel = styled('span', { name: 'MuiChip', slot: 'Label', -})({ - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingLeft: 12, - paddingRight: 12, - whiteSpace: 'nowrap', - variants: [ - { - props: { variant: 'outlined' }, - style: { - paddingLeft: 11, - paddingRight: 11, +})( + memoTheme(({ theme }) => ({ + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingLeft: `calc(${theme.spacing(2)} - 4px)`, + paddingRight: `calc(${theme.spacing(2)} - 4px)`, + whiteSpace: 'nowrap', + variants: [ + { + props: { variant: 'outlined' }, + style: { + paddingLeft: `calc(${theme.spacing(1)} + 3px)`, + paddingRight: `calc(${theme.spacing(1)} + 3px)`, + }, }, - }, - { - props: { size: 'small' }, - style: { - paddingLeft: 8, - paddingRight: 8, + { + props: { size: 'small' }, + style: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, }, - }, - { - props: { size: 'small', variant: 'outlined' }, - style: { - paddingLeft: 7, - paddingRight: 7, + { + props: { size: 'small', variant: 'outlined' }, + style: { + paddingLeft: `calc(${theme.spacing(1)} - 1px)`, + paddingRight: `calc(${theme.spacing(1)} - 1px)`, + }, }, - }, - ], -}); + ], + })), +); function isDeleteKeyboardEvent(keyboardEvent) { return keyboardEvent.key === 'Backspace' || keyboardEvent.key === 'Delete'; diff --git a/packages/mui-material/src/DialogActions/DialogActions.js b/packages/mui-material/src/DialogActions/DialogActions.js index 8e1b730bc4c096..019caa715cb023 100644 --- a/packages/mui-material/src/DialogActions/DialogActions.js +++ b/packages/mui-material/src/DialogActions/DialogActions.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getDialogActionsUtilityClass } from './dialogActionsClasses'; @@ -25,23 +26,25 @@ const DialogActionsRoot = styled('div', { return [styles.root, !ownerState.disableSpacing && styles.spacing]; }, -})({ - display: 'flex', - alignItems: 'center', - padding: 8, - justifyContent: 'flex-end', - flex: '0 0 auto', - variants: [ - { - props: ({ ownerState }) => !ownerState.disableSpacing, - style: { - '& > :not(style) ~ :not(style)': { - marginLeft: 8, +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + justifyContent: 'flex-end', + flex: '0 0 auto', + variants: [ + { + props: ({ ownerState }) => !ownerState.disableSpacing, + style: { + '& > :not(style) ~ :not(style)': { + marginLeft: theme.spacing(1), + }, }, }, - }, - ], -}); + ], + })), +); const DialogActions = React.forwardRef(function DialogActions(inProps, ref) { const props = useDefaultProps({ diff --git a/packages/mui-material/src/DialogContent/DialogContent.js b/packages/mui-material/src/DialogContent/DialogContent.js index c5acfb7c3ef304..2d57f27d2d0a71 100644 --- a/packages/mui-material/src/DialogContent/DialogContent.js +++ b/packages/mui-material/src/DialogContent/DialogContent.js @@ -33,12 +33,12 @@ const DialogContentRoot = styled('div', { // Add iOS momentum scrolling for iOS < 13.0 WebkitOverflowScrolling: 'touch', overflowY: 'auto', - padding: '20px 24px', + padding: `calc(${theme.spacing(3)} - 4px) ${theme.spacing(3)}`, variants: [ { props: ({ ownerState }) => ownerState.dividers, style: { - padding: '16px 24px', + padding: `${theme.spacing(2)} ${theme.spacing(3)}`, borderTop: `1px solid ${(theme.vars || theme).palette.divider}`, borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`, }, diff --git a/packages/mui-material/src/DialogTitle/DialogTitle.js b/packages/mui-material/src/DialogTitle/DialogTitle.js index b40d2f5af7b2ca..8689f6cecad57d 100644 --- a/packages/mui-material/src/DialogTitle/DialogTitle.js +++ b/packages/mui-material/src/DialogTitle/DialogTitle.js @@ -5,6 +5,7 @@ import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import Typography from '../Typography'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getDialogTitleUtilityClass } from './dialogTitleClasses'; import DialogContext from '../Dialog/DialogContext'; @@ -22,10 +23,12 @@ const useUtilityClasses = (ownerState) => { const DialogTitleRoot = styled(Typography, { name: 'MuiDialogTitle', slot: 'Root', -})({ - padding: '16px 24px', - flex: '0 0 auto', -}); +})( + memoTheme(({ theme }) => ({ + padding: `${theme.spacing(2)} ${theme.spacing(3)}`, + flex: '0 0 auto', + })), +); const DialogTitle = React.forwardRef(function DialogTitle(inProps, ref) { const props = useDefaultProps({ diff --git a/packages/mui-material/src/Fab/Fab.js b/packages/mui-material/src/Fab/Fab.js index 3e15961383cd53..19fc9bdf70dbb1 100644 --- a/packages/mui-material/src/Fab/Fab.js +++ b/packages/mui-material/src/Fab/Fab.js @@ -99,7 +99,7 @@ const FabRoot = styled(ButtonBase, { props: { variant: 'extended' }, style: { borderRadius: 48 / 2, - padding: '0 16px', + padding: `0 ${theme.spacing(2)}`, width: 'auto', minHeight: 'auto', minWidth: 48, @@ -110,7 +110,7 @@ const FabRoot = styled(ButtonBase, { props: { variant: 'extended', size: 'small' }, style: { width: 'auto', - padding: '0 8px', + padding: `0 ${theme.spacing(1)}`, borderRadius: 34 / 2, minWidth: 34, height: 34, @@ -120,7 +120,7 @@ const FabRoot = styled(ButtonBase, { props: { variant: 'extended', size: 'medium' }, style: { width: 'auto', - padding: '0 16px', + padding: `0 ${theme.spacing(2)}`, borderRadius: 40 / 2, minWidth: 40, height: 40, diff --git a/packages/mui-material/src/FilledInput/FilledInput.js b/packages/mui-material/src/FilledInput/FilledInput.js index e70b47cde933dd..3a9ec50894fab7 100644 --- a/packages/mui-material/src/FilledInput/FilledInput.js +++ b/packages/mui-material/src/FilledInput/FilledInput.js @@ -168,29 +168,29 @@ const FilledInputRoot = styled(InputBaseRoot, { { props: ({ ownerState }) => ownerState.multiline, style: { - padding: '25px 12px 8px', + padding: `calc(${theme.spacing(3)} + 1px) 12px ${theme.spacing(1)}`, }, }, { props: ({ ownerState, size }) => ownerState.multiline && size === 'small', style: { - paddingTop: 21, - paddingBottom: 4, + paddingTop: `calc(${theme.spacing(3)} - 3px)`, + paddingBottom: `calc(${theme.spacing(1)} - 4px)`, }, }, { props: ({ ownerState }) => ownerState.multiline && ownerState.hiddenLabel, style: { - paddingTop: 16, - paddingBottom: 17, + paddingTop: theme.spacing(2), + paddingBottom: `calc(${theme.spacing(2)} + 1px)`, }, }, { props: ({ ownerState }) => ownerState.multiline && ownerState.hiddenLabel && ownerState.size === 'small', style: { - paddingTop: 8, - paddingBottom: 9, + paddingTop: theme.spacing(1), + paddingBottom: `calc(${theme.spacing(1)} + 1px)`, }, }, ], @@ -204,9 +204,9 @@ const FilledInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - paddingTop: 25, + paddingTop: `calc(${theme.spacing(3)} + 1px)`, paddingRight: 12, - paddingBottom: 8, + paddingBottom: theme.spacing(1), paddingLeft: 12, '&:-webkit-autofill': { ...(!theme.vars && { @@ -229,15 +229,15 @@ const FilledInputInput = styled(InputBaseInput, { size: 'small', }, style: { - paddingTop: 21, - paddingBottom: 4, + paddingTop: `calc(${theme.spacing(3)} - 3px)`, + paddingBottom: `calc(${theme.spacing(1)} - 4px)`, }, }, { props: ({ ownerState }) => ownerState.hiddenLabel, style: { - paddingTop: 16, - paddingBottom: 17, + paddingTop: theme.spacing(2), + paddingBottom: `calc(${theme.spacing(2)} + 1px)`, }, }, { @@ -255,8 +255,8 @@ const FilledInputInput = styled(InputBaseInput, { { props: ({ ownerState }) => ownerState.hiddenLabel && ownerState.size === 'small', style: { - paddingTop: 8, - paddingBottom: 9, + paddingTop: theme.spacing(1), + paddingBottom: `calc(${theme.spacing(1)} + 1px)`, }, }, { diff --git a/packages/mui-material/src/FormControl/FormControl.js b/packages/mui-material/src/FormControl/FormControl.js index f05e75e14872ce..581bfc112ba3b7 100644 --- a/packages/mui-material/src/FormControl/FormControl.js +++ b/packages/mui-material/src/FormControl/FormControl.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { isFilled, isAdornedStart } from '../InputBase/utils'; import capitalize from '../utils/capitalize'; @@ -32,39 +33,41 @@ const FormControlRoot = styled('div', { ownerState.fullWidth && styles.fullWidth, ]; }, -})({ - display: 'inline-flex', - flexDirection: 'column', - position: 'relative', - // Reset fieldset default style. - minWidth: 0, - padding: 0, - margin: 0, - border: 0, - verticalAlign: 'top', // Fix alignment issue on Safari. - variants: [ - { - props: { margin: 'normal' }, - style: { - marginTop: 16, - marginBottom: 8, +})( + memoTheme(({ theme }) => ({ + display: 'inline-flex', + flexDirection: 'column', + position: 'relative', + // Reset fieldset default style. + minWidth: 0, + padding: 0, + margin: 0, + border: 0, + verticalAlign: 'top', // Fix alignment issue on Safari. + variants: [ + { + props: { margin: 'normal' }, + style: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + }, }, - }, - { - props: { margin: 'dense' }, - style: { - marginTop: 8, - marginBottom: 4, + { + props: { margin: 'dense' }, + style: { + marginTop: theme.spacing(1), + marginBottom: `calc(${theme.spacing(1)} - 4px)`, + }, }, - }, - { - props: { fullWidth: true }, - style: { - width: '100%', + { + props: { fullWidth: true }, + style: { + width: '100%', + }, }, - }, - ], -}); + ], + })), +); /** * Provides context such as filled/focused/error/required for form inputs. diff --git a/packages/mui-material/src/FormControlLabel/FormControlLabel.js b/packages/mui-material/src/FormControlLabel/FormControlLabel.js index eec629368023e3..5727f6f555c891 100644 --- a/packages/mui-material/src/FormControlLabel/FormControlLabel.js +++ b/packages/mui-material/src/FormControlLabel/FormControlLabel.js @@ -52,8 +52,8 @@ export const FormControlLabelRoot = styled('label', { // For correct alignment with the text. verticalAlign: 'middle', WebkitTapHighlightColor: 'transparent', - marginLeft: -11, - marginRight: 16, // used for row presentation of radio/checkbox + marginLeft: `calc(${theme.spacing(-1)} - 3px)`, // compensates the control's icon-button padding (geometry) + marginRight: theme.spacing(2), // gap used for row presentation of radio/checkbox [`&.${formControlLabelClasses.disabled}`]: { cursor: 'default', }, @@ -67,7 +67,7 @@ export const FormControlLabelRoot = styled('label', { props: { labelPlacement: 'start' }, style: { flexDirection: 'row-reverse', - marginRight: -11, + marginRight: `calc(${theme.spacing(-1)} - 3px)`, }, }, { @@ -86,7 +86,7 @@ export const FormControlLabelRoot = styled('label', { props: ({ labelPlacement }) => labelPlacement === 'start' || labelPlacement === 'top' || labelPlacement === 'bottom', style: { - marginLeft: 16, // used for row presentation of radio/checkbox + marginLeft: theme.spacing(2), // gap used for row presentation of radio/checkbox }, }, ], diff --git a/packages/mui-material/src/IconButton/IconButton.js b/packages/mui-material/src/IconButton/IconButton.js index 9b459b7d22e5d1..5b95a53e4f5ab5 100644 --- a/packages/mui-material/src/IconButton/IconButton.js +++ b/packages/mui-material/src/IconButton/IconButton.js @@ -52,7 +52,7 @@ const IconButtonRoot = styled(ButtonBase, { textAlign: 'center', flex: '0 0 auto', fontSize: theme.typography.pxToRem(24), - padding: 8, + padding: theme.spacing(1), borderRadius: '50%', color: (theme.vars || theme).palette.action.active, transition: theme.transitions.create('background-color', { @@ -124,14 +124,14 @@ const IconButtonRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - padding: 5, + padding: `calc(${theme.spacing(1)} - 3px)`, fontSize: theme.typography.pxToRem(18), }, }, { props: { size: 'large' }, style: { - padding: 12, + padding: `calc(${theme.spacing(2)} - 4px)`, fontSize: theme.typography.pxToRem(28), }, }, diff --git a/packages/mui-material/src/ImageListItemBar/ImageListItemBar.js b/packages/mui-material/src/ImageListItemBar/ImageListItemBar.js index 74703023feaf83..6e05b95c0a3eea 100644 --- a/packages/mui-material/src/ImageListItemBar/ImageListItemBar.js +++ b/packages/mui-material/src/ImageListItemBar/ImageListItemBar.js @@ -84,7 +84,7 @@ const ImageListItemBarTitleWrap = styled('div', { memoTheme(({ theme }) => { return { flexGrow: 1, - padding: '12px 16px', + padding: `calc(${theme.spacing(2)} - 4px) ${theme.spacing(2)}`, color: (theme.vars || theme).palette.common.white, overflow: 'hidden', variants: [ @@ -93,7 +93,7 @@ const ImageListItemBarTitleWrap = styled('div', { position: 'below', }, style: { - padding: '6px 0 12px', + padding: `calc(${theme.spacing(1)} - 2px) 0 calc(${theme.spacing(2)} - 4px)`, color: 'inherit', }, }, diff --git a/packages/mui-material/src/Input/Input.js b/packages/mui-material/src/Input/Input.js index 28254bbfa0a522..856dcefa472e28 100644 --- a/packages/mui-material/src/Input/Input.js +++ b/packages/mui-material/src/Input/Input.js @@ -64,7 +64,7 @@ const InputRoot = styled(InputBaseRoot, { props: ({ ownerState }) => ownerState.formControl, style: { [`label + &, .${inputLabelClasses.root} + &`]: { - marginTop: 16, + marginTop: theme.spacing(2), }, }, }, diff --git a/packages/mui-material/src/InputAdornment/InputAdornment.js b/packages/mui-material/src/InputAdornment/InputAdornment.js index abb06efb23735f..be5933e08326d2 100644 --- a/packages/mui-material/src/InputAdornment/InputAdornment.js +++ b/packages/mui-material/src/InputAdornment/InputAdornment.js @@ -58,7 +58,7 @@ const InputAdornmentRoot = styled('div', { style: { [`&.${inputAdornmentClasses.positionStart}&:not(.${inputAdornmentClasses.hiddenLabel})`]: { - marginTop: 16, + marginTop: theme.spacing(2), }, }, }, diff --git a/packages/mui-material/src/InputBase/InputBase.js b/packages/mui-material/src/InputBase/InputBase.js index 7bbe9ca97e94c9..f04ed48e5d104d 100644 --- a/packages/mui-material/src/InputBase/InputBase.js +++ b/packages/mui-material/src/InputBase/InputBase.js @@ -112,7 +112,7 @@ export const InputBaseRoot = styled('div', { { props: ({ ownerState }) => ownerState.multiline, style: { - padding: '4px 0 5px', + padding: `calc(${theme.spacing(1)} - 4px) 0 calc(${theme.spacing(1)} - 3px)`, }, }, { @@ -166,7 +166,7 @@ export const InputBaseInput = styled('input', { font: 'inherit', letterSpacing: 'inherit', color: 'currentColor', - padding: '4px 0 5px', + padding: `calc(${theme.spacing(1)} - 4px) 0 calc(${theme.spacing(1)} - 3px)`, border: 0, boxSizing: 'content-box', background: 'none', diff --git a/packages/mui-material/src/InputLabel/InputLabel.js b/packages/mui-material/src/InputLabel/InputLabel.js index e523c16453a762..1b812359bc976a 100644 --- a/packages/mui-material/src/InputLabel/InputLabel.js +++ b/packages/mui-material/src/InputLabel/InputLabel.js @@ -66,8 +66,9 @@ const InputLabelRoot = styled(FormLabel, { position: 'absolute', left: 0, top: 0, - // slight alteration to spec spacing to match visual spec result - transform: 'translate(0, 20px) scale(1)', + // resting y tracks the standard input's text top: Input marginTop + // (spacing(2)) + InputBase paddingTop (spacing(1) − 4px) = spacing(3) − 4px. + transform: `translate(0, calc(${theme.spacing(3)} - 4px)) scale(1)`, }, }, { @@ -75,8 +76,9 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - // Compensation for the `Input` small size style. - transform: 'translate(0, 17px) scale(1)', + // Compensation for the `Input` small size style: marginTop (spacing(2)) + // + small paddingTop (1px literal). + transform: `translate(0, calc(${theme.spacing(2)} + 1px)) scale(1)`, }, }, { @@ -107,7 +109,7 @@ const InputLabelRoot = styled(FormLabel, { // zIndex: 1 will raise the label above opaque background-colors of input. zIndex: 1, pointerEvents: 'none', - transform: 'translate(12px, 16px) scale(1)', + transform: `translate(12px, ${theme.spacing(2)}) scale(1)`, maxWidth: 'calc(100% - 24px)', }, }, @@ -117,7 +119,7 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - transform: 'translate(12px, 13px) scale(1)', + transform: `translate(12px, calc(${theme.spacing(2)} - 3px)) scale(1)`, }, }, { @@ -125,7 +127,7 @@ const InputLabelRoot = styled(FormLabel, { style: { userSelect: 'none', pointerEvents: 'auto', - transform: 'translate(12px, 7px) scale(0.75)', + transform: `translate(12px, calc(${theme.spacing(1)} - 1px)) scale(0.75)`, maxWidth: 'calc(133% - 24px)', }, }, @@ -133,7 +135,7 @@ const InputLabelRoot = styled(FormLabel, { props: ({ variant, ownerState, size }) => variant === 'filled' && ownerState.shrink && size === 'small', style: { - transform: 'translate(12px, 4px) scale(0.75)', + transform: `translate(12px, calc(${theme.spacing(1)} - 4px)) scale(0.75)`, }, }, { @@ -144,7 +146,7 @@ const InputLabelRoot = styled(FormLabel, { // see comment above on filled.zIndex zIndex: 1, pointerEvents: 'none', - transform: 'translate(14px, 16px) scale(1)', + transform: `translate(14px, ${theme.spacing(2)}) scale(1)`, maxWidth: 'calc(100% - 24px)', }, }, @@ -154,7 +156,7 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - transform: 'translate(14px, 9px) scale(1)', + transform: `translate(14px, calc(${theme.spacing(1)} + 1px)) scale(1)`, }, }, { diff --git a/packages/mui-material/src/List/List.js b/packages/mui-material/src/List/List.js index 3e885b9208be28..b81acf0733c69a 100644 --- a/packages/mui-material/src/List/List.js +++ b/packages/mui-material/src/List/List.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import ListContext from './ListContext'; import { getListUtilityClass } from './listClasses'; @@ -31,28 +32,30 @@ const ListRoot = styled('ul', { ownerState.subheader && styles.subheader, ]; }, -})({ - listStyle: 'none', - margin: 0, - padding: 0, - position: 'relative', - variants: [ - { - props: ({ ownerState }) => !ownerState.disablePadding, - style: { - paddingTop: 8, - paddingBottom: 8, +})( + memoTheme(({ theme }) => ({ + listStyle: 'none', + margin: 0, + padding: 0, + position: 'relative', + variants: [ + { + props: ({ ownerState }) => !ownerState.disablePadding, + style: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, }, - }, - { - props: ({ ownerState }) => ownerState.subheader, - style: { - paddingTop: 0, - isolation: 'isolate', // Prevent overlap with iOS overlay scrollbars. + { + props: ({ ownerState }) => ownerState.subheader, + style: { + paddingTop: 0, + isolation: 'isolate', // Prevent overlap with iOS overlay scrollbars. + }, }, - }, - ], -}); + ], + })), +); const List = React.forwardRef(function List(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiList' }); diff --git a/packages/mui-material/src/ListItem/ListItem.js b/packages/mui-material/src/ListItem/ListItem.js index 72c51915c4ad67..17211aa185279b 100644 --- a/packages/mui-material/src/ListItem/ListItem.js +++ b/packages/mui-material/src/ListItem/ListItem.js @@ -61,22 +61,22 @@ export const ListItemRoot = styled('div', { { props: ({ ownerState }) => !ownerState.disablePadding, style: { - paddingTop: 8, - paddingBottom: 8, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), }, }, { props: ({ ownerState }) => !ownerState.disablePadding && ownerState.dense, style: { - paddingTop: 4, - paddingBottom: 4, + paddingTop: `calc(${theme.spacing(1)} - 4px)`, + paddingBottom: `calc(${theme.spacing(1)} - 4px)`, }, }, { props: ({ ownerState }) => !ownerState.disablePadding && !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), }, }, { diff --git a/packages/mui-material/src/ListItemAvatar/ListItemAvatar.js b/packages/mui-material/src/ListItemAvatar/ListItemAvatar.js index e6c360ed238e50..d7c2d8cbea764f 100644 --- a/packages/mui-material/src/ListItemAvatar/ListItemAvatar.js +++ b/packages/mui-material/src/ListItemAvatar/ListItemAvatar.js @@ -5,6 +5,7 @@ import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import ListContext from '../List/ListContext'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getListItemAvatarUtilityClass } from './listItemAvatarClasses'; @@ -26,20 +27,22 @@ const ListItemAvatarRoot = styled('div', { return [styles.root, ownerState.alignItems === 'flex-start' && styles.alignItemsFlexStart]; }, -})({ - minWidth: 56, - flexShrink: 0, - variants: [ - { - props: { - alignItems: 'flex-start', +})( + memoTheme(({ theme }) => ({ + minWidth: 56, + flexShrink: 0, + variants: [ + { + props: { + alignItems: 'flex-start', + }, + style: { + marginTop: theme.spacing(1), + }, }, - style: { - marginTop: 8, - }, - }, - ], -}); + ], + })), +); /** * A simple wrapper to apply `List` styles to an `Avatar`. diff --git a/packages/mui-material/src/ListItemButton/ListItemButton.js b/packages/mui-material/src/ListItemButton/ListItemButton.js index b7681b7e52b2a9..e8198535b3b220 100644 --- a/packages/mui-material/src/ListItemButton/ListItemButton.js +++ b/packages/mui-material/src/ListItemButton/ListItemButton.js @@ -64,8 +64,8 @@ const ListItemButtonRoot = styled(ButtonBase, { minWidth: 0, boxSizing: 'border-box', textAlign: 'left', - paddingTop: 8, - paddingBottom: 8, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), transition: theme.transitions.create('background-color', { duration: theme.transitions.duration.shortest, }), @@ -127,15 +127,15 @@ const ListItemButtonRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), }, }, { props: ({ ownerState }) => ownerState.dense, style: { - paddingTop: 4, - paddingBottom: 4, + paddingTop: `calc(${theme.spacing(1)} - 4px)`, + paddingBottom: `calc(${theme.spacing(1)} - 4px)`, }, }, ], diff --git a/packages/mui-material/src/ListItemIcon/ListItemIcon.js b/packages/mui-material/src/ListItemIcon/ListItemIcon.js index 81aeb8629aede4..82be2008724a93 100644 --- a/packages/mui-material/src/ListItemIcon/ListItemIcon.js +++ b/packages/mui-material/src/ListItemIcon/ListItemIcon.js @@ -29,7 +29,7 @@ const ListItemIconRoot = styled('div', { }, })( memoTheme(({ theme }) => ({ - minWidth: theme.spacing(4.5), + minWidth: `max(${theme.spacing(4.5)}, 24px)`, color: (theme.vars || theme).palette.action.active, flexShrink: 0, display: 'inline-flex', @@ -39,7 +39,7 @@ const ListItemIconRoot = styled('div', { alignItems: 'flex-start', }, style: { - marginTop: 8, + marginTop: theme.spacing(1), }, }, ], diff --git a/packages/mui-material/src/ListItemText/ListItemText.js b/packages/mui-material/src/ListItemText/ListItemText.js index 8b81317e5d2955..63190cdbb126e6 100644 --- a/packages/mui-material/src/ListItemText/ListItemText.js +++ b/packages/mui-material/src/ListItemText/ListItemText.js @@ -6,6 +6,7 @@ import composeClasses from '@mui/utils/composeClasses'; import Typography, { typographyClasses } from '../Typography'; import ListContext from '../List/ListContext'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import listItemTextClasses, { getListItemTextUtilityClass } from './listItemTextClasses'; import useSlot from '../utils/useSlot'; @@ -37,34 +38,36 @@ const ListItemTextRoot = styled('div', { ownerState.dense && styles.dense, ]; }, -})({ - flex: '1 1 auto', - minWidth: 0, - marginTop: 4, - marginBottom: 4, - // Combine this and the below selector once https://github.com/emotion-js/emotion/issues/3366 is solved - [`.${typographyClasses.root}:where(& .${listItemTextClasses.primary})`]: { - display: 'block', - }, - [`.${typographyClasses.root}:where(& .${listItemTextClasses.secondary})`]: { - display: 'block', - }, - variants: [ - { - props: ({ ownerState }) => ownerState.primary && ownerState.secondary, - style: { - marginTop: 6, - marginBottom: 6, - }, +})( + memoTheme(({ theme }) => ({ + flex: '1 1 auto', + minWidth: 0, + marginTop: `calc(${theme.spacing(1)} - 4px)`, + marginBottom: `calc(${theme.spacing(1)} - 4px)`, + // Combine this and the below selector once https://github.com/emotion-js/emotion/issues/3366 is solved + [`.${typographyClasses.root}:where(& .${listItemTextClasses.primary})`]: { + display: 'block', }, - { - props: ({ ownerState }) => ownerState.inset, - style: { - paddingLeft: 56, - }, + [`.${typographyClasses.root}:where(& .${listItemTextClasses.secondary})`]: { + display: 'block', }, - ], -}); + variants: [ + { + props: ({ ownerState }) => ownerState.primary && ownerState.secondary, + style: { + marginTop: `calc(${theme.spacing(1)} - 2px)`, + marginBottom: `calc(${theme.spacing(1)} - 2px)`, + }, + }, + { + props: ({ ownerState }) => ownerState.inset, + style: { + paddingLeft: 56, + }, + }, + ], + })), +); const ListItemText = React.forwardRef(function ListItemText(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiListItemText' }); diff --git a/packages/mui-material/src/ListSubheader/ListSubheader.js b/packages/mui-material/src/ListSubheader/ListSubheader.js index 36bd3865891efe..f8b9356e0cecce 100644 --- a/packages/mui-material/src/ListSubheader/ListSubheader.js +++ b/packages/mui-material/src/ListSubheader/ListSubheader.js @@ -68,8 +68,8 @@ const ListSubheaderRoot = styled('li', { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), }, }, { diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 68df44938b4404..49fdf972aa975e 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -67,8 +67,8 @@ const MenuItemRoot = styled(ButtonBase, { position: 'relative', textDecoration: 'none', minHeight: 48, - paddingTop: 6, - paddingBottom: 6, + paddingTop: `calc(${theme.spacing(1)} - 2px)`, + paddingBottom: `calc(${theme.spacing(1)} - 2px)`, boxSizing: 'border-box', whiteSpace: 'nowrap', '&:hover': { @@ -154,8 +154,8 @@ const MenuItemRoot = styled(ButtonBase, { props: ({ ownerState }) => ownerState.dense, style: { minHeight: 32, // https://m2.material.io/components/menus#specs > Dense - paddingTop: 4, - paddingBottom: 4, + paddingTop: `calc(${theme.spacing(1)} - 4px)`, + paddingBottom: `calc(${theme.spacing(1)} - 4px)`, ...theme.typography.body2, [`& .${listItemIconClasses.root} svg`]: { fontSize: '1.25rem', diff --git a/packages/mui-material/src/MobileStepper/MobileStepper.js b/packages/mui-material/src/MobileStepper/MobileStepper.js index cbbee21b9eb915..e77d94e4bb98de 100644 --- a/packages/mui-material/src/MobileStepper/MobileStepper.js +++ b/packages/mui-material/src/MobileStepper/MobileStepper.js @@ -43,7 +43,7 @@ const MobileStepperRoot = styled(Paper, { justifyContent: 'space-between', alignItems: 'center', background: (theme.vars || theme).palette.background.default, - padding: 8, + padding: theme.spacing(1), variants: [ { props: ({ position }) => position === 'top' || position === 'bottom', diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index bb851995a8224d..dbf5ed38fef5cc 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -99,13 +99,13 @@ const OutlinedInputRoot = styled(InputBaseRoot, { { props: ({ ownerState }) => ownerState.multiline, style: { - padding: '16.5px 14px', + padding: `calc(${theme.spacing(2)} + 0.5px) 14px`, }, }, { props: ({ ownerState, size }) => ownerState.multiline && size === 'small', style: { - padding: '8.5px 14px', + padding: `calc(${theme.spacing(1)} + 0.5px) 14px`, }, }, ], @@ -134,7 +134,7 @@ const OutlinedInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - padding: '16.5px 14px', + padding: `calc(${theme.spacing(2)} + 0.5px) 14px`, '&:-webkit-autofill': { ...(!theme.vars && { WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', @@ -155,7 +155,7 @@ const OutlinedInputInput = styled(InputBaseInput, { size: 'small', }, style: { - padding: '8.5px 14px', + padding: `calc(${theme.spacing(1)} + 0.5px) 14px`, }, }, { diff --git a/packages/mui-material/src/PaginationItem/PaginationItem.js b/packages/mui-material/src/PaginationItem/PaginationItem.js index 59054c8645b291..c04544a82c18b6 100644 --- a/packages/mui-material/src/PaginationItem/PaginationItem.js +++ b/packages/mui-material/src/PaginationItem/PaginationItem.js @@ -71,7 +71,7 @@ const PaginationItemEllipsis = styled('div', { textAlign: 'center', boxSizing: 'border-box', minWidth: 32, - padding: '0 6px', + padding: `0 calc(${theme.spacing(1)} - 2px)`, margin: '0 3px', color: (theme.vars || theme).palette.text.primary, height: 'auto', @@ -85,7 +85,7 @@ const PaginationItemEllipsis = styled('div', { minWidth: 26, borderRadius: 26 / 2, margin: '0 1px', - padding: '0 4px', + padding: `0 calc(${theme.spacing(1)} - 4px)`, }, }, { @@ -93,7 +93,7 @@ const PaginationItemEllipsis = styled('div', { style: { minWidth: 40, borderRadius: 40 / 2, - padding: '0 10px', + padding: `0 calc(${theme.spacing(1)} + 2px)`, fontSize: theme.typography.pxToRem(15), }, }, @@ -113,7 +113,7 @@ const PaginationItemPage = styled(ButtonBase, { boxSizing: 'border-box', minWidth: 32, height: 32, - padding: '0 6px', + padding: `0 calc(${theme.spacing(1)} - 2px)`, margin: '0 3px', color: (theme.vars || theme).palette.text.primary, [`&.${paginationItemClasses.focusVisible}`]: { @@ -164,7 +164,7 @@ const PaginationItemPage = styled(ButtonBase, { height: 26, borderRadius: 26 / 2, margin: '0 1px', - padding: '0 4px', + padding: `0 calc(${theme.spacing(1)} - 4px)`, }, }, { @@ -173,7 +173,7 @@ const PaginationItemPage = styled(ButtonBase, { minWidth: 40, height: 40, borderRadius: 40 / 2, - padding: '0 10px', + padding: `0 calc(${theme.spacing(1)} + 2px)`, fontSize: theme.typography.pxToRem(15), }, }, diff --git a/packages/mui-material/src/SnackbarContent/SnackbarContent.js b/packages/mui-material/src/SnackbarContent/SnackbarContent.js index e685e5aab1faba..5ce94c06e0be57 100644 --- a/packages/mui-material/src/SnackbarContent/SnackbarContent.js +++ b/packages/mui-material/src/SnackbarContent/SnackbarContent.js @@ -40,7 +40,7 @@ const SnackbarContentRoot = styled(Paper, { display: 'flex', alignItems: 'center', flexWrap: 'wrap', - padding: '6px 16px', + padding: `calc(${theme.spacing(1)} - 2px) ${theme.spacing(2)}`, flexGrow: 1, [theme.breakpoints.up('sm')]: { flexGrow: 'initial', @@ -53,20 +53,24 @@ const SnackbarContentRoot = styled(Paper, { const SnackbarContentMessage = styled('div', { name: 'MuiSnackbarContent', slot: 'Message', -})({ - padding: '8px 0', -}); +})( + memoTheme(({ theme }) => ({ + padding: `${theme.spacing(1)} 0`, + })), +); const SnackbarContentAction = styled('div', { name: 'MuiSnackbarContent', slot: 'Action', -})({ - display: 'flex', - alignItems: 'center', - marginLeft: 'auto', - paddingLeft: 16, - marginRight: -8, -}); +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginLeft: 'auto', + paddingLeft: theme.spacing(2), + marginRight: theme.spacing(-1), + })), +); const SnackbarContent = React.forwardRef(function SnackbarContent(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiSnackbarContent' }); diff --git a/packages/mui-material/src/SpeedDialAction/SpeedDialAction.js b/packages/mui-material/src/SpeedDialAction/SpeedDialAction.js index e57790980df4e4..aaa5b07e3f6982 100644 --- a/packages/mui-material/src/SpeedDialAction/SpeedDialAction.js +++ b/packages/mui-material/src/SpeedDialAction/SpeedDialAction.js @@ -41,7 +41,7 @@ const SpeedDialActionFab = styled(Fab, { }, })( memoTheme(({ theme }) => ({ - margin: 8, + margin: theme.spacing(1), color: (theme.vars || theme).palette.text.secondary, backgroundColor: (theme.vars || theme).palette.background.paper, '&:hover': { @@ -106,7 +106,7 @@ const SpeedDialActionStaticTooltip = styled('span', { [`& .${speedDialActionClasses.staticTooltipLabel}`]: { transformOrigin: '100% 50%', right: '100%', - marginRight: 8, + marginRight: theme.spacing(1), }, }, }, @@ -118,7 +118,7 @@ const SpeedDialActionStaticTooltip = styled('span', { [`& .${speedDialActionClasses.staticTooltipLabel}`]: { transformOrigin: '0% 50%', left: '100%', - marginLeft: 8, + marginLeft: theme.spacing(1), }, }, }, @@ -137,7 +137,7 @@ const SpeedDialActionStaticTooltipLabel = styled('span', { borderRadius: (theme.vars || theme).shape.borderRadius, boxShadow: (theme.vars || theme).shadows[1], color: (theme.vars || theme).palette.text.secondary, - padding: '4px 16px', + padding: `calc(${theme.spacing(1)} - 4px) ${theme.spacing(2)}`, wordBreak: 'keep-all', })), ); diff --git a/packages/mui-material/src/Step/Step.js b/packages/mui-material/src/Step/Step.js index 80b88c01a6356f..e22792ed1a3aec 100644 --- a/packages/mui-material/src/Step/Step.js +++ b/packages/mui-material/src/Step/Step.js @@ -7,6 +7,7 @@ import composeClasses from '@mui/utils/composeClasses'; import { useStepperContext } from '../Stepper/StepperContext'; import StepContext from './StepContext'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getStepUtilityClass } from './stepClasses'; @@ -33,46 +34,48 @@ const StepRoot = styled('li', { ownerState.completed && styles.completed, ]; }, -})({ - variants: [ - { - props: { orientation: 'horizontal', alternativeLabel: false, hasConnector: false }, - style: { - paddingLeft: 8, +})( + memoTheme(({ theme }) => ({ + variants: [ + { + props: { orientation: 'horizontal', alternativeLabel: false, hasConnector: false }, + style: { + paddingLeft: theme.spacing(1), + }, }, - }, - { - props: { orientation: 'horizontal', alternativeLabel: false, last: true }, - style: { - paddingRight: 8, + { + props: { orientation: 'horizontal', alternativeLabel: false, last: true }, + style: { + paddingRight: theme.spacing(1), + }, }, - }, - { - props: { orientation: 'horizontal', alternativeLabel: false, hasConnector: true }, - style: { - flex: '1 1 auto', - display: 'grid', - gridTemplateColumns: '1fr auto', - alignItems: 'center', - gap: 8, + { + props: { orientation: 'horizontal', alternativeLabel: false, hasConnector: true }, + style: { + flex: '1 1 auto', + display: 'grid', + gridTemplateColumns: '1fr auto', + alignItems: 'center', + gap: theme.spacing(1), + }, }, - }, - { - props: { orientation: 'vertical', alternativeLabel: true }, - style: { - display: 'flex', - flexDirection: 'column', + { + props: { orientation: 'vertical', alternativeLabel: true }, + style: { + display: 'flex', + flexDirection: 'column', + }, }, - }, - { - props: { orientation: 'horizontal', alternativeLabel: true }, - style: { - flex: 1, - position: 'relative', + { + props: { orientation: 'horizontal', alternativeLabel: true }, + style: { + flex: 1, + position: 'relative', + }, }, - }, - ], -}); + ], + })), +); const Step = React.forwardRef(function Step(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiStep' }); diff --git a/packages/mui-material/src/StepButton/StepButton.js b/packages/mui-material/src/StepButton/StepButton.js index 966679e508b8e4..dca1f72e3cd82f 100644 --- a/packages/mui-material/src/StepButton/StepButton.js +++ b/packages/mui-material/src/StepButton/StepButton.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import ButtonBase from '../ButtonBase'; import StepLabel from '../StepLabel'; @@ -36,25 +37,27 @@ const StepButtonRoot = styled(ButtonBase, { styles[ownerState.orientation], ]; }, -})({ - width: '100%', - padding: '24px 16px', - margin: '-24px -16px', - boxSizing: 'content-box', - [`& .${stepButtonClasses.touchRipple}`]: { - color: 'rgba(0, 0, 0, 0.3)', - }, - variants: [ - { - props: { orientation: 'vertical' }, - style: { - justifyContent: 'flex-start', - padding: '8px', - margin: '-8px', - }, +})( + memoTheme(({ theme }) => ({ + width: '100%', + padding: `${theme.spacing(3)} ${theme.spacing(2)}`, + margin: `${theme.spacing(-3)} ${theme.spacing(-2)}`, + boxSizing: 'content-box', + [`& .${stepButtonClasses.touchRipple}`]: { + color: 'rgba(0, 0, 0, 0.3)', }, - ], -}); + variants: [ + { + props: { orientation: 'vertical' }, + style: { + justifyContent: 'flex-start', + padding: theme.spacing(1), + margin: theme.spacing(-1), + }, + }, + ], + })), +); const RovingStepButton = React.forwardRef(function RovingStepButton(props, ref) { // eslint-disable-next-line react/prop-types diff --git a/packages/mui-material/src/StepContent/StepContent.js b/packages/mui-material/src/StepContent/StepContent.js index 27c3f58b9d075d..624acfdd9044b4 100644 --- a/packages/mui-material/src/StepContent/StepContent.js +++ b/packages/mui-material/src/StepContent/StepContent.js @@ -31,8 +31,8 @@ const StepContentRoot = styled('div', { })( memoTheme(({ theme }) => ({ marginLeft: 12, // half icon - paddingLeft: 8 + 12, // margin + half icon - paddingRight: 8, + paddingLeft: `calc(${theme.spacing(1)} + 12px)`, // margin + half icon + paddingRight: theme.spacing(1), borderLeft: theme.vars ? `1px solid ${theme.vars.palette.StepContent.border}` : `1px solid ${ @@ -50,8 +50,8 @@ const StepContentRoot = styled('div', { style: { marginLeft: 0, marginRight: 12, // half icon - paddingLeft: 8, - paddingRight: 8 + 12, // margin + half icon + paddingLeft: theme.spacing(1), + paddingRight: `calc(${theme.spacing(1)} + 12px)`, // margin + half icon borderLeft: 'none', borderRight: theme.vars ? `1px solid ${theme.vars.palette.StepContent.border}` diff --git a/packages/mui-material/src/StepLabel/StepLabel.js b/packages/mui-material/src/StepLabel/StepLabel.js index c35808bd5cf30c..025a18da674a46 100644 --- a/packages/mui-material/src/StepLabel/StepLabel.js +++ b/packages/mui-material/src/StepLabel/StepLabel.js @@ -53,34 +53,36 @@ const StepLabelRoot = styled('span', { return [styles.root, styles[ownerState.orientation]]; }, -})({ - display: 'flex', - alignItems: 'center', - [`&.${stepLabelClasses.disabled}`]: { - cursor: 'default', - }, - variants: [ - { - props: { orientation: 'vertical' }, - style: { - textAlign: 'left', - padding: '8px 0', - }, +})( + memoTheme(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + [`&.${stepLabelClasses.disabled}`]: { + cursor: 'default', }, - { - props: { alternativeLabel: true }, - style: { - flexDirection: 'column', + variants: [ + { + props: { orientation: 'vertical' }, + style: { + textAlign: 'left', + padding: `${theme.spacing(1)} 0`, + }, }, - }, - { - props: { orientation: 'vertical', alternativeLabel: true }, - style: { - flexDirection: 'row-reverse', + { + props: { alternativeLabel: true }, + style: { + flexDirection: 'column', + }, }, - }, - ], -}); + { + props: { orientation: 'vertical', alternativeLabel: true }, + style: { + flexDirection: 'row-reverse', + }, + }, + ], + })), +); const StepLabelLabel = styled('span', { name: 'MuiStepLabel', @@ -97,7 +99,7 @@ const StepLabelLabel = styled('span', { fontWeight: 500, }, [`&.${stepLabelClasses.alternativeLabel}`]: { - marginTop: 16, + marginTop: theme.spacing(2), }, [`&.${stepLabelClasses.error}`]: { color: (theme.vars || theme).palette.error.main, @@ -118,23 +120,25 @@ const StepLabelLabel = styled('span', { const StepLabelIconContainer = styled('span', { name: 'MuiStepLabel', slot: 'IconContainer', -})({ - flexShrink: 0, - display: 'flex', - paddingRight: 8, - [`&.${stepLabelClasses.alternativeLabel}`]: { - paddingRight: 0, - }, - variants: [ - { - props: { orientation: 'vertical', alternativeLabel: true }, - style: { - paddingRight: 0, - paddingLeft: 8, - }, +})( + memoTheme(({ theme }) => ({ + flexShrink: 0, + display: 'flex', + paddingRight: theme.spacing(1), + [`&.${stepLabelClasses.alternativeLabel}`]: { + paddingRight: 0, }, - ], -}); + variants: [ + { + props: { orientation: 'vertical', alternativeLabel: true }, + style: { + paddingRight: 0, + paddingLeft: theme.spacing(1), + }, + }, + ], + })), +); const StepLabelLabelContainer = styled('span', { name: 'MuiStepLabel', diff --git a/packages/mui-material/src/Stepper/Stepper.js b/packages/mui-material/src/Stepper/Stepper.js index 2bee19e3f175da..7c71ee4be4953f 100644 --- a/packages/mui-material/src/Stepper/Stepper.js +++ b/packages/mui-material/src/Stepper/Stepper.js @@ -6,6 +6,7 @@ import integerPropType from '@mui/utils/integerPropType'; import composeClasses from '@mui/utils/composeClasses'; import { useRtl } from '@mui/system/RtlProvider'; import { styled } from '../zero-styled'; +import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { RovingTabIndexContext, useRovingTabIndexRoot } from '../utils/useRovingTabIndex'; import { getStepperUtilityClass } from './stepperClasses'; @@ -34,45 +35,47 @@ const StepperRoot = styled('ol', { ownerState.nonLinear && styles.nonLinear, ]; }, -})({ - display: 'flex', - listStyle: 'none', - margin: 0, - padding: 0, - variants: [ - { - props: { orientation: 'horizontal' }, - style: { - flexDirection: 'row', - alignItems: 'center', +})( + memoTheme(({ theme }) => ({ + display: 'flex', + listStyle: 'none', + margin: 0, + padding: 0, + variants: [ + { + props: { orientation: 'horizontal' }, + style: { + flexDirection: 'row', + alignItems: 'center', + }, }, - }, - { - props: { orientation: 'horizontal', alternativeLabel: false }, - style: { - gap: 8, + { + props: { orientation: 'horizontal', alternativeLabel: false }, + style: { + gap: theme.spacing(1), + }, }, - }, - { - props: { orientation: 'vertical' }, - style: { - flexDirection: 'column', + { + props: { orientation: 'vertical' }, + style: { + flexDirection: 'column', + }, }, - }, - { - props: { alternativeLabel: true }, - style: { - alignItems: 'flex-start', + { + props: { alternativeLabel: true }, + style: { + alignItems: 'flex-start', + }, }, - }, - { - props: { orientation: 'vertical', alternativeLabel: true }, - style: { - alignItems: 'flex-end', + { + props: { orientation: 'vertical', alternativeLabel: true }, + style: { + alignItems: 'flex-end', + }, }, - }, - ], -}); + ], + })), +); const defaultConnector = ; diff --git a/packages/mui-material/src/Tab/Tab.js b/packages/mui-material/src/Tab/Tab.js index 05ddee7f3a0ba7..753a3206b1d981 100644 --- a/packages/mui-material/src/Tab/Tab.js +++ b/packages/mui-material/src/Tab/Tab.js @@ -56,7 +56,7 @@ const TabRoot = styled(ButtonBase, { position: 'relative', minHeight: 48, flexShrink: 0, - padding: '12px 16px', + padding: `calc(${theme.spacing(2)} - 4px) ${theme.spacing(2)}`, overflow: 'hidden', whiteSpace: 'normal', textAlign: 'center', @@ -83,8 +83,8 @@ const TabRoot = styled(ButtonBase, { props: ({ ownerState }) => ownerState.icon && ownerState.label, style: { minHeight: 72, - paddingTop: 9, - paddingBottom: 9, + paddingTop: `calc(${theme.spacing(1)} + 1px)`, + paddingBottom: `calc(${theme.spacing(1)} + 1px)`, }, }, { @@ -92,7 +92,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'top', style: { [`& > .${tabClasses.icon}`]: { - marginBottom: 6, + marginBottom: `calc(${theme.spacing(1)} - 2px)`, }, }, }, @@ -101,7 +101,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'bottom', style: { [`& > .${tabClasses.icon}`]: { - marginTop: 6, + marginTop: `calc(${theme.spacing(1)} - 2px)`, }, }, }, diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 46f5500ebfdc82..ffbde17ed1e916 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -191,7 +191,8 @@ describe('', () => { expect(wrapper).to.have.class('test-icon'); }); - it('should have bottom margin when passed together with label', () => { + // Margin is now `calc(spacing(1) - 2px)`; jsdom can't resolve calc, chromium does. + it.skipIf(isJsdom())('should have bottom margin when passed together with label', () => { render( } label="foo" /> diff --git a/packages/mui-material/src/TableCell/TableCell.js b/packages/mui-material/src/TableCell/TableCell.js index 62ea0c0656a7ac..64ec6cbf684442 100644 --- a/packages/mui-material/src/TableCell/TableCell.js +++ b/packages/mui-material/src/TableCell/TableCell.js @@ -59,7 +59,7 @@ const TableCellRoot = styled('td', { : theme.darken(theme.alpha(theme.palette.divider, 1), 0.68) }`, textAlign: 'left', - padding: 16, + padding: theme.spacing(2), variants: [ { props: { @@ -94,7 +94,7 @@ const TableCellRoot = styled('td', { size: 'small', }, style: { - padding: '6px 16px', + padding: `calc(${theme.spacing(1)} - 2px) ${theme.spacing(2)}`, [`&.${tableCellClasses.paddingCheckbox}`]: { width: 24, // prevent the checkbox column from growing padding: '0 12px 0 16px', diff --git a/packages/mui-material/src/TablePagination/TablePagination.js b/packages/mui-material/src/TablePagination/TablePagination.js index 45c2b598ea838e..fc829caabbc0ea 100644 --- a/packages/mui-material/src/TablePagination/TablePagination.js +++ b/packages/mui-material/src/TablePagination/TablePagination.js @@ -56,7 +56,7 @@ const TablePaginationToolbar = styled(Toolbar, { }, [`& .${tablePaginationClasses.actions}`]: { flexShrink: 0, - marginLeft: 20, + marginLeft: `calc(${theme.spacing(3)} - 4px)`, }, })), ); @@ -87,19 +87,21 @@ const TablePaginationSelect = styled(Select, { ...styles.input, ...styles.selectRoot, }), -})({ - color: 'inherit', - fontSize: 'inherit', - flexShrink: 0, - marginRight: 32, - marginLeft: 8, - [`& .${tablePaginationClasses.select}`]: { - paddingLeft: 8, - paddingRight: 24, - textAlign: 'right', - textAlignLast: 'right', // Align on Chrome. + }, + })), +); const TablePaginationMenuItem = styled(MenuItem, { name: 'MuiTablePagination', diff --git a/packages/mui-material/src/TableSortLabel/TableSortLabel.js b/packages/mui-material/src/TableSortLabel/TableSortLabel.js index e237bd20184bb2..2028b0080b05e3 100644 --- a/packages/mui-material/src/TableSortLabel/TableSortLabel.js +++ b/packages/mui-material/src/TableSortLabel/TableSortLabel.js @@ -63,8 +63,8 @@ const TableSortLabelIcon = styled('span', { })( memoTheme(({ theme }) => ({ fontSize: 18, - marginRight: 4, - marginLeft: 4, + marginRight: `calc(${theme.spacing(1)} - 4px)`, + marginLeft: `calc(${theme.spacing(1)} - 4px)`, opacity: 0, transition: theme.transitions.create(['opacity', 'transform'], { duration: theme.transitions.duration.shorter, diff --git a/packages/mui-material/src/ToggleButton/ToggleButton.js b/packages/mui-material/src/ToggleButton/ToggleButton.js index bc8e7e207c0dfa..07e42ce707e7d5 100644 --- a/packages/mui-material/src/ToggleButton/ToggleButton.js +++ b/packages/mui-material/src/ToggleButton/ToggleButton.js @@ -45,7 +45,7 @@ const ToggleButtonRoot = styled(ButtonBase, { memoTheme(({ theme }) => ({ ...theme.typography.button, borderRadius: (theme.vars || theme).shape.borderRadius, - padding: 11, + padding: `calc(${theme.spacing(1)} + 3px)`, border: `1px solid ${(theme.vars || theme).palette.divider}`, color: (theme.vars || theme).palette.action.active, [`&.${toggleButtonClasses.disabled}`]: { @@ -125,14 +125,14 @@ const ToggleButtonRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - padding: 7, + padding: `calc(${theme.spacing(1)} - 1px)`, fontSize: theme.typography.pxToRem(13), }, }, { props: { size: 'large' }, style: { - padding: 15, + padding: `calc(${theme.spacing(2)} - 1px)`, fontSize: theme.typography.pxToRem(15), }, }, diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js index cede32e7240910..b98e46037c626b 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.js +++ b/packages/mui-material/src/Tooltip/Tooltip.js @@ -127,7 +127,7 @@ const TooltipTooltip = styled('div', { borderRadius: (theme.vars || theme).shape.borderRadius, color: (theme.vars || theme).palette.common.white, fontFamily: theme.typography.fontFamily, - padding: '4px 8px', + padding: `calc(${theme.spacing(1)} - 4px) ${theme.spacing(1)}`, fontSize: theme.typography.pxToRem(11), maxWidth: 300, margin: 2, @@ -160,7 +160,7 @@ const TooltipTooltip = styled('div', { { props: ({ ownerState }) => ownerState.touch, style: { - padding: '8px 16px', + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, fontSize: theme.typography.pxToRem(14), lineHeight: `${round(16 / 14)}em`, fontWeight: theme.typography.fontWeightRegular, diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js index 5257bfe688e49e..2f52f5a23b60ce 100644 --- a/packages/mui-material/src/internal/SwitchBase.js +++ b/packages/mui-material/src/internal/SwitchBase.js @@ -24,8 +24,8 @@ const useUtilityClasses = (ownerState) => { const SwitchBaseRoot = styled(ButtonBase, { name: 'MuiSwitchBase', -})({ - padding: 9, +})(({ theme }) => ({ + padding: `calc(${theme.spacing(1)} + 1px)`, borderRadius: '50%', variants: [ { @@ -59,7 +59,7 @@ const SwitchBaseRoot = styled(ButtonBase, { }, }, ], -}); +})); const SwitchBaseInput = styled('input', { name: 'MuiSwitchBase', diff --git a/scripts/spacing-screenshots/README.md b/scripts/spacing-screenshots/README.md new file mode 100644 index 00000000000000..2024dd2717f088 --- /dev/null +++ b/scripts/spacing-screenshots/README.md @@ -0,0 +1,42 @@ +# Spacing-derivation screenshot harness + +Local visual verification for the spacing-derivation rollout — no Argos. +Decision/spec/plan: [`docs/adr/`](../../docs/adr/spacing-derived-rollout-plan.md). + +Asserts the default render is **pixel-identical** before/after the change +(Playwright `toHaveScreenshot`, `maxDiffPixels: 0`) and captures density +screenshots (`--mui-spacing` 6px/10px) for human review. + +## Prerequisites + +- `pnpm docs:dev` running (serves the fixture at + `/experiments/spacing-fixture`). Override the URL with `SPACING_BASE_URL`. +- Chromium for Playwright: `pnpm exec playwright install chromium` (once). + +## Steps (per component) + +1. Add the component's matrix to the `demos` map in + `docs/pages/experiments/spacing-fixture.tsx`. +2. **Baseline (before)** — on the _unconverted_ component: + + ```bash + COMPONENT= pnpm spacing:shot:update + ``` + + Writes the baseline to `scripts/spacing-screenshots/__baselines__/`. + +3. Implement the spacing-derivation in the component. +4. **Assert + density (after)**: + + ```bash + COMPONENT= pnpm spacing:shot + ``` + + - Fails if the default render differs from the baseline (⇒ wrong offset); + a diff image is written under `test-results/`. + - Writes `spacing-screenshots//after-{default,6px,10px}.png`. + +5. Eyeball `after-6px.png` / `after-10px.png` for density reflow and + anchored/notch alignment. + +Outputs (`spacing-screenshots/`, `__baselines__/`) are gitignored. diff --git a/scripts/spacing-screenshots/playwright.config.mjs b/scripts/spacing-screenshots/playwright.config.mjs new file mode 100644 index 00000000000000..a5698f378c5927 --- /dev/null +++ b/scripts/spacing-screenshots/playwright.config.mjs @@ -0,0 +1,19 @@ +import { defineConfig } from '@playwright/test'; + +// Local verification harness for the spacing-derivation rollout. +// See docs/adr/spacing-derived-rollout-plan.md ("How to verify"). +export default defineConfig({ + testDir: '.', + testMatch: /spacing\.spec\.mjs/, + // Keep Playwright's artifacts out of the repo's tracked test-results/. + outputDir: './.playwright-output', + // "before" baselines (gitignored) — co-located with the harness. + snapshotPathTemplate: '{testDir}/__baselines__/{arg}{ext}', + reporter: 'list', + use: { + baseURL: process.env.SPACING_BASE_URL || 'http://localhost:3000', + viewport: { width: 760, height: 720 }, + }, + // Strict: the default render must be pixel-identical to the baseline. + expect: { toHaveScreenshot: { maxDiffPixels: 0 } }, +}); diff --git a/scripts/spacing-screenshots/spacing.spec.mjs b/scripts/spacing-screenshots/spacing.spec.mjs new file mode 100644 index 00000000000000..9b3ad9cc2155fd --- /dev/null +++ b/scripts/spacing-screenshots/spacing.spec.mjs @@ -0,0 +1,33 @@ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +// Component under verification, e.g. `COMPONENT=Button pnpm spacing:shot`. +const component = process.env.COMPONENT || 'Button'; +const outDir = path.resolve(process.cwd(), 'spacing-screenshots', component); +const scopeSelector = '#spacing-scope'; + +async function scopeAt(page, spacing) { + await page.goto(`/experiments/spacing-fixture?c=${component}&spacing=${spacing}`); + const scope = page.locator(scopeSelector); + await scope.waitFor(); + await page.evaluate(() => document.fonts.ready); + return scope; +} + +test.describe.configure({ mode: 'serial' }); + +// Regression gate: default render must match the "before" baseline exactly. +// Run with --update-snapshots on the UNCONVERTED component to capture the baseline. +test(`${component} — default is pixel-identical to baseline`, async ({ page }) => { + const scope = await scopeAt(page, 8); + await scope.screenshot({ path: path.join(outDir, 'after-default.png') }); + await expect(scope).toHaveScreenshot(`${component}-default.png`); +}); + +// Density review (human only — new behavior, not assertable). +for (const spacing of [6, 10]) { + test(`${component} — density --mui-spacing ${spacing}px (review)`, async ({ page }) => { + const scope = await scopeAt(page, spacing); + await scope.screenshot({ path: path.join(outDir, `after-${spacing}px.png`) }); + }); +}