From ffe175fd3bc4b43a76960f3485b78730e739ff9a Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 5 Jun 2026 12:41:17 +0900 Subject: [PATCH 001/114] [material-ui] Add CSS-var density adapter experiment (Button) Expose Button padding as overridable CSS vars resolved inline from a (variant,size) lookup; add enhanceDensity to wire tokens to a --mui-density-* scale. Literal-px fallbacks keep the default pixel-identical. Design in CONTEXT.md + docs/adr/0001; demo at /experiments/density-tokens. --- CONTEXT.md | 93 +++++++++ docs/adr/0001-css-var-density-adapter.md | 89 +++++++++ docs/pages/experiments/density-tokens.tsx | 178 ++++++++++++++++++ packages/mui-material/src/Button/Button.js | 38 +++- .../mui-material/src/styles/enhanceDensity.ts | 124 ++++++++++++ packages/mui-material/src/styles/index.d.ts | 1 + packages/mui-material/src/styles/index.js | 1 + 7 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-css-var-density-adapter.md create mode 100644 docs/pages/experiments/density-tokens.tsx create mode 100644 packages/mui-material/src/styles/enhanceDensity.ts diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000000000..7e07c49a824983 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,93 @@ +# Density (CSS-var adapter) + +How Material UI component dimensions (padding / gap / height) are exposed as +hand-authorable CSS variables so a designer can tune component density — per +component, per size, or holistically — without touching component source, doing +`calc` arithmetic, or riding the single `--mui-spacing` dial. + +This is the **adapter** sibling of the earlier `--mui-spacing`-derived +experiment (`feat/components-theme-spacing`): instead of one global dial, each +dimension is an overridable token whose default is a literal px. + +## Language + +**Component spacing token** (public, base): +A per-component, per-CSS-property variable a designer may set, shape +`--Component-` — PascalCase component, camelCase **logical** CSS +property, unprefixed (e.g. `--Button-paddingInline`, `--Chip-gap`). Matches the +existing component-var convention (`--AppBar-background`). Setting it reflows +that property across every variant and size of the component. +_Avoid_: kebab property (`--Button-padding-inline`), `--mui-`-prefixed component vars, "variable". + +**Sized token** (public, size-specific): +A size-scoped override, shape `--Component--` +(e.g. `--Button-small-paddingInline`). Reflows only that one size. **More +specific than the base token** — when both are set, the sized token wins. +_Avoid_: "size variant token". + +**Internal resolution var**: +A private variable, shape `--_Component-` (leading underscore), +**set via inline style** from the rendered `(variant, size)` and consumed once +in the styled root. It carries the full fallback chain (sized token → base +token → literal). Lowest priority, so any public token or plain `styleOverrides` +property still wins. +_Avoid_: exposing it as API, "private token". + +**Token fallback**: +The literal px at the end of the chain — today's exact value for that +`(variant, size)` cell. Makes the default render pixel-identical and bundle-light, +at the cost that the single `--mui-spacing` dial no longer reflows the component. +_Avoid_: "default value", "initial". + +**Density scale** (tier-1): +A named, ordered set of density steps (`xxs / xs / sm / md / lg …`), values +derived from `theme.spacing`, surfaced as `--mui-density-*` CSS vars. The shared, +designer-facing holistic-density surface. **Emitted by `enhanceDensity`, not +`createTheme`** (runtime opt-in); its **types ship built-in** +(`theme.vars.density.*` always type-checks). +_Avoid_: "spacing scale" (that is `theme.spacing`), "grid". + +**enhanceDensity**: +A single post-`createTheme` function (mirroring `enhanceHighContrast`) that does +**both**: (a) emits the **density scale** as `--mui-density-*` and populates +`theme.vars.density`, and (b) injects per-component `styleOverrides.root` mapping +**component spacing tokens** to density steps +(`--Button-paddingInline: theme.vars.density.md`). `createTheme` is untouched. +Opt-in: without it, components render their literal-px defaults; with it, tuning +the density scale (or scoping `--mui-density-*`) reflows every wired component. +_Avoid_: "density preset" (that is the resulting effect, not the function). + +## Relationships + +- The styled root reads **one** internal resolution var per property; **no JS + conditional** lives in the styles implementation. The `(variant, size)` → px + matrix is a **lookup table** in the component body, applied via inline style. +- Override priority (high → low): plain `styleOverrides` property → **sized + token** → **base token** → internal resolution var (literal fallback). +- Custom (user-defined) sizes work for free: the sized-token name is built from + the runtime size string; no static per-size CSS is emitted. +- **enhanceDensity** (opt-in) connects tier-2 component tokens to the tier-1 + **density scale**; un-enhanced, the literal fallbacks reproduce today's pixels. +- This experiment does **not** ride `--mui-spacing`; holistic density comes from + the density scale, not that dial. + +## Example dialogue + +> **Dev:** "If I set `--Button-paddingInline` on the theme, what happens to a +> small outlined button?" +> **Domain expert:** "It reflows to your value — base token covers every +> variant and size. Unless you also set `--Button-small-paddingInline`; the +> **sized token** is more specific and wins for small." +> **Dev:** "And with nothing set?" +> **Domain expert:** "The **internal resolution var**, set inline from the +> `(variant, size)` cell, falls through to the literal px — pixel-identical to +> today. The `--mui-spacing` dial does nothing here; for holistic density you +> run **enhanceDensity** and tune the **density scale**." + +## Flagged ambiguities + +- "spacing token" meant both a `theme.spacing` key and a per-component value — + resolved: `theme.spacing` is untouched; per-component vars are **component + spacing tokens** (base) and **sized tokens**. +- "spacing scale" (earlier draft, tier-1) — renamed **density scale** and moved + to `theme.density`, to disambiguate from `theme.spacing`. diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md new file mode 100644 index 00000000000000..6361de7ba99cba --- /dev/null +++ b/docs/adr/0001-css-var-density-adapter.md @@ -0,0 +1,89 @@ +# Component density via a CSS-var adapter, resolved inline + +Component dimensions are exposed as public CSS variables with **literal-px +fallbacks**, resolved through an **internal var set by inline style**, instead of +riding the single `--mui-spacing` dial (`feat/components-theme-spacing`) or +emitting a static per-(variant, size) token matrix (`poc/css-vars-map`). + +## Context + +We want designers to tune density — per component, per size, or holistically — +without editing component source, writing `calc`, or accepting that every +dimension reflows off one global `--mui-spacing` value. + +Constraints that shaped the design: + +- **Pixel-identical default.** The un-configured theme must render today's exact + px for every `(variant, size)` cell (Argos zero-diff). +- **No token bloat at build time.** Don't emit a static CSS rule (or named + token) per variant×size×property cell. +- **Support user-provided sizes.** A custom `size` added via the theme must get + the same tunability as built-in sizes. +- **No JS conditionals in the styles implementation.** The `styled()` body must + not branch on `ownerState.size`/`variant` to pick a value. +- **Non-breaking.** Existing variant/size padding and existing + `styleOverrides`/`sx` overrides must keep working unchanged. + +## Decision + +Three token layers per component property, e.g. inline padding on Button: + +- **Base token** `--Button-paddingInline` (public) — reflows all variants/sizes. +- **Sized token** `--Button--paddingInline` (public) — reflows one size; + **more specific than base** (size wins). +- **Internal resolution var** `--_Button-paddingInline` (private, leading + underscore) — set via **inline style** from the rendered `(variant, size)`, + carrying the chain `var(--Button--paddingInline, var(--Button-paddingInline, ))`. + +The styled root has **one** consumption point per property and **no conditional**: + +```js +const ButtonRoot = styled(ButtonBase)({ + paddingInline: 'var(--_Button-paddingInline)', + paddingBlock: 'var(--_Button-paddingBlock)', +}); +``` + +The component body holds the `(variant, size)` → px values as a **lookup table** +(today's exact numbers) and applies them via inline style: + +```js +const [block, inline] = PADDING[variant][size]; +const sizingVars = { + '--_Button-paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${block}))`, + '--_Button-paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${inline}))`, +}; + +``` + +Holistic density is a separate, opt-in layer driven by a **single** +`enhanceDensity(theme)` function (mirroring `enhanceHighContrast`) that does +both jobs: it **emits** the density scale as `--mui-density-*` (and populates +`theme.vars.density`), and **maps** base tokens to density steps via injected +`styleOverrides.root` (`--Button-paddingInline: var(--mui-density-md)`). +`createTheme` is left untouched. Types for `theme.vars.density` ship built-in; +the vars exist at runtime only after `enhanceDensity` runs. + +We considered making `density` a first-class `createTheme` node so the normal +css-var generator emits the vars. That is more "correct" (the vars participate +in the standard generation and can be re-scoped at any level), but it requires +`createTheme`/css-vars surgery. For an experiment we chose the self-contained +function: easy to A/B, easy to delete, no core change. The cost is that +post-hoc-emitted vars live outside the standard `theme.vars` pipeline. + +Scope: **Button only** for this experiment. + +## Consequences + +- **Pixel-identical default & non-breaking.** Literals come from the lookup + table; the internal var is the lowest-priority fallback, so public tokens, + `styleOverrides`, and `sx` all still win via the cascade. +- **Custom sizes work for free** — the sized-token name is built from the + runtime size string; nothing static is emitted per size. +- **Inline style is the price.** Every instance carries a `style` attr with the + resolution vars (larger HTML, no CSS dedup of those values) — accepted to kill + the static token matrix and support arbitrary sizes. +- **No `--mui-spacing` reflow.** Components opt out of the global dial; holistic + density flows through the density scale + `enhanceDensity` instead. +- **calc resolves only in a real browser** (jsdom does not), so density + assertions belong in browser/visual tests, not jsdom unit tests. diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx new file mode 100644 index 00000000000000..ec6381b6178e48 --- /dev/null +++ b/docs/pages/experiments/density-tokens.tsx @@ -0,0 +1,178 @@ +'use client'; +import * as React from 'react'; +import { createTheme, ThemeProvider, enhanceDensity } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Paper from '@mui/material/Paper'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; + +// Density experiment — CSS-var adapter (docs/adr/0001-css-var-density-adapter.md). +// Button consumes `var(--_Button-padding*)`, resolved inline from a (variant, +// size) lookup through `var(--Button--prop, var(--Button-prop, ))`. +// `enhanceDensity` wires `--Button-*` to the `--mui-density-*` scale. + +const VARIANTS = ['text', 'outlined', 'contained'] as const; +const SIZES = ['small', 'medium', 'large'] as const; + +const theme = enhanceDensity(createTheme({ cssVariables: true })); + +function ButtonMatrix() { + return ( + + {VARIANTS.map((variant) => ( + + {SIZES.map((size) => ( + + ))} + + ))} + + ); +} + +function Panel({ + title, + caption, + style, + children, +}: { + title: string; + caption: string; + style?: React.CSSProperties; + children: React.ReactNode; +}) { + return ( + + {title} + + {caption} + + {children} + + ); +} + +export default function DensityTokens() { + // --mui-density-* live retune (overrides the scale at this scope). + const [densityXs, setDensityXs] = React.useState(6); + const [densityLg, setDensityLg] = React.useState(16); + // Per-token overrides (granular, base + sized). + const [baseInline, setBaseInline] = React.useState(''); + const [smallInline, setSmallInline] = React.useState(''); + + const densityScope: React.CSSProperties = { + // Retunes every enhanced button without rebuilding the theme. + ['--mui-density-xs' as any]: `${densityXs}px`, + ['--mui-density-lg' as any]: `${densityLg}px`, + }; + + const tokenScope: React.CSSProperties = { + ...(baseInline ? { ['--Button-paddingInline' as any]: baseInline } : null), + ...(smallInline ? { ['--Button-small-paddingInline' as any]: smallInline } : null), + }; + + return ( + + + + + + Density tokens — CSS-var adapter + + + Button padding is exposed as --Button-paddingInline /{' '} + --Button-paddingBlock (base), --Button-<size>-paddingInline{' '} + (sized, wins over base), with a literal-px fallback so the default is pixel-identical.{' '} + enhanceDensity wires the base tokens to the{' '} + --mui-density-* scale. + + + + + + + + + + + + + + Density scale + + --mui-density-xs (base block): {densityXs}px + setDensityXs(value as number)} + /> + + + + --mui-density-lg (base inline): {densityLg}px + + setDensityLg(value as number)} + /> + + + + + + + Per-token override (granular) + setBaseInline(event.target.value)} + /> + setSmallInline(event.target.value)} + /> + + + Scoped preview + + + + + + + + enhanceDensity toggle + + } + label="enhanceDensity is applied to this page's theme (createTheme is untouched)." + /> + + + ); +} diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index cbed494f1e7399..53277e3074a57d 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -73,6 +73,15 @@ const commonIconStyles = [ }, ]; +// [block, inline] padding per (variant, size), in px — today's exact values. +// Resolved to `--_Button-padding*` via inline style so the (variant, size) +// matrix needs no static CSS and custom sizes work; see docs/adr/0001. +const buttonPadding = { + text: { small: ['4px', '5px'], medium: ['6px', '8px'], large: ['8px', '11px'] }, + outlined: { small: ['3px', '9px'], medium: ['5px', '15px'], large: ['7px', '21px'] }, + contained: { small: ['4px', '10px'], medium: ['6px', '16px'], large: ['8px', '22px'] }, +}; + const ButtonRoot = styled(ButtonBase, { shouldForwardProp: (prop) => rootShouldForwardProp(prop) || prop === 'classes', name: 'MuiButton', @@ -100,7 +109,9 @@ const ButtonRoot = styled(ButtonBase, { return { ...theme.typography.button, minWidth: 64, - padding: '6px 16px', + // `--_Button-padding*` are set via inline style from the (variant, size) + // lookup; the literals here are only a safety fallback (medium contained). + padding: 'var(--_Button-paddingBlock, 6px) var(--_Button-paddingInline, 16px)', border: 0, borderRadius: (theme.vars || theme).shape.borderRadius, transition: theme.transitions.create( @@ -145,7 +156,6 @@ const ButtonRoot = styled(ButtonBase, { { props: { variant: 'outlined' }, style: { - padding: '5px 15px', border: '1px solid currentColor', borderColor: `var(--variant-outlinedBorder, currentColor)`, backgroundColor: `var(--variant-outlinedBg)`, @@ -158,7 +168,6 @@ const ButtonRoot = styled(ButtonBase, { { props: { variant: 'text' }, style: { - padding: '6px 8px', color: `var(--variant-textColor)`, backgroundColor: `var(--variant-textBg)`, }, @@ -225,7 +234,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'text', }, style: { - padding: '4px 5px', fontSize: theme.typography.pxToRem(13), }, }, @@ -235,7 +243,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'text', }, style: { - padding: '8px 11px', fontSize: theme.typography.pxToRem(15), }, }, @@ -245,7 +252,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'outlined', }, style: { - padding: '3px 9px', fontSize: theme.typography.pxToRem(13), }, }, @@ -255,7 +261,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'outlined', }, style: { - padding: '7px 21px', fontSize: theme.typography.pxToRem(15), }, }, @@ -265,7 +270,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'contained', }, style: { - padding: '4px 10px', fontSize: theme.typography.pxToRem(13), }, }, @@ -275,7 +279,6 @@ const ButtonRoot = styled(ButtonBase, { variant: 'contained', }, style: { - padding: '8px 22px', fontSize: theme.typography.pxToRem(15), }, }, @@ -550,6 +553,18 @@ const Button = React.forwardRef(function Button(inProps, ref) { const classes = useUtilityClasses(ownerState); + // Resolve the (variant, size) padding cell to the internal vars. Each falls + // through: sized token -> base token -> literal, so overriding either public + // token at any scope reflows the button (see docs/adr/0001). Unknown variant + // falls back to the root default, unknown size to the variant's medium. + const variantPadding = buttonPadding[variant]; + const [paddingBlock, paddingInline] = (variantPadding && + (variantPadding[size] || variantPadding.medium)) || ['6px', '16px']; + const densityVars = { + '--_Button-paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${paddingBlock}))`, + '--_Button-paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${paddingInline}))`, + }; + const startIcon = (startIconProp || (loading && loadingPosition === 'start')) && ( {startIconProp || ( @@ -602,6 +617,7 @@ const Button = React.forwardRef(function Button(inProps, ref) { type={type} id={loading ? loadingId : idProp} {...other} + style={{ ...densityVars, ...other.style }} classes={forwardedClasses} > {startIcon} @@ -721,6 +737,10 @@ Button.propTypes /* remove-proptypes */ = { * Element placed before the children. */ startIcon: PropTypes.node, + /** + * @ignore + */ + style: PropTypes.object, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts new file mode 100644 index 00000000000000..6d37be4d2769c5 --- /dev/null +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -0,0 +1,124 @@ +import { Theme } from './createTheme'; + +/** + * Named density steps, surfaced as `--mui-density-*` CSS vars. Components wired + * by `enhanceDensity` pull their spacing tokens from these. + */ +export interface DensityScale { + xxs: string; + xs: string; + sm: string; + md: string; + lg: string; + xl: string; +} + +export interface DensityOptions { + /** + * Override any density step. Defaults derive from `theme.spacing`. + */ + scale?: Partial | undefined; +} + +type DensityKey = keyof DensityScale; + +const densityKeys: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl']; + +// Default scale: t-shirt steps derived from the theme spacing unit. +const defaultMultiplier: Record = { + xxs: 0.5, + xs: 0.75, + sm: 1, + md: 1.5, + lg: 2, + xl: 3, +}; + +const cssVar = (key: DensityKey) => `--mui-density-${key}`; + +/** + * Enhances a created theme with a holistic density layer. + * + * Does both jobs in one call (mirroring `enhanceHighContrast`): + * 1. **Emits** the density scale as `--mui-density-*` CSS vars at `:root` + * (via `MuiCssBaseline` — requires ``) and exposes them on + * `theme.density` / `theme.vars.density`. + * 2. **Maps** each component's spacing tokens to a density step through injected + * `styleOverrides.root` (e.g. `--Button-paddingInline: var(--mui-density-lg)`). + * + * `createTheme` is left untouched; without this function components render their + * literal-px defaults. See `docs/adr/0001-css-var-density-adapter.md`. + * + * @param themeInput - The created theme to enhance. + * @param options - Override the density scale. + * @returns The enhanced theme. + * + * @example + * const theme = enhanceDensity(createTheme({ cssVariables: true })); + * + * @example + * const theme = enhanceDensity(createTheme(), { scale: { lg: '12px' } }); + */ +export default function enhanceDensity< + T extends { + spacing: (value: number) => string | number; + components?: Theme['components'] | undefined; + vars?: Record | undefined; + }, +>(themeInput: T, options?: DensityOptions): T & { density: DensityScale } { + const scale = densityKeys.reduce((acc, key) => { + acc[key] = options?.scale?.[key] ?? String(themeInput.spacing(defaultMultiplier[key])); + return acc; + }, {} as DensityScale); + + const rootVars = densityKeys.reduce>((acc, key) => { + acc[cssVar(key)] = scale[key]; + return acc; + }, {}); + + const varRefs = densityKeys.reduce((acc, key) => { + acc[key] = `var(${cssVar(key)})`; + return acc; + }, {} as DensityScale); + + const theme = { ...themeInput } as T & { density: DensityScale }; + theme.density = scale; + theme.vars = { ...themeInput.vars, density: varRefs }; + + const c = themeInput.components; + const existingBaseline = c?.MuiCssBaseline?.styleOverrides; + const baselineObject = + existingBaseline && typeof existingBaseline === 'object' ? existingBaseline : undefined; + + theme.components = { + ...c, + MuiCssBaseline: { + ...c?.MuiCssBaseline, + styleOverrides: { + ...baselineObject, + ':root': { + ...(baselineObject as any)?.[':root'], + ...rootVars, + }, + }, + }, + MuiButton: { + ...c?.MuiButton, + styleOverrides: { + ...c?.MuiButton?.styleOverrides, + root: [ + c?.MuiButton?.styleOverrides?.root, + { + // Base tokens cover every size, so density flattens the size matrix + // to one comfortable value (set sized tokens to keep per-size + // steps). Medium block/inline map to xs/lg by default. + '--Button-paddingBlock': varRefs.xs, + '--Button-paddingInline': varRefs.lg, + }, + ], + }, + }, + }; + + return theme; +} diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index 1241e52782be10..90e00f9a7207ca 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -9,6 +9,7 @@ export { CssThemeVariables, } from './createTheme'; export { default as enhanceHighContrast, HighContrastTokens } from './enhanceHighContrast'; +export { default as enhanceDensity, DensityScale, DensityOptions } from './enhanceDensity'; export { default as adaptV4Theme, DeprecatedThemeOptions } from './adaptV4Theme'; export { Shadows } from './shadows'; export { ZIndex } from './zIndex'; diff --git a/packages/mui-material/src/styles/index.js b/packages/mui-material/src/styles/index.js index b9dfa4913c7d61..c78e02b9604d71 100644 --- a/packages/mui-material/src/styles/index.js +++ b/packages/mui-material/src/styles/index.js @@ -26,6 +26,7 @@ export function experimental_sx() { } export { default as createTheme } from './createTheme'; export { default as enhanceHighContrast } from './enhanceHighContrast'; +export { default as enhanceDensity } from './enhanceDensity'; export { default as unstable_createMuiStrictModeTheme } from './createMuiStrictModeTheme'; export { default as createStyles } from './createStyles'; export { getUnit as unstable_getUnit, toUnitless as unstable_toUnitless } from './cssUtils'; From 40ea056a325d66ad21d6172de2b6bcb728290fc3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 5 Jun 2026 12:51:49 +0900 Subject: [PATCH 002/114] [docs] Fix density experiment CI: Head description, prettier, vale prose --- CONTEXT.md | 8 ++++---- docs/adr/0001-css-var-density-adapter.md | 6 +++--- docs/pages/experiments/density-tokens.tsx | 17 +++++++++++------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 7e07c49a824983..80e2b1d3f25e8a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,6 +1,6 @@ # Density (CSS-var adapter) -How Material UI component dimensions (padding / gap / height) are exposed as +How Material UI component dimensions (padding / gap / height) are exposed as hand-authorable CSS variables so a designer can tune component density — per component, per size, or holistically — without touching component source, doing `calc` arithmetic, or riding the single `--mui-spacing` dial. @@ -14,14 +14,14 @@ dimension is an overridable token whose default is a literal px. **Component spacing token** (public, base): A per-component, per-CSS-property variable a designer may set, shape `--Component-` — PascalCase component, camelCase **logical** CSS -property, unprefixed (e.g. `--Button-paddingInline`, `--Chip-gap`). Matches the +property, unprefixed (for example `--Button-paddingInline`, `--Chip-gap`). Matches the existing component-var convention (`--AppBar-background`). Setting it reflows that property across every variant and size of the component. _Avoid_: kebab property (`--Button-padding-inline`), `--mui-`-prefixed component vars, "variable". **Sized token** (public, size-specific): A size-scoped override, shape `--Component--` -(e.g. `--Button-small-paddingInline`). Reflows only that one size. **More +(for example `--Button-small-paddingInline`). Reflows only that one size. **More specific than the base token** — when both are set, the sized token wins. _Avoid_: "size variant token". @@ -59,7 +59,7 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). ## Relationships -- The styled root reads **one** internal resolution var per property; **no JS +- The styled root reads **one** internal resolution var per property; **no JavaScript conditional** lives in the styles implementation. The `(variant, size)` → px matrix is a **lookup table** in the component body, applied via inline style. - Override priority (high → low): plain `styleOverrides` property → **sized diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 6361de7ba99cba..a7edd5c13c9b51 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -19,14 +19,14 @@ Constraints that shaped the design: token) per variant×size×property cell. - **Support user-provided sizes.** A custom `size` added via the theme must get the same tunability as built-in sizes. -- **No JS conditionals in the styles implementation.** The `styled()` body must +- **No JavaScript conditionals in the styles implementation.** The `styled()` body must not branch on `ownerState.size`/`variant` to pick a value. - **Non-breaking.** Existing variant/size padding and existing `styleOverrides`/`sx` overrides must keep working unchanged. ## Decision -Three token layers per component property, e.g. inline padding on Button: +Three token layers per component property, for example inline padding on Button: - **Base token** `--Button-paddingInline` (public) — reflows all variants/sizes. - **Sized token** `--Button--paddingInline` (public) — reflows one size; @@ -53,7 +53,7 @@ const sizingVars = { '--_Button-paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${block}))`, '--_Button-paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${inline}))`, }; - +; ``` Holistic density is a separate, opt-in layer driven by a **single** diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx index ec6381b6178e48..00cc00dc4c322a 100644 --- a/docs/pages/experiments/density-tokens.tsx +++ b/docs/pages/experiments/density-tokens.tsx @@ -84,17 +84,20 @@ export default function DensityTokens() { return ( - + Density tokens — CSS-var adapter Button padding is exposed as --Button-paddingInline /{' '} - --Button-paddingBlock (base), --Button-<size>-paddingInline{' '} - (sized, wins over base), with a literal-px fallback so the default is pixel-identical.{' '} - enhanceDensity wires the base tokens to the{' '} - --mui-density-* scale. + --Button-paddingBlock (base),{' '} + --Button-<size>-paddingInline (sized, wins over base), with a + literal-px fallback so the default is pixel-identical. enhanceDensity wires + the base tokens to the --mui-density-* scale. @@ -117,7 +120,9 @@ export default function DensityTokens() { Density scale - --mui-density-xs (base block): {densityXs}px + + --mui-density-xs (base block): {densityXs}px + Date: Fri, 5 Jun 2026 12:55:30 +0900 Subject: [PATCH 003/114] [docs] Fix Button a11y snapshot (avoid-inline-spacing) + mobile layout of density experiment --- .../components/buttons/buttons.a11y.json | 8 ++++++ docs/pages/experiments/density-tokens.tsx | 27 +++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/data/material/components/buttons/buttons.a11y.json b/docs/data/material/components/buttons/buttons.a11y.json index c440f41f0f264e..146595d0af9bad 100644 --- a/docs/data/material/components/buttons/buttons.a11y.json +++ b/docs/data/material/components/buttons/buttons.a11y.json @@ -21,6 +21,10 @@ "status": "pass", "tags": ["wcag2a"] }, + "avoid-inline-spacing": { + "status": "pass", + "tags": ["wcag21aa"] + }, "button-name": { "status": "pass", "tags": ["wcag2a"] @@ -61,6 +65,10 @@ "status": "pass", "tags": ["wcag2a"] }, + "avoid-inline-spacing": { + "status": "pass", + "tags": ["wcag21aa"] + }, "button-name": { "status": "pass", "tags": ["wcag2a"] diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx index 00cc00dc4c322a..e7a62a057b0495 100644 --- a/docs/pages/experiments/density-tokens.tsx +++ b/docs/pages/experiments/density-tokens.tsx @@ -26,15 +26,20 @@ const theme = enhanceDensity(createTheme({ cssVariables: true })); function ButtonMatrix() { return ( - + {VARIANTS.map((variant) => ( - - {SIZES.map((size) => ( - - ))} - + + + {variant} + + + {SIZES.map((size) => ( + + ))} + + ))} ); @@ -52,7 +57,7 @@ function Panel({ children: React.ReactNode; }) { return ( - + {title} {caption} @@ -117,7 +122,7 @@ export default function DensityTokens() { - + Density scale @@ -145,7 +150,7 @@ export default function DensityTokens() { - + Per-token override (granular) Date: Fri, 5 Jun 2026 16:03:25 +0900 Subject: [PATCH 004/114] [material-ui] Shorten Button internal density var to unprefixed --_padding* --- CONTEXT.md | 14 ++++++++------ docs/adr/0001-css-var-density-adapter.md | 10 +++++----- docs/pages/experiments/density-tokens.tsx | 2 +- packages/mui-material/src/Button/Button.js | 12 +++++++----- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 80e2b1d3f25e8a..bb6361ffbe3504 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -26,12 +26,14 @@ specific than the base token** — when both are set, the sized token wins. _Avoid_: "size variant token". **Internal resolution var**: -A private variable, shape `--_Component-` (leading underscore), -**set via inline style** from the rendered `(variant, size)` and consumed once -in the styled root. It carries the full fallback chain (sized token → base -token → literal). Lowest priority, so any public token or plain `styleOverrides` -property still wins. -_Avoid_: exposing it as API, "private token". +A private variable, shape `--_` (leading underscore, **no component +prefix**), **set via inline style** from the rendered `(variant, size)` and +consumed once in the styled root of the same element. It carries the full +fallback chain (sized token → base token → literal). No prefix is needed because +the reader is co-located with the inline setter, so an ancestor's value never +bleeds into a descendant. Lowest priority, so any public token or plain +`styleOverrides` property still wins. +_Avoid_: exposing it as API, prefixing with the component name, "private token". **Token fallback**: The literal px at the end of the chain — today's exact value for that diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index a7edd5c13c9b51..848e004cc37bf6 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -31,7 +31,7 @@ Three token layers per component property, for example inline padding on Button: - **Base token** `--Button-paddingInline` (public) — reflows all variants/sizes. - **Sized token** `--Button--paddingInline` (public) — reflows one size; **more specific than base** (size wins). -- **Internal resolution var** `--_Button-paddingInline` (private, leading +- **Internal resolution var** `--_paddingInline` (private, leading underscore) — set via **inline style** from the rendered `(variant, size)`, carrying the chain `var(--Button--paddingInline, var(--Button-paddingInline, ))`. @@ -39,8 +39,8 @@ The styled root has **one** consumption point per property and **no conditional* ```js const ButtonRoot = styled(ButtonBase)({ - paddingInline: 'var(--_Button-paddingInline)', - paddingBlock: 'var(--_Button-paddingBlock)', + paddingInline: 'var(--_paddingInline)', + paddingBlock: 'var(--_paddingBlock)', }); ``` @@ -50,8 +50,8 @@ The component body holds the `(variant, size)` → px values as a **lookup table ```js const [block, inline] = PADDING[variant][size]; const sizingVars = { - '--_Button-paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${block}))`, - '--_Button-paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${inline}))`, + '--_paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${block}))`, + '--_paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${inline}))`, }; ; ``` diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx index e7a62a057b0495..e67a73f8f22d16 100644 --- a/docs/pages/experiments/density-tokens.tsx +++ b/docs/pages/experiments/density-tokens.tsx @@ -15,7 +15,7 @@ import Typography from '@mui/material/Typography'; import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; // Density experiment — CSS-var adapter (docs/adr/0001-css-var-density-adapter.md). -// Button consumes `var(--_Button-padding*)`, resolved inline from a (variant, +// Button consumes `var(--_padding*)`, resolved inline from a (variant, // size) lookup through `var(--Button--prop, var(--Button-prop, ))`. // `enhanceDensity` wires `--Button-*` to the `--mui-density-*` scale. diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index 53277e3074a57d..6d995eaf30ddde 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -109,9 +109,11 @@ const ButtonRoot = styled(ButtonBase, { return { ...theme.typography.button, minWidth: 64, - // `--_Button-padding*` are set via inline style from the (variant, size) - // lookup; the literals here are only a safety fallback (medium contained). - padding: 'var(--_Button-paddingBlock, 6px) var(--_Button-paddingInline, 16px)', + // `--_padding*` are set via inline style from the (variant, size) lookup; + // the literals here are only a safety fallback (medium contained). The + // internal var is unprefixed — it's read on the same element that sets it + // inline, so no component prefix is needed to avoid inheritance bleed. + padding: 'var(--_paddingBlock, 6px) var(--_paddingInline, 16px)', border: 0, borderRadius: (theme.vars || theme).shape.borderRadius, transition: theme.transitions.create( @@ -561,8 +563,8 @@ const Button = React.forwardRef(function Button(inProps, ref) { const [paddingBlock, paddingInline] = (variantPadding && (variantPadding[size] || variantPadding.medium)) || ['6px', '16px']; const densityVars = { - '--_Button-paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${paddingBlock}))`, - '--_Button-paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${paddingInline}))`, + '--_paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${paddingBlock}))`, + '--_paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${paddingInline}))`, }; const startIcon = (startIconProp || (loading && loadingPosition === 'start')) && ( From 2748d1cc2b2461d37c667c7eee0b26740b5af3c3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 7 Jun 2026 13:31:00 +0700 Subject: [PATCH 005/114] [material-ui] Refine Button density adapter: variants-resolved --_pad + --Button-pad seam - Root consumes var(--Button-pad, var(--_pad)); --_pad universal default on root - (variant,size) literals + built-in-size routing live in variants (deduped CSS) - Inline bridge only for custom sizes (keeps custom sizes tunable, zero inline for built-ins) - Two-var rationale + accepted trade-offs documented in ADR-0001 + CONTEXT - enhanceDensity maps sized tokens (--Button--pad) to density scale --- CONTEXT.md | 112 +++++++++----- docs/adr/0001-css-var-density-adapter.md | 143 +++++++++++++----- docs/pages/experiments/density-tokens.tsx | 57 ++++--- packages/mui-material/src/Button/Button.js | 61 +++++--- .../mui-material/src/styles/enhanceDensity.ts | 14 +- 5 files changed, 254 insertions(+), 133 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index bb6361ffbe3504..bbd23f85b2f781 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -9,34 +9,53 @@ This is the **adapter** sibling of the earlier `--mui-spacing`-derived experiment (`feat/components-theme-spacing`): instead of one global dial, each dimension is an overridable token whose default is a literal px. +## Layers + +The component is read as **three layers of responsibility**, each owning a slice +of one cascade: + +- **Agnostic** — the styled root, no design meaning (no size/variant/color). Its + spacing surface is one public var it consumes directly, falling back to the + internal default (`padding: var(--Button-pad, var(--_pad))`). +- **Material UI** — Material Design's sizes/variants, all in `variants`: the + `(variant, size)` literal defaults (`--_pad`) and the sized-token routing + (`--Button-pad`) for built-in sizes. A custom size routes inline instead (it + needs the runtime size string). +- **Design system** — tunes the public **sized tokens** the Material UI layer + routes over its default (wired via `enhanceDensity`). + ## Language -**Component spacing token** (public, base): -A per-component, per-CSS-property variable a designer may set, shape -`--Component-` — PascalCase component, camelCase **logical** CSS -property, unprefixed (for example `--Button-paddingInline`, `--Chip-gap`). Matches the -existing component-var convention (`--AppBar-background`). Setting it reflows -that property across every variant and size of the component. -_Avoid_: kebab property (`--Button-padding-inline`), `--mui-`-prefixed component vars, "variable". - -**Sized token** (public, size-specific): -A size-scoped override, shape `--Component--` -(for example `--Button-small-paddingInline`). Reflows only that one size. **More -specific than the base token** — when both are set, the sized token wins. -_Avoid_: "size variant token". - -**Internal resolution var**: -A private variable, shape `--_` (leading underscore, **no component -prefix**), **set via inline style** from the rendered `(variant, size)` and -consumed once in the styled root of the same element. It carries the full -fallback chain (sized token → base token → literal). No prefix is needed because -the reader is co-located with the inline setter, so an ancestor's value never -bleeds into a descendant. Lowest priority, so any public token or plain +**Agnostic var** (public, the layer-1 surface): +The single per-property variable the styled root consumes, shape +`--Component-` — PascalCase component, short semantic key (`pad`, `gap`), +unprefixed (for example `--Button-pad`). Matches the existing component-var +convention (`--AppBar-background`). The root reads it with the internal default +behind it (`var(--Button-pad, var(--_pad))`); the Material UI layer **sets it in +a per-size `variants` block** to the sized-token routing (inline for custom +sizes). A designer tunes it through the sized token, not by setting it directly. +_Avoid_: literal CSS-property keys (`--Button-padding`), kebab keys, `--mui-`-prefixed component vars, "variable". + +**Sized token** (public, the design-system knob): +A size-scoped override, shape `--Component--` +(for example `--Button-small-pad`). Reflows only that one size. The Material UI +layer routes it over the internal default; when set at any scope it wins. +**Resolution is sized-only** — there is no all-sizes base token. +_Avoid_: "size variant token", a base/all-sizes token. + +**Internal default**: +A private variable, shape `--_` (leading underscore, **no component +prefix**), **set in `variants`** per `(variant, size)` cell (medium defaults +reuse the `{ variant }` blocks), over a **universal default on the root** so a +custom variant/size still renders. It holds the Material default — today's exact +px for that cell. No prefix is needed because the reader (the agnostic var's +fallback / the routing) is on the same element, so an ancestor's value never +bleeds into a descendant. Lowest priority, so any sized token or plain `styleOverrides` property still wins. _Avoid_: exposing it as API, prefixing with the component name, "private token". **Token fallback**: -The literal px at the end of the chain — today's exact value for that +The literal px the internal default carries — today's exact value for that `(variant, size)` cell. Makes the default render pixel-identical and bundle-light, at the cost that the single `--mui-spacing` dial no longer reflows the component. _Avoid_: "default value", "initial". @@ -53,21 +72,29 @@ _Avoid_: "spacing scale" (that is `theme.spacing`), "grid". A single post-`createTheme` function (mirroring `enhanceHighContrast`) that does **both**: (a) emits the **density scale** as `--mui-density-*` and populates `theme.vars.density`, and (b) injects per-component `styleOverrides.root` mapping -**component spacing tokens** to density steps -(`--Button-paddingInline: theme.vars.density.md`). `createTheme` is untouched. +**sized tokens** to density steps +(`--Button-medium-pad: theme.vars.density.md`). `createTheme` is untouched. Opt-in: without it, components render their literal-px defaults; with it, tuning the density scale (or scoping `--mui-density-*`) reflows every wired component. _Avoid_: "density preset" (that is the resulting effect, not the function). ## Relationships -- The styled root reads **one** internal resolution var per property; **no JavaScript +- The styled root reads **one** agnostic var per property; **no JavaScript conditional** lives in the styles implementation. The `(variant, size)` → px - matrix is a **lookup table** in the component body, applied via inline style. + matrix and the built-in-size routing are both **`variants` cells**, not a body + lookup table; only custom-size routing is inline. +- **Two vars, not one** (`--Button-pad` over `--_pad`): the cells write the + *value* (`--_pad`), the routing writes a *reference* (`--Button-pad`). One var + fails three ways — a self-referencing fallback in the inline bridge (invalid + CSS, forcing the literal back into runtime style), the `(variant×size)` and + size-only write-axes clobbering on one element, and losing the agnostic seam. + Full reasoning in `docs/adr/0001` → *Why two vars*. - Override priority (high → low): plain `styleOverrides` property → **sized - token** → **base token** → internal resolution var (literal fallback). -- Custom (user-defined) sizes work for free: the sized-token name is built from - the runtime size string; no static per-size CSS is emitted. + token** → internal default (literal fallback). +- Custom (user-defined) sizes work for free: when the size isn't built-in, the + inline routing builds the sized-token name from the runtime size string; the + design system supplies the value via that token. - **enhanceDensity** (opt-in) connects tier-2 component tokens to the tier-1 **density scale**; un-enhanced, the literal fallbacks reproduce today's pixels. - This experiment does **not** ride `--mui-spacing`; holistic density comes from @@ -75,21 +102,26 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). ## Example dialogue -> **Dev:** "If I set `--Button-paddingInline` on the theme, what happens to a -> small outlined button?" -> **Domain expert:** "It reflows to your value — base token covers every -> variant and size. Unless you also set `--Button-small-paddingInline`; the -> **sized token** is more specific and wins for small." +> **Dev:** "How do I shrink the padding of small buttons?" +> **Domain expert:** "Set the **sized token** `--Button-small-pad` at any scope; +> the Material UI layer routes it over its default, so every small button +> reflows. Resolution is sized-only — there's no all-sizes base token, so do it +> per size." > **Dev:** "And with nothing set?" -> **Domain expert:** "The **internal resolution var**, set inline from the -> `(variant, size)` cell, falls through to the literal px — pixel-identical to -> today. The `--mui-spacing` dial does nothing here; for holistic density you -> run **enhanceDensity** and tune the **density scale**." +> **Domain expert:** "The agnostic `--Button-pad` falls back to the **internal +> default** `--_pad` — the literal px set in the `(variant, size)` `variants` +> cell, pixel-identical to today. The `--mui-spacing` dial does nothing here; for +> holistic density you run **enhanceDensity** and tune the **density scale**." ## Flagged ambiguities - "spacing token" meant both a `theme.spacing` key and a per-component value — - resolved: `theme.spacing` is untouched; per-component vars are **component - spacing tokens** (base) and **sized tokens**. + resolved: `theme.spacing` is untouched; per-component vars are the **agnostic + var** (layer-1 surface) and **sized tokens** (the design-system knob). - "spacing scale" (earlier draft, tier-1) — renamed **density scale** and moved to `theme.density`, to disambiguate from `theme.spacing`. +- Base (all-sizes) token — dropped; resolution is **sized-only**, so a designer + tunes per size. +- Var key — `pad` shorthand (single `padding`), not logical `paddingInline`/ + `paddingBlock`. Button padding is horizontally symmetric, so the physical + shorthand stays RTL-safe. diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 848e004cc37bf6..2916ec879fee8a 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -1,9 +1,10 @@ -# Component density via a CSS-var adapter, resolved inline +# Component density via a CSS-var adapter, resolved in variants Component dimensions are exposed as public CSS variables with **literal-px -fallbacks**, resolved through an **internal var set by inline style**, instead of -riding the single `--mui-spacing` dial (`feat/components-theme-spacing`) or -emitting a static per-(variant, size) token matrix (`poc/css-vars-map`). +fallbacks**, resolved through an **internal var whose value lives in `variants`** +(inline style bridges only the custom-size case), instead of riding the single +`--mui-spacing` dial (`feat/components-theme-spacing`) or emitting a static +per-(variant, size) token matrix (`poc/css-vars-map`). ## Context @@ -15,10 +16,12 @@ Constraints that shaped the design: - **Pixel-identical default.** The un-configured theme must render today's exact px for every `(variant, size)` cell (Argos zero-diff). -- **No token bloat at build time.** Don't emit a static CSS rule (or named - token) per variant×size×property cell. +- **Literal defaults in `variants`, not the body.** The `(variant, size)` → + px matrix uses the idiomatic `variants` mechanism (Button already has these + cells for `fontSize`); no JS lookup table lives in the component body. - **Support user-provided sizes.** A custom `size` added via the theme must get - the same tunability as built-in sizes. + the same tunability as built-in sizes — built-in routing is per-size `variants` + blocks; a custom size falls back to inline routing (dynamic size string). - **No JavaScript conditionals in the styles implementation.** The `styled()` body must not branch on `ownerState.size`/`variant` to pick a value. - **Non-breaking.** Existing variant/size padding and existing @@ -26,41 +29,94 @@ Constraints that shaped the design: ## Decision -Three token layers per component property, for example inline padding on Button: - -- **Base token** `--Button-paddingInline` (public) — reflows all variants/sizes. -- **Sized token** `--Button--paddingInline` (public) — reflows one size; - **more specific than base** (size wins). -- **Internal resolution var** `--_paddingInline` (private, leading - underscore) — set via **inline style** from the rendered `(variant, size)`, - carrying the chain `var(--Button--paddingInline, var(--Button-paddingInline, ))`. - -The styled root has **one** consumption point per property and **no conditional**: +The component is read as **three layers of responsibility**, each owning a +distinct slice of the same cascade: + +1. **Agnostic** — the styled root with no design meaning (no size/variant/color). + Its whole spacing surface is one public var it consumes directly, falling back + to the internal default: `padding: var(--Button-pad, var(--_pad))`. +2. **Material UI** — Material Design's sizes/variants, all in `variants`: the + `(variant, size)` literal **defaults** (`--_pad`) and the **sized-token + routing** for the built-in sizes (`--Button-pad`). No JS lookup in the body. + Custom (non-built-in) sizes route inline instead — the one case that needs the + runtime size string. +3. **Design system** — overrides through the public **sized token** the Material + UI layer routes over its default (driven by `enhanceDensity`). + +Per property, the chain (inline padding on Button): + +- **Agnostic var** `--Button-pad` (public) — the styled root's only spacing + consumption point; **set by a built-in-size `variants` block** to the + sized-token routing (inline for custom sizes), falling back to `--_pad`. +- **Sized token** `--Button--pad` (public) — the design-system knob; + reflows one size. +- **Internal default** `--_pad` (private, leading underscore) — the Material + default, **set in `variants`** per `(variant, size)`, with a universal default + on the root so a custom variant/size still renders a sane value. + +Resolution is **sized-only** (no all-sizes base token): the sized token wins, +else the Material default. + +The styled root has **one** consumption point per property and **no conditional**; +the defaults and built-in-size routing are plain `variants` entries: ```js const ButtonRoot = styled(ButtonBase)({ - paddingInline: 'var(--_paddingInline)', - paddingBlock: 'var(--_paddingBlock)', + '--_pad': '6px 16px', // universal default (today's root padding) + padding: 'var(--Button-pad, var(--_pad))', // agnostic layer + variants: [ + // routing for built-in sizes (deduped CSS) + { props: { size: 'small' }, style: { '--Button-pad': 'var(--Button-small-pad, var(--_pad))' } }, + // literal default per (variant, size); medium lives in the { variant } blocks (DRY) + { props: { variant: 'text', size: 'small' }, style: { '--_pad': '4px 5px' } }, + ], }); ``` -The component body holds the `(variant, size)` → px values as a **lookup table** -(today's exact numbers) and applies them via inline style: +Only **custom sizes** route inline — the one case needing the runtime size +string (so custom sizes stay tunable without registering a variant): ```js -const [block, inline] = PADDING[variant][size]; -const sizingVars = { - '--_paddingBlock': `var(--Button-${size}-paddingBlock, var(--Button-paddingBlock, ${block}))`, - '--_paddingInline': `var(--Button-${size}-paddingInline, var(--Button-paddingInline, ${inline}))`, -}; -; +const buttonSizes = ['small', 'medium', 'large']; +const densityVars = buttonSizes.includes(size) + ? undefined // built-in: routed via variants above + : { '--Button-pad': `var(--Button-${size}-pad, var(--_pad))` }; +; ``` +### Why two vars (`--Button-pad` and `--_pad`), not one + +Three reasons, all pointing the same way: + +1. **Values belong in `variants`; the inline bridge must stay value-free.** The + literal px is a design decision — it must live in `variants`, co-located with + the rest of the variant's styling, statically deduped, smallest diff from + today. Inline style is only a **bridge** for the one dynamic case (a custom + size's token name) and must carry **no values**, only routing. Two vars allow + that: cells write the value (`--_pad`), the bridge writes a *reference* + (`--Button-pad: var(--Button--pad, var(--_pad))`). With a single + `--_pad`, the custom-size bridge would have to write + `--_pad: var(--Button--pad, var(--_pad))` — a property referencing + **itself**, which CSS treats as guaranteed-invalid. The only escape is + embedding the literal back into the inline string, dragging the value into + runtime style — exactly what we moved out. +2. **Two write-axes on one element clobber if they share a name.** The literal + varies by **(variant × size)** (the cells); the token interception varies by + **size only** (`--Button--pad`, the routing). A single `--_pad` keeps + exactly one: routing-wins loses the per-variant literal (and the size block + can't supply the right fallback — it doesn't know the variant); literal-wins + never consults the token, so no override. Two names let each axis write + independently; `--Button-pad` chains to `--_pad`, the root reads `--Button-pad`. +3. **Layer seam (naming).** `--Button-pad` is public-shaped because it's the + **agnostic-layer seam** — the var a no-design consumer of the bare root would + set; Material UI takes it over to inject token routing. `--_pad` is private: + Material UI's internal default, not a contract. + Holistic density is a separate, opt-in layer driven by a **single** `enhanceDensity(theme)` function (mirroring `enhanceHighContrast`) that does both jobs: it **emits** the density scale as `--mui-density-*` (and populates -`theme.vars.density`), and **maps** base tokens to density steps via injected -`styleOverrides.root` (`--Button-paddingInline: var(--mui-density-md)`). +`theme.vars.density`), and **maps** sized tokens to density steps via injected +`styleOverrides.root` (`--Button-medium-pad: var(--mui-density-md)`). `createTheme` is left untouched. Types for `theme.vars.density` ship built-in; the vars exist at runtime only after `enhanceDensity` runs. @@ -75,15 +131,28 @@ Scope: **Button only** for this experiment. ## Consequences -- **Pixel-identical default & non-breaking.** Literals come from the lookup - table; the internal var is the lowest-priority fallback, so public tokens, - `styleOverrides`, and `sx` all still win via the cascade. -- **Custom sizes work for free** — the sized-token name is built from the - runtime size string; nothing static is emitted per size. -- **Inline style is the price.** Every instance carries a `style` attr with the - resolution vars (larger HTML, no CSS dedup of those values) — accepted to kill - the static token matrix and support arbitrary sizes. +- **Pixel-identical default & non-breaking.** Literals come from the `variants` + cells (`--_pad`) over a universal root default, so a custom variant/size still + renders; public tokens, `styleOverrides`, and `sx` all still win via the cascade. +- **No inline for built-in sizes.** Routing for `small`/`medium`/`large` lives in + `variants` (deduped CSS), so the common case carries no per-instance `style` + attr. Only a **custom size** routes inline. +- **Custom sizes work for free** — the inline routing builds the sized-token name + from the runtime size string; the design system supplies the value via that + token, no variant registration needed. - **No `--mui-spacing` reflow.** Components opt out of the global dial; holistic density flows through the density scale + `enhanceDensity` instead. - **calc resolves only in a real browser** (jsdom does not), so density assertions belong in browser/visual tests, not jsdom unit tests. + +### Accepted trade-offs + +| Trade-off | Why we can live with it | +| --- | --- | +| `--Button-pad` is public-shaped but not a designer knob in the assembled Button (plumbing) | It's the agnostic seam; the real knob is `--Button--pad`, documented. The name marks the layer boundary. | +| Two vars per property instead of one | Mandatory (see *Why two vars*); the indirection is mechanical and documented. | +| Unprefixed `--_pad` could inherit a foreign value | Every built-in cell plus the root universal default set it on the element; revisit a prefix only if cross-component collisions surface as the pattern spreads. | +| `pad` shorthand is coarse (an override sets all sides) | Button padding is symmetric; tiny token surface; granular logical props can come later. | +| `var()` unresolved in jsdom (no computed-px assertions) | Argos covers default visuals; the chain is declarative and inspectable. | +| Inline still present for custom sizes | Rare; the built-in common case carries zero inline. | +| Per-property boilerplate grows with rollout | Acceptable for the payoff (runtime scoped theming); extract a helper before component #3. | diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx index e67a73f8f22d16..190523bd568975 100644 --- a/docs/pages/experiments/density-tokens.tsx +++ b/docs/pages/experiments/density-tokens.tsx @@ -15,9 +15,11 @@ import Typography from '@mui/material/Typography'; import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; // Density experiment — CSS-var adapter (docs/adr/0001-css-var-density-adapter.md). -// Button consumes `var(--_padding*)`, resolved inline from a (variant, -// size) lookup through `var(--Button--prop, var(--Button-prop, ))`. -// `enhanceDensity` wires `--Button-*` to the `--mui-density-*` scale. +// Agnostic layer: Button consumes `var(--Button-pad, var(--_pad))`. Material UI +// layer sets the (variant, size) literal default `--_pad` and the built-in-size +// routing `--Button-pad: var(--Button--pad, var(--_pad))` in `variants` +// (custom sizes route inline). `enhanceDensity` wires the sized tokens to the +// `--mui-density-*` scale. const VARIANTS = ['text', 'outlined', 'contained'] as const; const SIZES = ['small', 'medium', 'large'] as const; @@ -32,7 +34,12 @@ function ButtonMatrix() { {variant} - + {SIZES.map((size) => ( + + + + ))} + + ), + OutlinedInput: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + @} + /> + + ))} + + ), + TextField: ( + + {(['medium', 'small'] as const).map((size) => ( + + {`outlined ${size}`} + + + ))} + + $, + endAdornment: kg, + }, + }} + /> + + + ), +}; + +// Per-component density-token overrides for the review levels. `default` is +// empty on purpose — that render is the pixel-identical regression gate. +const scopes: Record> = { + Button: { + dense: { + ['--Button-small-pad' as any]: '2px 6px', + ['--Button-medium-pad' as any]: '3px 10px', + ['--Button-large-pad' as any]: '4px 14px', + }, + loose: { + ['--Button-small-pad' as any]: '8px 14px', + ['--Button-medium-pad' as any]: '12px 22px', + ['--Button-large-pad' as any]: '16px 30px', + }, + }, + OutlinedInput: { + dense: { + ['--OutlinedInput-small-padBlock' as any]: '4px', + ['--OutlinedInput-medium-padBlock' as any]: '10px', + }, + loose: { + ['--OutlinedInput-small-padBlock' as any]: '14px', + ['--OutlinedInput-medium-padBlock' as any]: '28px', + }, + }, +}; +// TextField rides the same OutlinedInput tokens; OutlinedInput's `:has` rule +// drives the label's --InputLabel-y, so input box + label move together. +scopes.TextField = scopes.OutlinedInput; + +export default function DensityFixture() { + const router = useRouter(); + const component = (router.query.c as string) || 'Button'; + const level = (router.query.level as string) || 'default'; + const demo = demos[component] ??
No demo registered for "{component}".
; + const tokens = scopes[component]?.[level] ?? {}; + return ( + + + {demo} + + + ); +} diff --git a/package.json b/package.json index bb3cb74af4faf7..dc0eda295fb615 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "test:coverage": "pnpm test:unit run --coverage", "vitest": "vitest", "test:coverage:html": "pnpm test:unit run --coverage --coverage.reporter html", + "density:shot": "playwright test -c scripts/density-screenshots/playwright.config.mjs", + "density:shot:update": "playwright test -c scripts/density-screenshots/playwright.config.mjs --update-snapshots", "test:e2e": "pnpm -F ./test/e2e start", "test:e2e:dev": "pnpm -F ./test/e2e dev", "test:e2e-website": "playwright test test/e2e-website --config test/e2e-website/playwright.config.ts", diff --git a/scripts/density-screenshots/README.md b/scripts/density-screenshots/README.md new file mode 100644 index 00000000000000..8a97040139991b --- /dev/null +++ b/scripts/density-screenshots/README.md @@ -0,0 +1,49 @@ +# Density-adapter screenshot harness + +Local visual verification for the CSS-var density adapter — no Argos. +Decision/spec: [`docs/adr/0001-css-var-density-adapter.md`](../../docs/adr/0001-css-var-density-adapter.md). + +Asserts the default render is **pixel-identical** before/after the change +(Playwright `toHaveScreenshot`, `maxDiffPixels: 0`) and captures density +screenshots (token `dense`/`loose` levels) for human review. + +Unlike the `--mui-spacing` sibling experiment, density here is driven by +per-component tokens (`--Button--pad`, `--OutlinedInput--padBlock`), +so the review `level` maps to those tokens — see `scopes` in the fixture. + +## Prerequisites + +- `pnpm docs:dev` running (serves the fixture at + `/experiments/density-fixture`). Override the URL with `DENSITY_BASE_URL`. +- Chromium for Playwright: `pnpm exec playwright install chromium` (once). + +## Steps (per component) + +1. Add the component's matrix to the `demos` map (and its token overrides to + `scopes`) in `docs/pages/experiments/density-fixture.tsx`. +2. **Baseline (before)** — on the _unconverted_ component (from `master`): + + ```bash + git stash # or check out the component file(s) from master + COMPONENT= pnpm density:shot:update + git stash pop + ``` + + Writes the baseline to `scripts/density-screenshots/__baselines__/`. + +3. Implement / keep the density-adapter in the component. +4. **Assert + density (after)**: + + ```bash + COMPONENT= pnpm density:shot + ``` + + - Fails if the default render differs from the baseline (⇒ not + pixel-identical); a diff image is written under `.playwright-output/`. + - Writes `density-screenshots//after-{default,dense,loose}.png`. + +5. Eyeball `after-dense.png` / `after-loose.png` for density reflow (and, for + TextField, that the floating label stays centered). + +Outputs (`density-screenshots/`, `__baselines__/`, `.playwright-output/`) are +gitignored. diff --git a/scripts/density-screenshots/density.spec.mjs b/scripts/density-screenshots/density.spec.mjs new file mode 100644 index 00000000000000..9025e626fc1b2b --- /dev/null +++ b/scripts/density-screenshots/density.spec.mjs @@ -0,0 +1,35 @@ +import path from 'node:path'; +import { test, expect } from '@playwright/test'; + +// Component under verification, e.g. `COMPONENT=OutlinedInput pnpm density:shot`. +const component = process.env.COMPONENT || 'Button'; +const outDir = path.resolve(process.cwd(), 'density-screenshots', component); +const scopeSelector = '#density-scope'; + +async function scopeAt(page, level) { + await page.goto(`/experiments/density-fixture?c=${component}&level=${level}`); + const scope = page.locator(scopeSelector); + await scope.waitFor(); + await page.evaluate(() => document.fonts.ready); + return scope; +} + +test.describe.configure({ mode: 'serial' }); + +// Regression gate: the default render (no density tokens) must match the +// "before" baseline exactly. Capture the baseline on the UNCONVERTED component +// (from master) with --update-snapshots. +test(`${component} — default is pixel-identical to baseline`, async ({ page }) => { + const scope = await scopeAt(page, 'default'); + 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). Each level sets +// the component's density tokens (see density-fixture.tsx `scopes`). +for (const level of ['dense', 'loose']) { + test(`${component} — density ${level} (review)`, async ({ page }) => { + const scope = await scopeAt(page, level); + await scope.screenshot({ path: path.join(outDir, `after-${level}.png`) }); + }); +} diff --git a/scripts/density-screenshots/playwright.config.mjs b/scripts/density-screenshots/playwright.config.mjs new file mode 100644 index 00000000000000..0d29202f75f1ea --- /dev/null +++ b/scripts/density-screenshots/playwright.config.mjs @@ -0,0 +1,19 @@ +import { defineConfig } from '@playwright/test'; + +// Local verification harness for the CSS-var density adapter. +// See docs/adr/0001-css-var-density-adapter.md and ./README.md. +export default defineConfig({ + testDir: '.', + testMatch: /density\.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.DENSITY_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 } }, +}); From a89c0627146008d5c5a3f3cd8f16c81ff8fc110b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 7 Jun 2026 18:26:05 +0700 Subject: [PATCH 008/114] [material-ui] Add OutlinedInput inline padding base token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tokenize the 14px inline gutter as --OutlinedInput-padInline (size-invariant base token) - Uniform consume shape var(--seam, var(--_internal)) across both axes: block sized (routed), inline base; --_padInline internal default - Docs: base-token shape in ADR/CONTEXT; rollout gotchas — split-only-if-forced, uniform consume shape, inline gutter != adornment gap --- CONTEXT.md | 36 +++++++++++----- docs/adr/0001-css-var-density-adapter.md | 24 ++++++++--- docs/adr/density-adapter-rollout.md | 43 ++++++++++++++++--- .../src/OutlinedInput/OutlinedInput.js | 19 ++++---- 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 468755265279c5..3c15ebf36c561f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -40,8 +40,17 @@ _Avoid_: literal CSS-property keys (`--Button-padding`), kebab keys, `--mui-`-pr A size-scoped override, shape `--Component--` (for example `--Button-small-pad`). Reflows only that one size. The Material UI layer routes it over the internal default; when set at any scope it wins. -**Resolution is sized-only** — there is no all-sizes base token. -_Avoid_: "size variant token", a base/all-sizes token. +For a **size-varying** axis, resolution is **sized-only** — no all-sizes base token. +_Avoid_: "size variant token". + +**Base token** (public, size-invariant axes only): +When an axis does **not** vary by size (e.g. OutlinedInput's `14px` inline +gutter), skip the **size layer** but keep the same shape: internal default +`--_` + seam, consumed `var(--Component-, var(--_))` +(`var(--OutlinedInput-padInline, var(--_padInline))`, `--_padInline: 14px`). The +seam *is* the knob — nothing routes it, so a designer sets it directly. Use only +when the value is genuinely constant across sizes; otherwise use a **sized token**. +_Avoid_: a base token for a size-varying axis; a bare literal default (use `--_`). **Internal default**: A private variable, shape `--_` (leading underscore, **no component @@ -96,10 +105,13 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). - Custom (user-defined) sizes work for free: when the size isn't built-in, the inline routing builds the sized-token name from the runtime size string; the design system supplies the value via that token. -- **Token granularity follows the component's spacing structure.** Button tunes - all sides together → one `pad` shorthand var. OutlinedInput's density is - vertical only (the `14px` inline gutter is constant) → a single `padBlock` var; - and because its padding spans two elements, the root routes while the input +- **Token granularity follows the component's spacing structure; split only when + the impl forces it.** Button sets all sides together via one shorthand on one + element → one `pad` var (even though block 6 ≠ inline 8 — differing values alone + don't force a split). OutlinedInput *is* forced: block vs inline land on + different elements/states and zero per adornment, and block is sized while the + `14px` inline gutter is a size-invariant **base token** (`--OutlinedInput-padInline`). + Its padding spans two elements, so the root routes block while the input consumes by inheritance. - **Cross-component coordination respects dependency direction.** The outlined floating label must track the input's `padBlock`, but `InputLabel` is generic @@ -132,8 +144,10 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). var** (layer-1 surface) and **sized tokens** (the design-system knob). - "spacing scale" (earlier draft, tier-1) — renamed **density scale** and moved to `theme.density`, to disambiguate from `theme.spacing`. -- Base (all-sizes) token — dropped; resolution is **sized-only**, so a designer - tunes per size. -- Var key — `pad` shorthand (single `padding`), not logical `paddingInline`/ - `paddingBlock`. Button padding is horizontally symmetric, so the physical - shorthand stays RTL-safe. +- Base (all-sizes-over-sized) token — dropped for **size-varying** axes; + resolution is sized-only, tune per size. A **size-invariant** axis (e.g. + OutlinedInput inline gutter) is the one place a plain base token applies. +- Var key — single `pad` shorthand only when the impl sets all sides together on + one element (Button); split per axis (`padBlock`/`padInline`) when forced — + axes on different elements/states or different shapes (OutlinedInput). Sides are + symmetric within an axis, so `padding: ` stays RTL-safe. diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 68199238e5ad98..fce1727af4697a 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -54,8 +54,12 @@ Per property, the chain (inline padding on Button): default, **set in `variants`** per `(variant, size)`, with a universal default on the root so a custom variant/size still renders a sane value. -Resolution is **sized-only** (no all-sizes base token): the sized token wins, -else the Material default. +Resolution for a **size-varying** axis is **sized-only** (no all-sizes base +token): the sized token wins, else the Material default. A **size-invariant** +axis (e.g. OutlinedInput's inline gutter) skips the size layer entirely and uses +a plain **base token** `--Component-` over an internal default `--_`, +consumed `var(--Component-, var(--_))` — same consume shape as a sized +axis, just with no size layer/routing. The styled root has **one** consumption point per property and **no conditional**; the defaults and built-in-size routing are plain `variants` entries: @@ -134,11 +138,17 @@ InputBase/TextField to follow) for this experiment. Same three-tier model, with two component-driven differences: -- **Block-only.** Input "density" is vertical: Material's own `small` changes - only the block padding (`16.5px` → `8.5px`); the `14px` inline gutter is - constant, so only `--OutlinedInput-padBlock` is tokenized. The gutter stays a - literal. (Filled/Standard have asymmetric block padding — `4/5`, `25/8` — so a - shared InputBase block seam would need a richer, per-side shape; deferred.) +- **Block is sized; inline is a base token.** Block (`16.5px`→`8.5px`) is the + density axis → sized token `--OutlinedInput--padBlock`. The `14px` inline + gutter is constant across sizes → a **base token** `--OutlinedInput-padInline` + (the experiment's first): same `var(--seam, var(--_padInline))` consume shape, + just **no size layer/routing** — the seam is the public knob, `--_padInline` the + internal default. Consumed on each spot (input sides, root adornment side, + multiline). Block and inline are split because the impl + applies them separately — different elements/states, per-adornment side-zeroing, + and different token shapes (sized vs base). (Filled/Standard have asymmetric + block padding — `4/5`, `25/8` — so a shared InputBase block seam would need a + richer, per-side shape; deferred.) - **Two consuming elements via inheritance.** Padding lives on the input (non-multiline) *and* the root (multiline). The root owns the size routing and `--_padBlock`; the **input (a child) consumes the resolved diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index 15960dcca036f7..df6a6a3473d89a 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -19,8 +19,13 @@ Root consumes seam, seam falls to internal default: padding: 'var(--Button-pad, var(--_pad))' ``` -Resolution = **sized-only**. Sized token wins -> else internal default. No -all-sizes base token. +Resolution = **sized-only** for a size-varying axis. Sized token wins -> else +internal default. No all-sizes-over-sized base token. + +**Size-invariant axis** (value same every size, e.g. OutlinedInput `14px` inline +gutter) -> skip the **size layer** only; keep internal default `--_`. Base +token `--Component-`, consumed `var(--Component-, var(--_))` (same +shape as sized, no routing). Seam = knob (nothing routes it). ## Recipe A — small component (Button) @@ -53,9 +58,11 @@ One element. `pad` shorthand (all sides move together). Padding spans 2 elements (root when multiline, input otherwise) + paired sibling (InputLabel). More dimensions but token model is *simpler*. -**Pick real axis.** Input density = vertical only. Material's own `small` only -changes block (`16.5 -> 8.5`); `14px` inline gutter constant -> stays literal. -Tokenize `padBlock`, not `pad`. +**Pick real axis + shape.** Block (`16.5 -> 8.5`) varies by size -> **sized** +`padBlock`. Inline `14px` constant across sizes -> **base** `padInline` +(`var(--OutlinedInput-padInline, var(--_padInline))`, `--_padInline: 14px`, no +size layer). Split forced: +axes land on different elements/states, zero per adornment, different shapes. **Two elements, one source via inheritance.** Root owns routing + `--_padBlock`. Input is child -> consumes resolved `--OutlinedInput-padBlock` by inheritance. No @@ -95,10 +102,26 @@ One knob (`--OutlinedInput--padBlock`) -> input box + label move together. ## Gotchas +- **Split axes only when the impl forces it.** Differing values per side is NOT + enough. If all sides are set together via one shorthand on one element, keep one + key — Button uses `pad` even though block 6 ≠ inline 8. Split per axis + (`padBlock`/`padInline`) only when the impl applies them separately: different + elements/states (OutlinedInput block on input vs root-multiline), independent + side-zeroing (adornments), or different token shapes (sized block + base inline). + OutlinedInput is forced; Button is not. Don't over-tokenize. - **Two vars, not one.** Cells write value (`--_pad`), routing writes reference (`--Button-pad`). One var fails 3 ways: inline bridge self-references (invalid CSS) -> literal leaks to runtime; `(variant×size)` vs size-only writes clobber on one element; lose the seam. +- **Uniform consume shape — every axis.** Always `var(--Component-, + var(--_))`, including a size-invariant **base** axis. Two real mistakes to + avoid: (a) **bare literal default** for a base axis (`var(--seam, 14px)`) — + instead define `--_` (e.g. `--_padInline: 14px`) so the default lives in + one place and the shape matches sized axes; (b) **dropping a fallback because + the seam "is always set"** — keep it; the uniform shape is the contract (Button + `var(--Button-pad, var(--_pad))`; a sized axis carries `--_` in *both* the + routing and the consume — that double-reference is intentional). Consistency + over minimalism. - **Unprefixed `--_` safe only if every instance sets own.** Custom prop inherits. Co-located setter (Button) or every root re-sets (OutlinedInput) -> ancestor value never wins. Else prefix it. @@ -108,6 +131,11 @@ One knob (`--OutlinedInput--padBlock`) -> input box + label move together. - **One element can't see another's internal var.** Label can't read input-root `--_padBlock`. Reference **public** token (visible at `:root`) + literal fallback. Never the internal var across elements. +- **Inline padding = outer gutter, not the adornment↔input gap.** The inline + token (`padInline`) is the border→first/last content inset (border→adornment + when adorned). The adornment↔text gap is the adornment's own margin + (`InputAdornment` marginRight/Left, ~8px), separate and untouched. Don't read + the gutter as the gap, and don't expect tokenizing it to move that gap. - **Check if component defaults `size`.** Most components destructure a default (Button: `size = 'medium'`) -> `ownerState.size` always valid -> `{ size: medium }` variant matches, fine. But context-driven ones (InputBase/OutlinedInput read @@ -139,8 +167,9 @@ Screenshot harness `scripts/density-screenshots/` (`maxDiffPixels: 0`): - Public seam/token: `--Component-` / `--Component--`. PascalCase component, short semantic key (`pad`, `gap`, `padBlock`). Matches `--AppBar-background`. - Internal: `--_` (leading underscore, no prefix). -- Key granularity = component's real spacing structure. Tune-all-sides -> - shorthand. One axis -> that axis only. +- Key granularity = component's real spacing structure. One shorthand key + (`pad`) when sides set together on one element; split per axis only when forced + (see gotcha). Per axis: sized if size-varying, base if constant. ## Order to roll out diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index 9f8b758e53c8c3..2a3af962acfc39 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -50,9 +50,11 @@ const OutlinedInputRoot = styled(InputBaseRoot, { // Agnostic seam: the input (child) inherits the size-resolved // --OutlinedInput-padBlock; the root consumes it only when multiline. // --_padBlock is the medium default, specialized by the size variant. - // See docs/adr/0001. Block (vertical) is the density axis; the 14px inline - // gutter is constant, so it stays a literal. + // See docs/adr/0001. Each axis has an internal default `--_` consumed + // as `var(--seam, var(--_))`. Block (vertical) is sized — its seam is + // routed per size; inline is a base token — its seam is just the public knob. '--_padBlock': '16.5px', + '--_padInline': '14px', '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', // The outlined label centers on the input's block padding. It's a preceding // sibling, so reach it via :has and feed it the public token + the label's @@ -114,20 +116,20 @@ const OutlinedInputRoot = styled(InputBaseRoot, { { props: ({ ownerState }) => ownerState.startAdornment, style: { - paddingLeft: 14, + paddingLeft: 'var(--OutlinedInput-padInline, var(--_padInline))', }, }, { props: ({ ownerState }) => ownerState.endAdornment, style: { - paddingRight: 14, + paddingRight: 'var(--OutlinedInput-padInline, var(--_padInline))', }, }, { props: ({ ownerState }) => ownerState.multiline, style: { // Block from the size-resolved var (small + multiline → 8.5px). - padding: 'var(--OutlinedInput-padBlock) 14px', + padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', }, }, ], @@ -156,9 +158,10 @@ const OutlinedInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - // Inherits the size-resolved --OutlinedInput-padBlock from the root; the 14px - // inline gutter is constant (not a density axis). - padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) 14px', + // Both axes: `var(--seam, var(--_internal))`. padBlock is size-resolved on the + // root (routed) and inherited; padInline is a base token (no routing). The + // internal defaults `--_padBlock`/`--_padInline` are inherited from the root. + padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', '&:-webkit-autofill': { ...(!theme.vars && { WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', From 5bb9e6e802abad0fc3d8b5e2d6f65ee72eb7c110 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 7 Jun 2026 18:26:19 +0700 Subject: [PATCH 009/114] [docs] Add OutlinedInput padInline to density-screenshot fixture scopes --- docs/pages/experiments/density-fixture.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 47d428c05d9018..7f8487c38e51bc 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -94,10 +94,12 @@ const scopes: Record> = { dense: { ['--OutlinedInput-small-padBlock' as any]: '4px', ['--OutlinedInput-medium-padBlock' as any]: '10px', + ['--OutlinedInput-padInline' as any]: '8px', }, loose: { ['--OutlinedInput-small-padBlock' as any]: '14px', ['--OutlinedInput-medium-padBlock' as any]: '28px', + ['--OutlinedInput-padInline' as any]: '24px', }, }, }; From cce0ad0c0b4c5d9e0587931764b29fc71986ed9c Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 7 Jun 2026 23:33:50 +0700 Subject: [PATCH 010/114] [material-ui] OutlinedInput: tokenize padding in place, size both axes Revert the lift of block padding to the root + inheritance; tokenize each literal where master has it (input owns inline/non-multiline, root owns multiline/adornment gutters) for the smallest diff. Promote padInline from a base token to a sized axis: default 14px both sizes, but expose --OutlinedInput--padInline so a design system can tune inline density per size. Both axes now routed per size in place; label :has derives --InputLabel-y straight from the public sized token. Docs: base token reserved for axes where per-size override is meaningless; a size-invariant default alone no longer justifies it. --- CONTEXT.md | 43 ++++++---- docs/adr/0001-css-var-density-adapter.md | 62 +++++++------- docs/adr/density-adapter-rollout.md | 59 ++++++++----- .../src/OutlinedInput/OutlinedInput.js | 82 +++++++++++++------ 4 files changed, 153 insertions(+), 93 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 3c15ebf36c561f..e485999c968e3a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -43,24 +43,26 @@ layer routes it over the internal default; when set at any scope it wins. For a **size-varying** axis, resolution is **sized-only** — no all-sizes base token. _Avoid_: "size variant token". -**Base token** (public, size-invariant axes only): -When an axis does **not** vary by size (e.g. OutlinedInput's `14px` inline -gutter), skip the **size layer** but keep the same shape: internal default -`--_` + seam, consumed `var(--Component-, var(--_))` -(`var(--OutlinedInput-padInline, var(--_padInline))`, `--_padInline: 14px`). The -seam *is* the knob — nothing routes it, so a designer sets it directly. Use only -when the value is genuinely constant across sizes; otherwise use a **sized token**. -_Avoid_: a base token for a size-varying axis; a bare literal default (use `--_`). +**Base token** (public, only when per-size override is meaningless): +An axis can skip the **size layer** — internal default `--_` + seam, +consumed `var(--Component-, var(--_))`, nothing routes it, so a designer +sets the seam directly. Use this **only when tuning the axis per size makes no +sense**, because a base token can't be size-scoped from the theme. A +size-invariant *default* is **not** enough: OutlinedInput's inline gutter is +`14px` for both sizes, yet it's a **sized token** (`--OutlinedInput--padInline`, +default `14px` each) so a design system can make small inputs denser inline. +Reach for a base token rarely; default to a **sized token**. +_Avoid_: a base token just because the default is size-invariant; a bare literal +default (use `--_`). **Internal default**: A private variable, shape `--_` (leading underscore, **no component prefix**), **set in `variants`** per `(variant, size)` cell (medium defaults reuse the `{ variant }` blocks), over a **universal default on the root** so a custom variant/size still renders. It holds the Material default — today's exact -px for that cell. No prefix is needed because the consumer either reads it on the -same element (Button) or is a descendant that re-sets its own (OutlinedInput's -input inherits from the root, but every root re-declares it) — so an ancestor's -value never wins over a component's own. Lowest priority, so any sized token or +px for that cell. No prefix is needed because every cell that reads it also +sets it on the same element (Button; OutlinedInput's input and root each declare +their own) — so an ancestor's value never wins over a component's own. Lowest priority, so any sized token or plain `styleOverrides` property still wins. _Avoid_: exposing it as API, prefixing with the component name, "private token". @@ -109,10 +111,13 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). the impl forces it.** Button sets all sides together via one shorthand on one element → one `pad` var (even though block 6 ≠ inline 8 — differing values alone don't force a split). OutlinedInput *is* forced: block vs inline land on - different elements/states and zero per adornment, and block is sized while the - `14px` inline gutter is a size-invariant **base token** (`--OutlinedInput-padInline`). - Its padding spans two elements, so the root routes block while the input - consumes by inheritance. + different elements/states and zero per adornment. **Both axes are sized** + (`--OutlinedInput--padBlock`/`-padInline`) — block defaults vary by size + (16.5/8.5), inline defaults don't (14 both) but it's sized anyway so density can + tune it per size. Its padding spans two elements (input when inline, root when + multiline/adorned — never both on a side at once), so each site tokenizes its + own literal in place rather than lifting size resolution to one owner; smallest + diff from master. - **Cross-component coordination respects dependency direction.** The outlined floating label must track the input's `padBlock`, but `InputLabel` is generic (shared by all input variants) so it only exposes a seam (`--InputLabel-y`, @@ -145,8 +150,10 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). - "spacing scale" (earlier draft, tier-1) — renamed **density scale** and moved to `theme.density`, to disambiguate from `theme.spacing`. - Base (all-sizes-over-sized) token — dropped for **size-varying** axes; - resolution is sized-only, tune per size. A **size-invariant** axis (e.g. - OutlinedInput inline gutter) is the one place a plain base token applies. + resolution is sized-only, tune per size. A base token applies only when per-size + override is meaningless — *not* merely when the default is size-invariant + (OutlinedInput's `14px` inline gutter is still a **sized** token so density can + tune it per size). - Var key — single `pad` shorthand only when the impl sets all sides together on one element (Button); split per axis (`padBlock`/`padInline`) when forced — axes on different elements/states or different shapes (OutlinedInput). Sides are diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index fce1727af4697a..0483255e4a5d23 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -55,11 +55,12 @@ Per property, the chain (inline padding on Button): on the root so a custom variant/size still renders a sane value. Resolution for a **size-varying** axis is **sized-only** (no all-sizes base -token): the sized token wins, else the Material default. A **size-invariant** -axis (e.g. OutlinedInput's inline gutter) skips the size layer entirely and uses -a plain **base token** `--Component-` over an internal default `--_`, -consumed `var(--Component-, var(--_))` — same consume shape as a sized -axis, just with no size layer/routing. +token): the sized token wins, else the Material default. An axis whose default is +the same every size can still be **sized** — and usually should be, so a design +system can tune it per size (density). A plain **base token** `--Component-` +over an internal default `--_` (consumed `var(--Component-, var(--_))`, +no size layer/routing) is reserved for the rare axis where per-size override is +genuinely meaningless. The styled root has **one** consumption point per property and **no conditional**; the defaults and built-in-size routing are plain `variants` entries: @@ -138,28 +139,31 @@ InputBase/TextField to follow) for this experiment. Same three-tier model, with two component-driven differences: -- **Block is sized; inline is a base token.** Block (`16.5px`→`8.5px`) is the - density axis → sized token `--OutlinedInput--padBlock`. The `14px` inline - gutter is constant across sizes → a **base token** `--OutlinedInput-padInline` - (the experiment's first): same `var(--seam, var(--_padInline))` consume shape, - just **no size layer/routing** — the seam is the public knob, `--_padInline` the - internal default. Consumed on each spot (input sides, root adornment side, - multiline). Block and inline are split because the impl - applies them separately — different elements/states, per-adornment side-zeroing, - and different token shapes (sized vs base). (Filled/Standard have asymmetric +- **Both axes are sized.** Block (`16.5px`→`8.5px`) varies by size → sized token + `--OutlinedInput--padBlock`. The `14px` inline gutter is *constant* across + sizes, but it's **sized too** → `--OutlinedInput--padInline` (default + `14px` each size) so a design system can make small inputs denser inline. (We + first modeled inline as a single size-invariant **base token**, but that can't + be size-scoped from the theme — a flaw for density — so we promoted it; a base + token is now reserved for axes where per-size override is meaningless.) Block and + inline are still split because the impl applies them separately — different + elements/states, per-adornment side-zeroing. Each axis is routed per size **in + place** on the element/variant that consumes it (input + root cells), so sizing + inline adds a `&& size === 'small'` re-route beside each size-agnostic adornment + variant — no lift, both axes wired identically. (Filled/Standard have asymmetric block padding — `4/5`, `25/8` — so a shared InputBase block seam would need a richer, per-side shape; deferred.) -- **Two consuming elements via inheritance.** Padding lives on the input - (non-multiline) *and* the root (multiline). The root owns the size routing and - `--_padBlock`; the **input (a child) consumes the resolved - `--OutlinedInput-padBlock` by inheritance** — single source, no duplicated size - logic. This diverges from Button's "reader co-located with setter": here the - reader is a descendant. Unprefixed `--_padBlock` stays safe because every - `OutlinedInputRoot` re-sets its own value, shadowing any inherited one. - -Because the block var is size-resolved before the multiline/input rules read it, -the previous `multiline && size==='small'` and input `size: 'small'` variants -become redundant and are dropped (identical pixels, fewer rules). +- **Two consuming elements, tokenized in place.** Padding lives on the input + (non-multiline) *and* the root (multiline) — and the two never both apply block + padding at once (multiline zeroes the input's). Rather than lift size resolution + to a single owner, **each site keeps master's literal-bearing cell and + tokenizes in place**: input base + input `{ size: small }`, root `multiline` + + root `{ multiline && small }`, each declaring its own `--_padBlock` and routing + the size token. This keeps the smallest diff from master (no restructuring, no + inheritance reliance, no dropped variants); the minor cost is the size routing + written twice (input vs root-multiline), which is honest — they are genuinely + separate code paths. Unprefixed `--_padBlock` stays safe because every cell that + reads it also sets it on the same element. **Closing the loop — the floating label.** In a `TextField`, `InputLabel` is a *preceding sibling* of the input. The resting label must track the block padding @@ -172,8 +176,9 @@ The bridge must respect the **dependency direction**: `InputLabel` is generic So `InputLabel` only exposes a seam — its outlined resting transform reads `var(--InputLabel-y, )` — and **OutlinedInput owns the bridge**. Because the label precedes the input, OutlinedInput reaches it with `:has` (sibling -combinators only match *following* siblings) and, per size, routes its public -token into the label scope and derives the seam: +combinators only match *following* siblings) and, per size, derives the label +seam straight from its public sized token (a cross-element rule must reference the +public token, not the input's internal `--_padBlock`): ```js // InputLabel — generic seam, literal default @@ -181,8 +186,7 @@ transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)' // small: 9px // OutlinedInputRoot — base (medium) + size:small variant [`.${inputLabelClasses.root}:has(~ &)`]: { - '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, 16.5px)', - '--InputLabel-y': 'calc(var(--OutlinedInput-padBlock) - 0.5px)', // small: + 0.5px, 8.5px + '--InputLabel-y': 'calc(var(--OutlinedInput-medium-padBlock, 16.5px) - 0.5px)', // small: small token + 0.5px = 9px }, ``` diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index df6a6a3473d89a..89293c169bf5e0 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -22,10 +22,14 @@ padding: 'var(--Button-pad, var(--_pad))' Resolution = **sized-only** for a size-varying axis. Sized token wins -> else internal default. No all-sizes-over-sized base token. -**Size-invariant axis** (value same every size, e.g. OutlinedInput `14px` inline -gutter) -> skip the **size layer** only; keep internal default `--_`. Base -token `--Component-`, consumed `var(--Component-, var(--_))` (same -shape as sized, no routing). Seam = knob (nothing routes it). +**Size-invariant default ≠ base token.** If an axis's default is the same every +size (e.g. OutlinedInput inline `14px`) you *can* skip the size layer — base +token `--Component-`, consumed `var(--Component-, var(--_))`, +nothing routes it. But only when per-size override is genuinely meaningless, +because a **base token can't be tuned per size from the theme**. If a design +system might want that axis denser on small (density!), **size it anyway**: same +default both sizes, but expose `--Component--`. OutlinedInput sizes +*both* padBlock and padInline for this reason (inline default `14px` each size). ## Recipe A — small component (Button) @@ -59,26 +63,34 @@ Padding spans 2 elements (root when multiline, input otherwise) + paired sibling (InputLabel). More dimensions but token model is *simpler*. **Pick real axis + shape.** Block (`16.5 -> 8.5`) varies by size -> **sized** -`padBlock`. Inline `14px` constant across sizes -> **base** `padInline` -(`var(--OutlinedInput-padInline, var(--_padInline))`, `--_padInline: 14px`, no -size layer). Split forced: -axes land on different elements/states, zero per adornment, different shapes. - -**Two elements, one source via inheritance.** Root owns routing + `--_padBlock`. -Input is child -> consumes resolved `--OutlinedInput-padBlock` by inheritance. No -duplicated size logic. +`padBlock`. Inline default is `14px` both sizes, but a design system may want +per-size inline density -> **size it too** (`padInline`, same `14px` default each +size). Both axes sized, routed per size. Split block/inline forced: they land on +different elements/states + zero per adornment. + +**Two elements, tokenize in place.** Padding lives on the input (non-multiline, +inline gutters) *and* the root (multiline, adornment gutters) — never both on the +same side at once (multiline zeroes input padding; an adorned side zeroes the +input and gutters from the root). Keep master's split: each site declares its own +`--_` + routes the size token, right where the literal was. No lift to a +single owner, no inheritance, no dropped variants — smallest diff from master. ```js -// root base +// input base + root multiline cell: '--_padBlock': '16.5px', +'--_padInline': '14px', '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', -// root { size: small } -> --_padBlock 8.5px + route to small token -// input: padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) 14px' -// root multiline: padding: 'var(--OutlinedInput-padBlock) 14px' +'--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', +padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', +// each size-small cell re-routes its axis to the small token: +// input { size: small } -> padBlock + padInline small (input owns both) +// root { multiline && small } -> padBlock + padInline small +// root { startAdornment/endAdornment && small } -> padInline small (gutter) ``` -Var carries size -> redundant `multiline && small` + input `size: small` -variants gone. Same pixels, fewer rules. +Cost of sizing inline in place: the size-agnostic adornment gutters need a small +re-route, so each adornment variant gains a `&& size === 'small'` sibling. Cheap +(one line each), and keeps both axes wired identically (no lift). **Paired sibling component (the label).** Generic component must not name specific component token. Label exposes own seam: @@ -88,13 +100,14 @@ specific component token. Label exposes own seam: transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)' // small: 9px ``` -Specific component owns bridge. Label = *preceding* sibling -> reach via `:has`: +Specific component owns bridge. Label = *preceding* sibling -> reach via `:has`. +Cross-element rule -> derive the label seam straight from the **public sized +token** + literal fallback (can't read the input's internal `--_padBlock`): ```js // OutlinedInputRoot, per size [`.${inputLabelClasses.root}:has(~ &)`]: { - '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, 16.5px)', - '--InputLabel-y': 'calc(var(--OutlinedInput-padBlock) - 0.5px)', // small: + 0.5px + '--InputLabel-y': 'calc(var(--OutlinedInput-medium-padBlock, 16.5px) - 0.5px)', // small: small token + 0.5px } ``` @@ -169,7 +182,9 @@ Screenshot harness `scripts/density-screenshots/` (`maxDiffPixels: 0`): - Internal: `--_` (leading underscore, no prefix). - Key granularity = component's real spacing structure. One shorthand key (`pad`) when sides set together on one element; split per axis only when forced - (see gotcha). Per axis: sized if size-varying, base if constant. + (see gotcha). Per axis: **sized by default** (per-size tunable); base token only + if per-size override is genuinely meaningless — a size-invariant *default* alone + doesn't justify base (size it so density can tune it per size). ## Order to roll out diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index 2a3af962acfc39..4a0a76b8c1c5bd 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -47,21 +47,17 @@ const OutlinedInputRoot = styled(InputBaseRoot, { const borderColor = theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; return { - // Agnostic seam: the input (child) inherits the size-resolved - // --OutlinedInput-padBlock; the root consumes it only when multiline. - // --_padBlock is the medium default, specialized by the size variant. - // See docs/adr/0001. Each axis has an internal default `--_` consumed - // as `var(--seam, var(--_))`. Block (vertical) is sized — its seam is - // routed per size; inline is a base token — its seam is just the public knob. - '--_padBlock': '16.5px', - '--_padInline': '14px', - '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', + // Density adapter (docs/adr/0001): each padding literal becomes + // `var(--seam, var(--_))`, tokenized in place. Both axes are sized — + // each seam routes the per-size public token (block + inline). The internal + // defaults live in the variants that consume them (below), like Button's + // `--_pad`. Inline default is 14px for both sizes; the per-size inline + // tokens let a design system tune it per size anyway. // The outlined label centers on the input's block padding. It's a preceding - // sibling, so reach it via :has and feed it the public token + the label's - // resting-Y seam. Medium default resolves to 16px (16.5 - 0.5 rounding). + // sibling, so reach it via :has and derive its resting-Y seam straight from + // the public sized token. Medium resolves to 16px (16.5 - 0.5 rounding). [`.${inputLabelClasses.root}:has(~ &)`]: { - '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, 16.5px)', - '--InputLabel-y': 'calc(var(--OutlinedInput-padBlock) - 0.5px)', + '--InputLabel-y': 'calc(var(--OutlinedInput-medium-padBlock, 16.5px) - 0.5px)', }, position: 'relative', borderRadius: (theme.vars || theme).shape.borderRadius, @@ -104,32 +100,57 @@ const OutlinedInputRoot = styled(InputBaseRoot, { { props: { size: 'small' }, style: { - '--_padBlock': '8.5px', - '--OutlinedInput-padBlock': 'var(--OutlinedInput-small-padBlock, var(--_padBlock))', - // Small default resolves to 9px (8.5 + 0.5). + // Small label resolves to 9px (8.5 + 0.5). [`.${inputLabelClasses.root}:has(~ &)`]: { - '--OutlinedInput-padBlock': 'var(--OutlinedInput-small-padBlock, 8.5px)', - '--InputLabel-y': 'calc(var(--OutlinedInput-padBlock) + 0.5px)', + '--InputLabel-y': 'calc(var(--OutlinedInput-small-padBlock, 8.5px) + 0.5px)', }, }, }, { props: ({ ownerState }) => ownerState.startAdornment, style: { + '--_padInline': '14px', + '--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', paddingLeft: 'var(--OutlinedInput-padInline, var(--_padInline))', }, }, + { + props: ({ ownerState, size }) => ownerState.startAdornment && size === 'small', + style: { + '--OutlinedInput-padInline': 'var(--OutlinedInput-small-padInline, var(--_padInline))', + }, + }, { props: ({ ownerState }) => ownerState.endAdornment, style: { + '--_padInline': '14px', + '--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', paddingRight: 'var(--OutlinedInput-padInline, var(--_padInline))', }, }, + { + props: ({ ownerState, size }) => ownerState.endAdornment && size === 'small', + style: { + '--OutlinedInput-padInline': 'var(--OutlinedInput-small-padInline, var(--_padInline))', + }, + }, { props: ({ ownerState }) => ownerState.multiline, style: { - // Block from the size-resolved var (small + multiline → 8.5px). - padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', + '--_padBlock': '16.5px', + '--_padInline': '14px', + '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', + '--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', + padding: + 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', + }, + }, + { + props: ({ ownerState, size }) => ownerState.multiline && size === 'small', + style: { + '--_padBlock': '8.5px', + '--OutlinedInput-padBlock': 'var(--OutlinedInput-small-padBlock, var(--_padBlock))', + '--OutlinedInput-padInline': 'var(--OutlinedInput-small-padInline, var(--_padInline))', }, }, ], @@ -158,10 +179,15 @@ const OutlinedInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - // Both axes: `var(--seam, var(--_internal))`. padBlock is size-resolved on the - // root (routed) and inherited; padInline is a base token (no routing). The - // internal defaults `--_padBlock`/`--_padInline` are inherited from the root. - padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', + // Both axes: `var(--seam, var(--_))`, both sized — each seam routes the + // per-size public token over the internal default, specialized by the size + // variant below. Defaults are the Material px (inline 14px both sizes). + '--_padBlock': '16.5px', + '--_padInline': '14px', + '--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', + '--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', + padding: + 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', '&:-webkit-autofill': { ...(!theme.vars && { WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', @@ -177,6 +203,14 @@ const OutlinedInputInput = styled(InputBaseInput, { })), }, variants: [ + { + props: { size: 'small' }, + style: { + '--_padBlock': '8.5px', + '--OutlinedInput-padBlock': 'var(--OutlinedInput-small-padBlock, var(--_padBlock))', + '--OutlinedInput-padInline': 'var(--OutlinedInput-small-padInline, var(--_padInline))', + }, + }, { props: ({ ownerState }) => ownerState.multiline, style: { From ac28ccae48747cf45e563415b09672d66e26ea56 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 7 Jun 2026 23:33:51 +0700 Subject: [PATCH 011/114] [docs] Density fixture: drive OutlinedInput inline via per-size tokens --- docs/pages/experiments/density-fixture.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 7f8487c38e51bc..a8ea767c7f4480 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -94,12 +94,14 @@ const scopes: Record> = { dense: { ['--OutlinedInput-small-padBlock' as any]: '4px', ['--OutlinedInput-medium-padBlock' as any]: '10px', - ['--OutlinedInput-padInline' as any]: '8px', + ['--OutlinedInput-small-padInline' as any]: '6px', + ['--OutlinedInput-medium-padInline' as any]: '8px', }, loose: { ['--OutlinedInput-small-padBlock' as any]: '14px', ['--OutlinedInput-medium-padBlock' as any]: '28px', - ['--OutlinedInput-padInline' as any]: '24px', + ['--OutlinedInput-small-padInline' as any]: '20px', + ['--OutlinedInput-medium-padInline' as any]: '24px', }, }, }; From bad674b29e44fb689b57c5eb4362bc283f3ad271 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 00:12:32 +0700 Subject: [PATCH 012/114] [material-ui] Roll out CSS-var density adapter to dashboard components Apply the density adapter (docs/adr/0001) to the @mui/material components used by the dashboard template: Chip, IconButton, MenuItem, ListItem, ListItemButton, ListItemIcon, ListItemText, ListSubheader, Toolbar, Tab, Tabs, TablePagination, CardContent, Select, Breadcrumbs, InputAdornment, Badge. Each exposes its real spacing axes as public sized tokens over literal-px internal defaults; the default render stays pixel-identical to master (density screenshot harness, maxDiffPixels:0). Checkbox/FormControl skipped - no density axis. enhanceDensity wires every component's sized tokens (incl. OutlinedInput) to the density scale. The verification fixture gains a matrix + dense/loose scope per component. Boolean `dense` components (MenuItem, ListItem, ListItemButton, ListItemText) expose the default state via the plain seam --Component- and only the dense override as --Component-dense-. Toolbar keeps theme.mixins.toolbar for its regular height (only dense + gutters tokenized). --- docs/pages/experiments/density-fixture.tsx | 698 ++++++++++++++++++ packages/mui-material/src/Badge/Badge.js | 20 +- .../src/Breadcrumbs/Breadcrumbs.js | 5 +- .../src/CardContent/CardContent.js | 6 +- packages/mui-material/src/Chip/Chip.js | 73 +- .../mui-material/src/IconButton/IconButton.js | 40 +- .../src/InputAdornment/InputAdornment.js | 20 +- .../mui-material/src/ListItem/ListItem.js | 24 +- .../src/ListItemButton/ListItemButton.js | 33 +- .../src/ListItemIcon/ListItemIcon.js | 3 +- .../src/ListItemText/ListItemText.js | 30 +- .../src/ListSubheader/ListSubheader.js | 13 +- .../mui-material/src/MenuItem/MenuItem.js | 39 +- .../mui-material/src/Select/SelectInput.js | 7 +- packages/mui-material/src/Tab/Tab.js | 31 +- packages/mui-material/src/Tab/Tab.test.js | 5 +- .../src/TablePagination/TablePagination.js | 17 +- packages/mui-material/src/Toolbar/Toolbar.js | 18 +- .../mui-material/src/styles/enhanceDensity.ts | 254 +++++++ 19 files changed, 1252 insertions(+), 84 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index a8ea767c7f4480..9247ff193e3c28 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -9,6 +9,38 @@ import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; +import Chip from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import ListSubheader from '@mui/material/ListSubheader'; +import Toolbar from '@mui/material/Toolbar'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TablePagination from '@mui/material/TablePagination'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import Select from '@mui/material/Select'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Badge from '@mui/material/Badge'; +import Typography from '@mui/material/Typography'; +import FaceIcon from '@mui/icons-material/Face'; +import DeleteIcon from '@mui/icons-material/Delete'; +import InboxIcon from '@mui/icons-material/Inbox'; +import DraftsIcon from '@mui/icons-material/Drafts'; +import FavoriteIcon from '@mui/icons-material/Favorite'; +import VisibilityIcon from '@mui/icons-material/Visibility'; import { createTheme, ThemeProvider } from '@mui/material/styles'; // Local verification fixture for the CSS-var density adapter (docs/adr/0001). @@ -73,6 +105,490 @@ const demos: Record = {
), + Chip: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + A} /> + } /> + {}} /> + {}} + icon={} + /> + + ))} + + ), + IconButton: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + MenuItem: ( + // MenuItem requires a MenuList/Menu ancestor (MenuListContext). + + Default item + Selected item + + + + + With icon + + With divider + No gutters + Dense item + + + + + Dense + icon + + + Dense no gutters + + + ), + ListItem: ( + + + Default item + + + + } + > + With secondary action + + Divider item + + + + + + Dense item + + + + } + > + Dense with action + + Dense, no gutters + + + ), + ListItemButton: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + ListItemIcon: ( + + + + + + + + + + + + + + + + + + + + + + + + + ), + ListItemText: ( + + {([false, true] as const).map((dense) => ( + + + + + + + + + + + + + + + ))} + + ), + ListSubheader: ( + + + Gutters (default) + + + Inset + + + Disable gutters + + + Primary color + + + ), + Toolbar: ( + + {(['regular', 'dense'] as const).map((variant) => ( + + + {variant} gutters + action + + + {variant} no-gutters + action + + + ))} + + ), + Tab: ( + // Tab requires a Tabs ancestor (RovingTabIndexContext). + + {}}> + + + + + {}}> + } label="Top" iconPosition="top" value={0} /> + } label="Bottom" iconPosition="bottom" value={1} /> + } label="Start" iconPosition="start" value={2} /> + } label="End" iconPosition="end" value={3} /> + + {}}> + } aria-label="icon only" value={0} /> + + + + ), + Tabs: ( + + + + + + + + } label="Top" iconPosition="top" /> + } label="Bottom" iconPosition="bottom" /> + } label="Start" iconPosition="start" /> + } label="End" iconPosition="end" /> + + + } aria-label="fav" /> + } aria-label="del" /> + + + ), + TablePagination: ( + + + + + {}} + onRowsPerPageChange={() => {}} + /> + + + {}} + onRowsPerPageChange={() => {}} + /> + + + {}} + onRowsPerPageChange={() => {}} + /> + + +
+
+ ), + CardContent: ( + + + + Default + All-sides padding via --CardContent-pad. + + + + + Last-child + + Extra bottom inset (--CardContent-padBottom) since this is the last child. + + + + + + Above actions + Not last child -> base pad only on the bottom. + + + + + + + ), + Select: ( + + + + + + + + ), + Breadcrumbs: ( + + + + Home + + + Catalog + + Shoes + + + + Home + + + Library + + + Data + + Reports + + + + Home + + + Catalog + + + Accessories + + Belts + + + ), + InputAdornment: ( + + {(['small', 'medium'] as const).map((size) => ( + + $ }, + }} + /> + + + + + + ), + }, + }} + /> + kg }, + }} + /> + kg }, + }} + /> + + ))} + + ), + Badge: ( + + + + + + + + + + + + + + + + + + ), }; // Per-component density-token overrides for the review levels. `default` is @@ -104,6 +620,188 @@ const scopes: Record> = { ['--OutlinedInput-medium-padInline' as any]: '24px', }, }, + Chip: { + dense: { + ['--Chip-small-height' as any]: '18px', + ['--Chip-medium-height' as any]: '24px', + ['--Chip-small-padInline' as any]: '4px', + ['--Chip-medium-padInline' as any]: '6px', + }, + loose: { + ['--Chip-small-height' as any]: '32px', + ['--Chip-medium-height' as any]: '44px', + ['--Chip-small-padInline' as any]: '14px', + ['--Chip-medium-padInline' as any]: '20px', + }, + }, + IconButton: { + dense: { + ['--IconButton-small-pad' as any]: '1px', + ['--IconButton-medium-pad' as any]: '3px', + ['--IconButton-large-pad' as any]: '6px', + }, + loose: { + ['--IconButton-small-pad' as any]: '10px', + ['--IconButton-medium-pad' as any]: '16px', + ['--IconButton-large-pad' as any]: '22px', + }, + }, + MenuItem: { + dense: { + ['--MenuItem-minHeight' as any]: '36px', + ['--MenuItem-dense-minHeight' as any]: '24px', + ['--MenuItem-padBlock' as any]: '2px', + ['--MenuItem-dense-padBlock' as any]: '1px', + ['--MenuItem-padInline' as any]: '8px', + ['--MenuItem-dense-padInline' as any]: '6px', + }, + loose: { + ['--MenuItem-minHeight' as any]: '64px', + ['--MenuItem-dense-minHeight' as any]: '48px', + ['--MenuItem-padBlock' as any]: '14px', + ['--MenuItem-dense-padBlock' as any]: '10px', + ['--MenuItem-padInline' as any]: '28px', + ['--MenuItem-dense-padInline' as any]: '24px', + }, + }, + ListItemButton: { + dense: { + ['--ListItemButton-padBlock' as any]: '2px', + ['--ListItemButton-dense-padBlock' as any]: '0px', + ['--ListItemButton-padInline' as any]: '8px', + ['--ListItemButton-dense-padInline' as any]: '4px', + }, + loose: { + ['--ListItemButton-padBlock' as any]: '16px', + ['--ListItemButton-dense-padBlock' as any]: '12px', + ['--ListItemButton-padInline' as any]: '32px', + ['--ListItemButton-dense-padInline' as any]: '24px', + }, + }, + ListItemIcon: { + dense: { ['--ListItemIcon-minWidth' as any]: '24px' }, + loose: { ['--ListItemIcon-minWidth' as any]: '56px' }, + }, + ListItemText: { + dense: { + ['--ListItemText-marginBlock' as any]: '1px', + ['--ListItemText-dense-marginBlock' as any]: '0px', + ['--ListItemText-insetPad' as any]: '32px', + ['--ListItemText-dense-insetPad' as any]: '24px', + }, + loose: { + ['--ListItemText-marginBlock' as any]: '12px', + ['--ListItemText-dense-marginBlock' as any]: '8px', + ['--ListItemText-insetPad' as any]: '72px', + ['--ListItemText-dense-insetPad' as any]: '64px', + }, + }, + ListSubheader: { + dense: { + ['--ListSubheader-height' as any]: '32px', + ['--ListSubheader-padInline' as any]: '8px', + ['--ListSubheader-inset' as any]: '48px', + }, + loose: { + ['--ListSubheader-height' as any]: '64px', + ['--ListSubheader-padInline' as any]: '28px', + ['--ListSubheader-inset' as any]: '96px', + }, + }, + Toolbar: { + dense: { + ['--Toolbar-dense-minHeight' as any]: '32px', + ['--Toolbar-padInline' as any]: '8px', + }, + loose: { + ['--Toolbar-dense-minHeight' as any]: '72px', + ['--Toolbar-padInline' as any]: '40px', + }, + }, + Tab: { + dense: { + ['--Tab-padBlock' as any]: '4px', + ['--Tab-padInline' as any]: '8px', + ['--Tab-minHeight' as any]: '32px', + ['--Tab-iconSpacing' as any]: '2px', + }, + loose: { + ['--Tab-padBlock' as any]: '20px', + ['--Tab-padInline' as any]: '28px', + ['--Tab-minHeight' as any]: '72px', + ['--Tab-iconSpacing' as any]: '14px', + }, + }, + Tabs: { + dense: { + ['--Tab-padBlock' as any]: '4px', + ['--Tab-padInline' as any]: '8px', + ['--Tab-minHeight' as any]: '32px', + ['--Tab-iconSpacing' as any]: '2px', + }, + loose: { + ['--Tab-padBlock' as any]: '20px', + ['--Tab-padInline' as any]: '32px', + ['--Tab-minHeight' as any]: '72px', + ['--Tab-iconSpacing' as any]: '14px', + }, + }, + TablePagination: { + dense: { + ['--TablePagination-minHeight' as any]: '36px', + ['--TablePagination-actionsSpacing' as any]: '8px', + ['--TablePagination-selectSpacing' as any]: '12px', + }, + loose: { + ['--TablePagination-minHeight' as any]: '72px', + ['--TablePagination-actionsSpacing' as any]: '40px', + ['--TablePagination-selectSpacing' as any]: '56px', + }, + }, + CardContent: { + dense: { + ['--CardContent-pad' as any]: '8px', + ['--CardContent-padBottom' as any]: '10px', + }, + loose: { + ['--CardContent-pad' as any]: '32px', + ['--CardContent-padBottom' as any]: '40px', + }, + }, + Select: { + dense: { ['--Select-minHeight' as any]: '0.8em' }, + loose: { ['--Select-minHeight' as any]: '2.4em' }, + }, + Breadcrumbs: { + dense: { ['--Breadcrumbs-separatorGap' as any]: '2px' }, + loose: { ['--Breadcrumbs-separatorGap' as any]: '20px' }, + }, + InputAdornment: { + dense: { + ['--InputAdornment-small-gap' as any]: '2px', + ['--InputAdornment-medium-gap' as any]: '3px', + ['--InputAdornment-small-marginTop' as any]: '6px', + ['--InputAdornment-medium-marginTop' as any]: '10px', + }, + loose: { + ['--InputAdornment-small-gap' as any]: '16px', + ['--InputAdornment-medium-gap' as any]: '24px', + ['--InputAdornment-small-marginTop' as any]: '24px', + ['--InputAdornment-medium-marginTop' as any]: '32px', + }, + }, + Badge: { + dense: { + ['--Badge-standard-pad' as any]: '0 3px', + ['--Badge-standard-size' as any]: '14px', + ['--Badge-dot-size' as any]: '5px', + }, + loose: { + ['--Badge-standard-pad' as any]: '0 10px', + ['--Badge-standard-size' as any]: '28px', + ['--Badge-dot-size' as any]: '12px', + }, + }, }; // TextField rides the same OutlinedInput tokens; OutlinedInput's `:has` rule // drives the label's --InputLabel-y, so input box + label move together. diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 6d6c4f1683fdf9..c1a5f5fa7f5e4a 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -79,10 +79,14 @@ const BadgeBadge = styled('span', { fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontWeightMedium, fontSize: theme.typography.pxToRem(12), - minWidth: RADIUS_STANDARD * 2, + '--_pad': '0 6px', + '--_size': `${RADIUS_STANDARD * 2}px`, + '--Badge-pad': 'var(--Badge-standard-pad, var(--_pad))', + '--Badge-size': 'var(--Badge-standard-size, var(--_size))', + minWidth: 'var(--Badge-size, var(--_size))', lineHeight: 1, - padding: '0 6px', - height: RADIUS_STANDARD * 2, + padding: 'var(--Badge-pad, var(--_pad))', + height: 'var(--Badge-size, var(--_size))', borderRadius: RADIUS_STANDARD, zIndex: 1, // Render the badge on top of potential ripples. '@media (forced-colors: active)': { @@ -105,10 +109,14 @@ const BadgeBadge = styled('span', { { props: { variant: 'dot' }, style: { + '--_pad': '0px', + '--_size': `${RADIUS_DOT * 2}px`, + '--Badge-pad': 'var(--Badge-dot-pad, var(--_pad))', + '--Badge-size': 'var(--Badge-dot-size, var(--_size))', borderRadius: RADIUS_DOT, - height: RADIUS_DOT * 2, - minWidth: RADIUS_DOT * 2, - padding: 0, + height: 'var(--Badge-size, var(--_size))', + minWidth: 'var(--Badge-size, var(--_size))', + padding: 'var(--Badge-pad, var(--_pad))', }, }, { diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js index 63cb6bae7f3538..91e2f297f9e326 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js @@ -49,10 +49,11 @@ const BreadcrumbsSeparator = styled('li', { name: 'MuiBreadcrumbs', slot: 'Separator', })({ + '--_separatorGap': '8px', display: 'flex', userSelect: 'none', - marginLeft: 8, - marginRight: 8, + marginLeft: 'var(--Breadcrumbs-separatorGap, var(--_separatorGap))', + marginRight: 'var(--Breadcrumbs-separatorGap, var(--_separatorGap))', }); function insertSeparators(items, className, separator, ownerState) { diff --git a/packages/mui-material/src/CardContent/CardContent.js b/packages/mui-material/src/CardContent/CardContent.js index 769163a78edbad..6e447fd831cfa2 100644 --- a/packages/mui-material/src/CardContent/CardContent.js +++ b/packages/mui-material/src/CardContent/CardContent.js @@ -21,9 +21,11 @@ const CardContentRoot = styled('div', { name: 'MuiCardContent', slot: 'Root', })({ - padding: 16, + '--_pad': '16px', + '--_padBottom': '24px', + padding: 'var(--CardContent-pad, var(--_pad))', '&:last-child': { - paddingBottom: 24, + paddingBottom: 'var(--CardContent-padBottom, var(--_padBottom))', }, }); diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index 9723bdecf42fa9..e477ec00fb6508 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -72,7 +72,11 @@ const ChipRoot = styled('div', { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - height: 32, + // Agnostic seam: the styled root reads `--Chip-height`; `--_height` is the + // universal default (today's medium height). Size variants route the public + // sized token over it. See docs/adr/0001. + '--_height': '32px', + height: 'var(--Chip-height, var(--_height))', lineHeight: 1.5, color: (theme.vars || theme).palette.text.primary, backgroundColor: (theme.vars || theme).palette.action.selected, @@ -115,6 +119,16 @@ const ChipRoot = styled('div', { }, }, variants: [ + // Built-in size routing (CSS, deduped) — exposes the public sized token + // over the internal default. Custom sizes are routed inline instead. + { + props: { size: 'small' }, + style: { '--Chip-height': 'var(--Chip-small-height, var(--_height))' }, + }, + { + props: { size: 'medium' }, + style: { '--Chip-height': 'var(--Chip-medium-height, var(--_height))' }, + }, { props: { color: 'primary', @@ -140,7 +154,7 @@ const ChipRoot = styled('div', { { props: { size: 'small' }, style: { - height: 24, + '--_height': '24px', // small default; medium default lives in base [`& .${chipClasses.avatar}`]: { marginLeft: 4, marginRight: -4, @@ -199,7 +213,9 @@ const ChipRoot = styled('div', { [`&.${chipClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.focusOpacity + }`, ), }, }, @@ -225,13 +241,17 @@ const ChipRoot = styled('div', { '&:hover': { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.hoverOpacity + }`, ), }, [`&.${chipClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.focusOpacity + }`, ), }, '&:active': { @@ -327,29 +347,40 @@ const ChipLabel = styled('span', { })({ overflow: 'hidden', textOverflow: 'ellipsis', - paddingLeft: 12, - paddingRight: 12, + // Agnostic seam: the label reads `--Chip-padInline` on both sides; `--_padInline` + // is the universal default (today's medium filled inline padding). Variants + // specialize the default per (variant, size) and size variants route the public + // sized token over it. See docs/adr/0001. + '--_padInline': '12px', + paddingLeft: 'var(--Chip-padInline, var(--_padInline))', + paddingRight: 'var(--Chip-padInline, var(--_padInline))', whiteSpace: 'nowrap', variants: [ + // Built-in size routing (CSS, deduped). Custom sizes are routed inline. + { + props: { size: 'small' }, + style: { '--Chip-padInline': 'var(--Chip-small-padInline, var(--_padInline))' }, + }, + { + props: { size: 'medium' }, + style: { '--Chip-padInline': 'var(--Chip-medium-padInline, var(--_padInline))' }, + }, { props: { variant: 'outlined' }, style: { - paddingLeft: 11, - paddingRight: 11, + '--_padInline': '11px', // medium outlined default }, }, { props: { size: 'small' }, style: { - paddingLeft: 8, - paddingRight: 8, + '--_padInline': '8px', // small filled default }, }, { props: { size: 'small', variant: 'outlined' }, style: { - paddingLeft: 7, - paddingRight: 7, + '--_padInline': '7px', // small outlined default }, }, ], @@ -441,6 +472,21 @@ const Chip = React.forwardRef(function Chip(inProps, ref) { const classes = useUtilityClasses(ownerState); + // Material UI layer: built-in sizes route the public sized tokens via variants + // (deduped CSS). A custom size has no such variant, so route it inline on the + // root — the tokens inherit down to the label. The `--_*` defaults live in the + // variants. See docs/adr/0001. + const densityVars = + size === 'small' || size === 'medium' + ? undefined + : { + '--Chip-height': `var(--Chip-${size}-height, var(--_height))`, + // padInline is consumed on the label (a child); the root doesn't own + // `--_padInline`, so a cross-element route must fall back to a literal, + // never the internal var (else a missing token resolves to invalid -> 0). + '--Chip-padInline': `var(--Chip-${size}-padInline, 12px)`, + }; + const moreProps = component === ButtonBase ? { @@ -508,6 +554,7 @@ const Chip = React.forwardRef(function Chip(inProps, ref) { disabled: clickable && disabled ? true : undefined, tabIndex: skipFocusWhenDisabled && disabled ? -1 : tabIndex, ...moreProps, + ...(densityVars && { style: densityVars }), }, getSlotProps: (handlers) => ({ ...handlers, diff --git a/packages/mui-material/src/IconButton/IconButton.js b/packages/mui-material/src/IconButton/IconButton.js index 9b459b7d22e5d1..9b1c4abb3bf31f 100644 --- a/packages/mui-material/src/IconButton/IconButton.js +++ b/packages/mui-material/src/IconButton/IconButton.js @@ -14,6 +14,9 @@ import CircularProgress from '../CircularProgress'; import capitalize from '../utils/capitalize'; import iconButtonClasses, { getIconButtonUtilityClass } from './iconButtonClasses'; +// Built-in sizes route padding via variants; any other size routes inline. +const iconButtonSizes = ['small', 'medium', 'large']; + const useUtilityClasses = (ownerState) => { const { classes, disabled, color, edge, size, loading } = ownerState; @@ -52,13 +55,31 @@ const IconButtonRoot = styled(ButtonBase, { textAlign: 'center', flex: '0 0 auto', fontSize: theme.typography.pxToRem(24), - padding: 8, + // Agnostic layer: the only spacing surface the styled root reads. `--_pad` + // is the universal default (today's medium padding); size variants specialize + // it, so a custom size still gets a sane value. See docs/adr/0001. + '--_pad': '8px', + padding: 'var(--IconButton-pad, var(--_pad))', borderRadius: '50%', color: (theme.vars || theme).palette.action.active, transition: theme.transitions.create('background-color', { duration: theme.transitions.duration.shortest, }), variants: [ + // Built-in size routing (CSS, deduped) — exposes the public sized token + // over the internal default. Custom sizes are routed inline instead. + { + props: { size: 'small' }, + style: { '--IconButton-pad': 'var(--IconButton-small-pad, var(--_pad))' }, + }, + { + props: { size: 'medium' }, + style: { '--IconButton-pad': 'var(--IconButton-medium-pad, var(--_pad))' }, + }, + { + props: { size: 'large' }, + style: { '--IconButton-pad': 'var(--IconButton-large-pad, var(--_pad))' }, + }, { props: (props) => !props.disableRipple, style: { @@ -124,14 +145,14 @@ const IconButtonRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - padding: 5, + '--_pad': '5px', fontSize: theme.typography.pxToRem(18), }, }, { props: { size: 'large' }, style: { - padding: 12, + '--_pad': '12px', fontSize: theme.typography.pxToRem(28), }, }, @@ -198,6 +219,14 @@ const IconButton = React.forwardRef(function IconButton(inProps, ref) { const classes = useUtilityClasses(ownerState); + // Material UI layer: built-in sizes route the public sized token via variants + // (deduped CSS). A custom size has no such variant, so route it inline — the + // token name carries the runtime size string, keeping custom sizes tunable for + // free. The `--_pad` default lives in the variants. See docs/adr/0001. + const densityVars = iconButtonSizes.includes(size) + ? undefined + : { '--IconButton-pad': `var(--IconButton-${size}-pad, var(--_pad))` }; + return ( {typeof loading === 'boolean' && ( @@ -327,6 +357,10 @@ IconButton.propTypes /* remove-proptypes */ = { PropTypes.oneOf(['small', 'medium', 'large']), PropTypes.string, ]), + /** + * @ignore + */ + style: PropTypes.object, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ diff --git a/packages/mui-material/src/InputAdornment/InputAdornment.js b/packages/mui-material/src/InputAdornment/InputAdornment.js index abb06efb23735f..d268c3e07a5388 100644 --- a/packages/mui-material/src/InputAdornment/InputAdornment.js +++ b/packages/mui-material/src/InputAdornment/InputAdornment.js @@ -50,7 +50,21 @@ const InputAdornmentRoot = styled('div', { alignItems: 'center', whiteSpace: 'nowrap', color: (theme.vars || theme).palette.action.active, + // Internal defaults (Material literals). `size` is read from FormControl with no + // default (can be undefined) -> medium routing lives in base, not a `{ size: 'medium' }` variant. + '--_gap': '8px', + '--_marginTop': '16px', + '--InputAdornment-gap': 'var(--InputAdornment-medium-gap, var(--_gap))', + '--InputAdornment-marginTop': 'var(--InputAdornment-medium-marginTop, var(--_marginTop))', variants: [ + // Built-in size routing — exposes the public sized token over the internal default. + { + props: { size: 'small' }, + style: { + '--InputAdornment-gap': 'var(--InputAdornment-small-gap, var(--_gap))', + '--InputAdornment-marginTop': 'var(--InputAdornment-small-marginTop, var(--_marginTop))', + }, + }, { props: { variant: 'filled', @@ -58,7 +72,7 @@ const InputAdornmentRoot = styled('div', { style: { [`&.${inputAdornmentClasses.positionStart}&:not(.${inputAdornmentClasses.hiddenLabel})`]: { - marginTop: 16, + marginTop: 'var(--InputAdornment-marginTop, var(--_marginTop))', }, }, }, @@ -67,7 +81,7 @@ const InputAdornmentRoot = styled('div', { position: 'start', }, style: { - marginRight: 8, + marginRight: 'var(--InputAdornment-gap, var(--_gap))', }, }, { @@ -75,7 +89,7 @@ const InputAdornmentRoot = styled('div', { position: 'end', }, style: { - marginLeft: 8, + marginLeft: 'var(--InputAdornment-gap, var(--_gap))', }, }, { diff --git a/packages/mui-material/src/ListItem/ListItem.js b/packages/mui-material/src/ListItem/ListItem.js index 72c51915c4ad67..172c6266351dcf 100644 --- a/packages/mui-material/src/ListItem/ListItem.js +++ b/packages/mui-material/src/ListItem/ListItem.js @@ -57,26 +57,38 @@ export const ListItemRoot = styled('div', { width: '100%', boxSizing: 'border-box', textAlign: 'left', + // Density adapter: `dense` is the compactness axis (boolean). Default state = + // plain seam `--ListItem-` over `--_`; the `dense` variant re-routes + // the seam to `--ListItem-dense-`. + '--_padBlock': '8px', + '--_padInline': '16px', variants: [ { props: ({ ownerState }) => !ownerState.disablePadding, style: { - paddingTop: 8, - paddingBottom: 8, + paddingTop: 'var(--ListItem-padBlock, var(--_padBlock))', + paddingBottom: 'var(--ListItem-padBlock, var(--_padBlock))', }, }, { props: ({ ownerState }) => !ownerState.disablePadding && ownerState.dense, style: { - paddingTop: 4, - paddingBottom: 4, + '--_padBlock': '4px', + '--ListItem-padBlock': 'var(--ListItem-dense-padBlock, var(--_padBlock))', }, }, { props: ({ ownerState }) => !ownerState.disablePadding && !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + paddingLeft: 'var(--ListItem-padInline, var(--_padInline))', + paddingRight: 'var(--ListItem-padInline, var(--_padInline))', + }, + }, + { + props: ({ ownerState }) => + !ownerState.disablePadding && !ownerState.disableGutters && ownerState.dense, + style: { + '--ListItem-padInline': 'var(--ListItem-dense-padInline, var(--_padInline))', }, }, { diff --git a/packages/mui-material/src/ListItemButton/ListItemButton.js b/packages/mui-material/src/ListItemButton/ListItemButton.js index b7681b7e52b2a9..1db047fb03b54d 100644 --- a/packages/mui-material/src/ListItemButton/ListItemButton.js +++ b/packages/mui-material/src/ListItemButton/ListItemButton.js @@ -64,8 +64,13 @@ const ListItemButtonRoot = styled(ButtonBase, { minWidth: 0, boxSizing: 'border-box', textAlign: 'left', - paddingTop: 8, - paddingBottom: 8, + // Density adapter: `dense` is the compactness axis (boolean). Default state = + // plain seam `--ListItemButton-` over `--_`; the `dense` variant + // re-routes the seam to `--ListItemButton-dense-`. + '--_padBlock': '8px', + '--_padInline': '16px', + paddingTop: 'var(--ListItemButton-padBlock, var(--_padBlock))', + paddingBottom: 'var(--ListItemButton-padBlock, var(--_padBlock))', transition: theme.transitions.create('background-color', { duration: theme.transitions.duration.shortest, }), @@ -85,14 +90,18 @@ const ListItemButtonRoot = styled(ButtonBase, { [`&.${listItemButtonClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.focusOpacity + }`, ), }, }, [`&.${listItemButtonClasses.selected}:hover`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.hoverOpacity + }`, ), // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { @@ -127,15 +136,23 @@ const ListItemButtonRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + // gutters owns the inline axis (default state = plain seam). + paddingLeft: 'var(--ListItemButton-padInline, var(--_padInline))', + paddingRight: 'var(--ListItemButton-padInline, var(--_padInline))', }, }, { props: ({ ownerState }) => ownerState.dense, style: { - paddingTop: 4, - paddingBottom: 4, + '--_padBlock': '4px', // dense default; routes the dense block token + '--ListItemButton-padBlock': 'var(--ListItemButton-dense-padBlock, var(--_padBlock))', + }, + }, + { + props: ({ ownerState }) => ownerState.dense && !ownerState.disableGutters, + style: { + // gutters + dense: re-route inline to the dense token (block already routed above). + '--ListItemButton-padInline': 'var(--ListItemButton-dense-padInline, var(--_padInline))', }, }, ], diff --git a/packages/mui-material/src/ListItemIcon/ListItemIcon.js b/packages/mui-material/src/ListItemIcon/ListItemIcon.js index 81aeb8629aede4..fd51b98c1f6353 100644 --- a/packages/mui-material/src/ListItemIcon/ListItemIcon.js +++ b/packages/mui-material/src/ListItemIcon/ListItemIcon.js @@ -29,7 +29,8 @@ const ListItemIconRoot = styled('div', { }, })( memoTheme(({ theme }) => ({ - minWidth: theme.spacing(4.5), + '--_minWidth': theme.spacing(4.5), + minWidth: 'var(--ListItemIcon-minWidth, var(--_minWidth))', color: (theme.vars || theme).palette.action.active, flexShrink: 0, display: 'inline-flex', diff --git a/packages/mui-material/src/ListItemText/ListItemText.js b/packages/mui-material/src/ListItemText/ListItemText.js index 8b81317e5d2955..954c99c9b78ee9 100644 --- a/packages/mui-material/src/ListItemText/ListItemText.js +++ b/packages/mui-material/src/ListItemText/ListItemText.js @@ -40,8 +40,16 @@ const ListItemTextRoot = styled('div', { })({ flex: '1 1 auto', minWidth: 0, - marginTop: 4, - marginBottom: 4, + // Density adapter (docs/adr/0001): each spacing literal becomes + // `var(--seam, var(--_))`, tokenized in place. The compactness dimension + // is `dense` (boolean) — default state = plain seam `--ListItemText-` over + // `--_`; the dense variant re-routes the seam to its own token. + // `marginBlock` (top+bottom move together) varies by `multiline`, so its literal + // default is set per (dense × multiline) cell while routing keys on dense only. + // `insetPad` is the inset indentation. + '--_marginBlock': '4px', + marginTop: 'var(--ListItemText-marginBlock, var(--_marginBlock))', + marginBottom: 'var(--ListItemText-marginBlock, var(--_marginBlock))', // Combine this and the below selector once https://github.com/emotion-js/emotion/issues/3366 is solved [`.${typographyClasses.root}:where(& .${listItemTextClasses.primary})`]: { display: 'block', @@ -50,17 +58,29 @@ const ListItemTextRoot = styled('div', { display: 'block', }, variants: [ + { + props: ({ ownerState }) => ownerState.dense, + style: { + '--ListItemText-marginBlock': 'var(--ListItemText-dense-marginBlock, var(--_marginBlock))', + }, + }, { props: ({ ownerState }) => ownerState.primary && ownerState.secondary, style: { - marginTop: 6, - marginBottom: 6, + '--_marginBlock': '6px', }, }, { props: ({ ownerState }) => ownerState.inset, style: { - paddingLeft: 56, + '--_insetPad': '56px', + paddingLeft: 'var(--ListItemText-insetPad, var(--_insetPad))', + }, + }, + { + props: ({ ownerState }) => ownerState.inset && ownerState.dense, + style: { + '--ListItemText-insetPad': 'var(--ListItemText-dense-insetPad, var(--_insetPad))', }, }, ], diff --git a/packages/mui-material/src/ListSubheader/ListSubheader.js b/packages/mui-material/src/ListSubheader/ListSubheader.js index 36bd3865891efe..cb80bf40e478bf 100644 --- a/packages/mui-material/src/ListSubheader/ListSubheader.js +++ b/packages/mui-material/src/ListSubheader/ListSubheader.js @@ -42,7 +42,12 @@ const ListSubheaderRoot = styled('li', { })( memoTheme(({ theme }) => ({ boxSizing: 'border-box', - lineHeight: '48px', + // Internal defaults (Material literals). Base tokens: ListSubheader has no + // size prop, so per-size tuning is meaningless — density tunes these directly. + '--_height': '48px', + '--_padInline': '16px', + '--_inset': '72px', + lineHeight: 'var(--ListSubheader-height, var(--_height))', listStyle: 'none', color: (theme.vars || theme).palette.text.secondary, fontFamily: theme.typography.fontFamily, @@ -68,14 +73,14 @@ const ListSubheaderRoot = styled('li', { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + paddingLeft: 'var(--ListSubheader-padInline, var(--_padInline))', + paddingRight: 'var(--ListSubheader-padInline, var(--_padInline))', }, }, { props: ({ ownerState }) => ownerState.inset, style: { - paddingLeft: 72, + paddingLeft: 'var(--ListSubheader-inset, var(--_inset))', }, }, { diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 68df44938b4404..3e526ebf204d93 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -61,14 +61,21 @@ const MenuItemRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => ({ ...theme.typography.body1, + // Density adapter (docs/adr/0001): `dense` is the compactness axis (boolean). + // The default (non-dense) state is the plain seam `--MenuItem-` over the + // internal default `--_`; the `dense` variant re-routes the seam to the + // `--MenuItem-dense-` token. Block + min-height live on the root + // unconditionally; inline gutters live on the !disableGutters variant. + '--_minHeight': '48px', + '--_padBlock': '6px', display: 'flex', justifyContent: 'flex-start', alignItems: 'center', position: 'relative', textDecoration: 'none', - minHeight: 48, - paddingTop: 6, - paddingBottom: 6, + minHeight: 'var(--MenuItem-minHeight, var(--_minHeight))', + paddingTop: 'var(--MenuItem-padBlock, var(--_padBlock))', + paddingBottom: 'var(--MenuItem-padBlock, var(--_padBlock))', boxSizing: 'border-box', whiteSpace: 'nowrap', '&:hover': { @@ -87,14 +94,18 @@ const MenuItemRoot = styled(ButtonBase, { [`&.${menuItemClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.focusOpacity + }`, ), }, }, [`&.${menuItemClasses.selected}:hover`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${ + (theme.vars || theme).palette.action.hoverOpacity + }`, ), // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { @@ -131,8 +142,15 @@ const MenuItemRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + '--_padInline': '16px', + paddingLeft: 'var(--MenuItem-padInline, var(--_padInline))', + paddingRight: 'var(--MenuItem-padInline, var(--_padInline))', + }, + }, + { + props: ({ ownerState }) => !ownerState.disableGutters && ownerState.dense, + style: { + '--MenuItem-padInline': 'var(--MenuItem-dense-padInline, var(--_padInline))', }, }, { @@ -153,9 +171,10 @@ const MenuItemRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.dense, style: { - minHeight: 32, // https://m2.material.io/components/menus#specs > Dense - paddingTop: 4, - paddingBottom: 4, + '--_minHeight': '32px', // https://m2.material.io/components/menus#specs > Dense + '--_padBlock': '4px', + '--MenuItem-minHeight': 'var(--MenuItem-dense-minHeight, var(--_minHeight))', + '--MenuItem-padBlock': 'var(--MenuItem-dense-padBlock, var(--_padBlock))', ...theme.typography.body2, [`& .${listItemIconClasses.root} svg`]: { fontSize: '1.25rem', diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index 2148d64fda9159..d695707645bac9 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -88,8 +88,13 @@ const SelectSelect = styled(StyledSelectSelect, { })({ // Win specificity over the input base [`&.${selectClasses.select}`]: { + // Density seam: base axis (size-invariant — keeps select content box matched + // to the input line box for text-field height consistency; per-size + // compactness comes from the input root padding). Consume shape stays uniform + // with sized axes: var(seam, var(internal default)). + '--_minHeight': '1.4375em', // Required for select\text-field height consistency height: 'auto', // Resets for multiple select with chips - minHeight: '1.4375em', // Required for select\text-field height consistency + minHeight: 'var(--Select-minHeight, var(--_minHeight))', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', diff --git a/packages/mui-material/src/Tab/Tab.js b/packages/mui-material/src/Tab/Tab.js index 05ddee7f3a0ba7..0522b119c94d27 100644 --- a/packages/mui-material/src/Tab/Tab.js +++ b/packages/mui-material/src/Tab/Tab.js @@ -54,9 +54,15 @@ const TabRoot = styled(ButtonBase, { maxWidth: 360, minWidth: 90, position: 'relative', - minHeight: 48, + // Density seams: Tab has no `size` prop, so each axis is a base token + // (`--Tab-`) over an internal default (`--_`). The labelIcon state + // owns its own block/min-height literals; everything else reads these. + '--_padBlock': '12px', + '--_padInline': '16px', + '--_minHeight': '48px', + minHeight: 'var(--Tab-minHeight, var(--_minHeight))', flexShrink: 0, - padding: '12px 16px', + padding: 'var(--Tab-padBlock, var(--_padBlock)) var(--Tab-padInline, var(--_padInline))', overflow: 'hidden', whiteSpace: 'normal', textAlign: 'center', @@ -82,9 +88,12 @@ const TabRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.icon && ownerState.label, style: { - minHeight: 72, - paddingTop: 9, - paddingBottom: 9, + // labelIcon owns its own compactness + block padding literals; inline + // padding stays from the base shorthand (unchanged at 16px). + '--_minHeight': '72px', + '--_padBlock': '9px', + paddingTop: 'var(--Tab-padBlock, var(--_padBlock))', + paddingBottom: 'var(--Tab-padBlock, var(--_padBlock))', }, }, { @@ -92,7 +101,8 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'top', style: { [`& > .${tabClasses.icon}`]: { - marginBottom: 6, + '--_iconSpacing': '6px', + marginBottom: 'var(--Tab-iconSpacing, var(--_iconSpacing))', }, }, }, @@ -101,7 +111,8 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'bottom', style: { [`& > .${tabClasses.icon}`]: { - marginTop: 6, + '--_iconSpacing': '6px', + marginTop: 'var(--Tab-iconSpacing, var(--_iconSpacing))', }, }, }, @@ -110,7 +121,8 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'start', style: { [`& > .${tabClasses.icon}`]: { - marginRight: theme.spacing(1), + '--_iconSpacing': theme.spacing(1), + marginRight: 'var(--Tab-iconSpacing, var(--_iconSpacing))', }, }, }, @@ -119,7 +131,8 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'end', style: { [`& > .${tabClasses.icon}`]: { - marginLeft: theme.spacing(1), + '--_iconSpacing': theme.spacing(1), + marginLeft: 'var(--Tab-iconSpacing, var(--_iconSpacing))', }, }, }, diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 46f5500ebfdc82..065baf03e2ea08 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -191,7 +191,10 @@ describe('', () => { expect(wrapper).to.have.class('test-icon'); }); - it('should have bottom margin when passed together with label', () => { + // The icon gap is now a CSS var (var(--Tab-iconSpacing, ...)); jsdom can't + // resolve var(), so assert in a real browser only. Default render is verified + // pixel-identical by the density screenshot harness. + it.skipIf(isJsdom())('should have bottom margin when passed together with label', () => { render( } label="foo" /> diff --git a/packages/mui-material/src/TablePagination/TablePagination.js b/packages/mui-material/src/TablePagination/TablePagination.js index 45c2b598ea838e..61b8cbc5285b27 100644 --- a/packages/mui-material/src/TablePagination/TablePagination.js +++ b/packages/mui-material/src/TablePagination/TablePagination.js @@ -45,18 +45,22 @@ const TablePaginationToolbar = styled(Toolbar, { }), })( memoTheme(({ theme }) => ({ - minHeight: 52, + // Base density seams: no `size` prop here, so each axis is a size-invariant + // base token. `--_` carries today's literal; the seam falls back to it. + '--_minHeight': '52px', + '--_actionsSpacing': '20px', + minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', paddingRight: 2, [`${theme.breakpoints.up('xs')} and (orientation: landscape)`]: { - minHeight: 52, + minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', }, [theme.breakpoints.up('sm')]: { - minHeight: 52, + minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', paddingRight: 2, }, [`& .${tablePaginationClasses.actions}`]: { flexShrink: 0, - marginLeft: 20, + marginLeft: 'var(--TablePagination-actionsSpacing, var(--_actionsSpacing))', }, })), ); @@ -91,7 +95,10 @@ const TablePaginationSelect = styled(Select, { color: 'inherit', fontSize: 'inherit', flexShrink: 0, - marginRight: 32, + // Base density seam for the select-to-rows gap. Co-located default keeps the + // unprefixed `--_selectSpacing` from inheriting a foreign value. + '--_selectSpacing': '32px', + marginRight: 'var(--TablePagination-selectSpacing, var(--_selectSpacing))', marginLeft: 8, [`& .${tablePaginationClasses.select}`]: { paddingLeft: 8, diff --git a/packages/mui-material/src/Toolbar/Toolbar.js b/packages/mui-material/src/Toolbar/Toolbar.js index dbfde2fb54eb23..46939ac77a5362 100644 --- a/packages/mui-material/src/Toolbar/Toolbar.js +++ b/packages/mui-material/src/Toolbar/Toolbar.js @@ -31,15 +31,21 @@ const ToolbarRoot = styled('div', { position: 'relative', display: 'flex', alignItems: 'center', + // Gutter default (the responsive sm bump is set in the gutters variant). + // Only the `dense` minHeight is tokenized; the `regular` height stays driven + // by the public `theme.mixins.toolbar` so existing customization keeps working. + '--_minHeight': '48px', + '--_padInline': theme.spacing(2), variants: [ { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), + // Gutters are shared across variants -> base token (no size layer), + // consumed directly with the internal default as fallback. + paddingLeft: 'var(--Toolbar-padInline, var(--_padInline))', + paddingRight: 'var(--Toolbar-padInline, var(--_padInline))', [theme.breakpoints.up('sm')]: { - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3), + '--_padInline': theme.spacing(3), }, }, }, @@ -48,7 +54,9 @@ const ToolbarRoot = styled('div', { variant: 'dense', }, style: { - minHeight: 48, + '--_minHeight': '48px', + '--Toolbar-minHeight': 'var(--Toolbar-dense-minHeight, var(--_minHeight))', + minHeight: 'var(--Toolbar-minHeight, var(--_minHeight))', }, }, { diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 0a7e0ce1973003..79e48639d5b3c9 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -118,6 +118,260 @@ export default function enhanceDensity< ], }, }, + MuiChip: { + ...c?.MuiChip, + styleOverrides: { + ...c?.MuiChip?.styleOverrides, + root: [ + c?.MuiChip?.styleOverrides?.root, + { + '--Chip-small-height': varRefs.lg, + '--Chip-medium-height': varRefs.xl, + '--Chip-small-padInline': varRefs.sm, + '--Chip-medium-padInline': varRefs.md, + }, + ], + }, + }, + MuiIconButton: { + ...c?.MuiIconButton, + styleOverrides: { + ...c?.MuiIconButton?.styleOverrides, + root: [ + c?.MuiIconButton?.styleOverrides?.root, + { + '--IconButton-small-pad': varRefs.xs, + '--IconButton-medium-pad': varRefs.sm, + '--IconButton-large-pad': varRefs.lg, + }, + ], + }, + }, + MuiMenuItem: { + ...c?.MuiMenuItem, + styleOverrides: { + ...c?.MuiMenuItem?.styleOverrides, + root: [ + c?.MuiMenuItem?.styleOverrides?.root, + { + '--MenuItem-minHeight': varRefs.xl, + '--MenuItem-dense-minHeight': varRefs.lg, + '--MenuItem-padBlock': varRefs.xs, + '--MenuItem-dense-padBlock': varRefs.xxs, + '--MenuItem-padInline': varRefs.lg, + '--MenuItem-dense-padInline': varRefs.md, + }, + ], + }, + }, + MuiListItem: { + ...c?.MuiListItem, + styleOverrides: { + ...c?.MuiListItem?.styleOverrides, + root: [ + c?.MuiListItem?.styleOverrides?.root, + { + '--ListItem-padBlock': varRefs.sm, + '--ListItem-dense-padBlock': varRefs.xxs, + '--ListItem-padInline': varRefs.lg, + '--ListItem-dense-padInline': varRefs.md, + }, + ], + }, + }, + MuiListItemButton: { + ...c?.MuiListItemButton, + styleOverrides: { + ...c?.MuiListItemButton?.styleOverrides, + root: [ + c?.MuiListItemButton?.styleOverrides?.root, + { + '--ListItemButton-padBlock': varRefs.sm, + '--ListItemButton-dense-padBlock': varRefs.xs, + '--ListItemButton-padInline': varRefs.lg, + '--ListItemButton-dense-padInline': varRefs.md, + }, + ], + }, + }, + MuiListItemIcon: { + ...c?.MuiListItemIcon, + styleOverrides: { + ...c?.MuiListItemIcon?.styleOverrides, + root: [ + c?.MuiListItemIcon?.styleOverrides?.root, + { + '--ListItemIcon-minWidth': `calc(36px + ${varRefs.md})`, + }, + ], + }, + }, + MuiListItemText: { + ...c?.MuiListItemText, + styleOverrides: { + ...c?.MuiListItemText?.styleOverrides, + root: [ + c?.MuiListItemText?.styleOverrides?.root, + { + // Sized-only: regular vs dense compactness each maps to its own step. + // marginBlock = vertical row spacing (smaller = denser); insetPad = + // indentation. + '--ListItemText-marginBlock': varRefs.xs, + '--ListItemText-dense-marginBlock': varRefs.xxs, + '--ListItemText-insetPad': `calc(${varRefs.xl} + ${varRefs.lg})`, + '--ListItemText-dense-insetPad': varRefs.xl, + }, + ], + }, + }, + MuiListSubheader: { + ...c?.MuiListSubheader, + styleOverrides: { + ...c?.MuiListSubheader?.styleOverrides, + root: [ + c?.MuiListSubheader?.styleOverrides?.root, + { + // Base tokens (no size layer): map the agnostic seams directly. + '--ListSubheader-height': varRefs.xl, + '--ListSubheader-padInline': varRefs.md, + '--ListSubheader-inset': `calc(${varRefs.xl} + ${varRefs.lg})`, + }, + ], + }, + }, + MuiToolbar: { + ...c?.MuiToolbar, + styleOverrides: { + ...c?.MuiToolbar?.styleOverrides, + root: [ + c?.MuiToolbar?.styleOverrides?.root, + { + // Only `dense` minHeight is tokenized (regular stays mixins.toolbar); + // gutter padInline is a base token. + '--Toolbar-dense-minHeight': varRefs.lg, + '--Toolbar-padInline': varRefs.md, + }, + ], + }, + }, + MuiTab: { + ...c?.MuiTab, + styleOverrides: { + ...c?.MuiTab?.styleOverrides, + root: [ + c?.MuiTab?.styleOverrides?.root, + { + // Base tokens: Tab has no size prop, so map the agnostic seams + // directly to density steps (no per-size tokens to route). + '--Tab-padBlock': varRefs.sm, + '--Tab-padInline': varRefs.lg, + '--Tab-minHeight': `calc(${varRefs.xl} + ${varRefs.lg})`, + '--Tab-iconSpacing': varRefs.xs, + }, + ], + }, + }, + MuiTablePagination: { + ...c?.MuiTablePagination, + styleOverrides: { + ...c?.MuiTablePagination?.styleOverrides, + root: [ + c?.MuiTablePagination?.styleOverrides?.root, + { + '--TablePagination-minHeight': `calc(${varRefs.xl} + ${varRefs.md})`, + '--TablePagination-actionsSpacing': varRefs.lg, + '--TablePagination-selectSpacing': varRefs.xl, + }, + ], + }, + }, + MuiCardContent: { + ...c?.MuiCardContent, + styleOverrides: { + ...c?.MuiCardContent?.styleOverrides, + root: [ + c?.MuiCardContent?.styleOverrides?.root, + { + // CardContent has no size prop -> base tokens (no per-size layer). + '--CardContent-pad': varRefs.lg, + '--CardContent-padBottom': varRefs.xl, + }, + ], + }, + }, + MuiSelect: { + ...c?.MuiSelect, + styleOverrides: { + ...c?.MuiSelect?.styleOverrides, + root: [ + c?.MuiSelect?.styleOverrides?.root, + { + // Base axis (no size layer) — single agnostic seam, mapped to a + // mid-step so density nudges the select content-box floor uniformly. + '--Select-minHeight': varRefs.lg, + }, + ], + }, + }, + MuiBreadcrumbs: { + ...c?.MuiBreadcrumbs, + styleOverrides: { + ...c?.MuiBreadcrumbs?.styleOverrides, + root: [ + c?.MuiBreadcrumbs?.styleOverrides?.root, + { + '--Breadcrumbs-separatorGap': varRefs.sm, + }, + ], + }, + }, + MuiInputAdornment: { + ...c?.MuiInputAdornment, + styleOverrides: { + ...c?.MuiInputAdornment?.styleOverrides, + root: [ + c?.MuiInputAdornment?.styleOverrides?.root, + { + '--InputAdornment-small-gap': varRefs.xxs, + '--InputAdornment-medium-gap': varRefs.sm, + '--InputAdornment-small-marginTop': varRefs.md, + '--InputAdornment-medium-marginTop': varRefs.lg, + }, + ], + }, + }, + MuiBadge: { + ...c?.MuiBadge, + styleOverrides: { + ...c?.MuiBadge?.styleOverrides, + root: [ + c?.MuiBadge?.styleOverrides?.root, + { + '--Badge-standard-pad': `0 ${varRefs.sm}`, + '--Badge-standard-size': varRefs.lg, + '--Badge-dot-pad': '0px', + '--Badge-dot-size': varRefs.xs, + }, + ], + }, + }, + MuiOutlinedInput: { + ...c?.MuiOutlinedInput, + styleOverrides: { + ...c?.MuiOutlinedInput?.styleOverrides, + root: [ + c?.MuiOutlinedInput?.styleOverrides?.root, + { + // Sized block/inline padding per size; block < inline to keep the + // input's 16.5/14 feel. + '--OutlinedInput-medium-padBlock': varRefs.md, + '--OutlinedInput-small-padBlock': varRefs.sm, + '--OutlinedInput-medium-padInline': varRefs.lg, + '--OutlinedInput-small-padInline': varRefs.md, + }, + ], + }, + }, }; return theme; From 40a893df4a333d51a71ed99083ce62b557ecde7a Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 08:01:48 +0700 Subject: [PATCH 013/114] [docs] Document the dense state-token pattern Boolean compactness toggles (dense) use a state token: default state is the plain seam --Component- (base-token-shaped, no base routing), only the on state is qualified --Component-dense-. No --Component-normal/regular/default- qualifier - a boolean has no name for off. Added to CONTEXT language, ADR 0001 resolution, and the rollout recipe + naming. --- CONTEXT.md | 24 +++++++++--- docs/adr/0001-css-var-density-adapter.md | 42 +++++++++++++-------- docs/adr/density-adapter-rollout.md | 47 +++++++++++++++++------- 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index e485999c968e3a..f1312c44701628 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -48,13 +48,27 @@ An axis can skip the **size layer** — internal default `--_` + seam, consumed `var(--Component-, var(--_))`, nothing routes it, so a designer sets the seam directly. Use this **only when tuning the axis per size makes no sense**, because a base token can't be size-scoped from the theme. A -size-invariant *default* is **not** enough: OutlinedInput's inline gutter is +size-invariant _default_ is **not** enough: OutlinedInput's inline gutter is `14px` for both sizes, yet it's a **sized token** (`--OutlinedInput--padInline`, default `14px` each) so a design system can make small inputs denser inline. Reach for a base token rarely; default to a **sized token**. _Avoid_: a base token just because the default is size-invariant; a bare literal default (use `--_`). +**State token** (public, for a boolean compactness toggle like `dense`): +When the compactness axis is a **boolean** prop (`dense`) rather than a `size` +enum, the **default (off) state** is exposed through the plain seam +`--Component-` — consumed `var(--Component-, var(--_))`, **nothing +routes it in the base** (the seam _is_ the default knob, base-token-shaped) — and +**only the on state** gets a qualified token `--Component-dense-`, routed in +the `dense` variant over its own `--_` literal. A boolean has no name for +"off", so there is **no** `--Component-normal/regular/default-`: the absence +of the toggle is the plain seam, not an arbitrarily-named size. Contrast a +**sized token**, where every value (including `medium`) is qualified because each +is a real named size. Used by MenuItem, ListItem, ListItemButton, ListItemText. +_Avoid_: naming the off state (`-normal-`, `-regular-`, `-default-`); routing the +seam in the base; treating `dense` as a 2-value size enum. + **Internal default**: A private variable, shape `--_` (leading underscore, **no component prefix**), **set in `variants`** per `(variant, size)` cell (medium defaults @@ -97,11 +111,11 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). matrix and the built-in-size routing are both **`variants` cells**, not a body lookup table; only custom-size routing is inline. - **Two vars, not one** (`--Button-pad` over `--_pad`): the cells write the - *value* (`--_pad`), the routing writes a *reference* (`--Button-pad`). One var + _value_ (`--_pad`), the routing writes a _reference_ (`--Button-pad`). One var fails three ways — a self-referencing fallback in the inline bridge (invalid CSS, forcing the literal back into runtime style), the `(variant×size)` and size-only write-axes clobbering on one element, and losing the agnostic seam. - Full reasoning in `docs/adr/0001` → *Why two vars*. + Full reasoning in `docs/adr/0001` → _Why two vars_. - Override priority (high → low): plain `styleOverrides` property → **sized token** → internal default (literal fallback). - Custom (user-defined) sizes work for free: when the size isn't built-in, the @@ -110,7 +124,7 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). - **Token granularity follows the component's spacing structure; split only when the impl forces it.** Button sets all sides together via one shorthand on one element → one `pad` var (even though block 6 ≠ inline 8 — differing values alone - don't force a split). OutlinedInput *is* forced: block vs inline land on + don't force a split). OutlinedInput _is_ forced: block vs inline land on different elements/states and zero per adornment. **Both axes are sized** (`--OutlinedInput--padBlock`/`-padInline`) — block defaults vary by size (16.5/8.5), inline defaults don't (14 both) but it's sized anyway so density can @@ -151,7 +165,7 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). to `theme.density`, to disambiguate from `theme.spacing`. - Base (all-sizes-over-sized) token — dropped for **size-varying** axes; resolution is sized-only, tune per size. A base token applies only when per-size - override is meaningless — *not* merely when the default is size-invariant + override is meaningless — _not_ merely when the default is size-invariant (OutlinedInput's `14px` inline gutter is still a **sized** token so density can tune it per size). - Var key — single `pad` shorthand only when the impl sets all sides together on diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 0483255e4a5d23..094d8f51bd3a58 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -62,6 +62,15 @@ over an internal default `--_` (consumed `var(--Component-, var(--_` (base-token-shaped — nothing routes it in the base), and only +the on state is qualified `--Component-dense-`, routed in the `dense` +variant. A boolean has no name for "off", so there is no +`--Component-normal/regular/default-` — unlike a size enum, where every value +(including `medium`) is qualified because each is a real named size. Used by +MenuItem, ListItem, ListItemButton, and ListItemText. + The styled root has **one** consumption point per property and **no conditional**; the defaults and built-in-size routing are plain `variants` entries: @@ -71,7 +80,10 @@ const ButtonRoot = styled(ButtonBase)({ padding: 'var(--Button-pad, var(--_pad))', // agnostic layer variants: [ // routing for built-in sizes (deduped CSS) - { props: { size: 'small' }, style: { '--Button-pad': 'var(--Button-small-pad, var(--_pad))' } }, + { + props: { size: 'small' }, + style: { '--Button-pad': 'var(--Button-small-pad, var(--_pad))' }, + }, // literal default per (variant, size); medium lives in the { variant } blocks (DRY) { props: { variant: 'text', size: 'small' }, style: { '--_pad': '4px 5px' } }, ], @@ -98,7 +110,7 @@ Three reasons, all pointing the same way: the rest of the variant's styling, statically deduped, smallest diff from today. Inline style is only a **bridge** for the one dynamic case (a custom size's token name) and must carry **no values**, only routing. Two vars allow - that: cells write the value (`--_pad`), the bridge writes a *reference* + that: cells write the value (`--_pad`), the bridge writes a _reference_ (`--Button-pad: var(--Button--pad, var(--_pad))`). With a single `--_pad`, the custom-size bridge would have to write `--_pad: var(--Button--pad, var(--_pad))` — a property referencing @@ -140,7 +152,7 @@ InputBase/TextField to follow) for this experiment. Same three-tier model, with two component-driven differences: - **Both axes are sized.** Block (`16.5px`→`8.5px`) varies by size → sized token - `--OutlinedInput--padBlock`. The `14px` inline gutter is *constant* across + `--OutlinedInput--padBlock`. The `14px` inline gutter is _constant_ across sizes, but it's **sized too** → `--OutlinedInput--padInline` (default `14px` each size) so a design system can make small inputs denser inline. (We first modeled inline as a single size-invariant **base token**, but that can't @@ -154,7 +166,7 @@ Same three-tier model, with two component-driven differences: block padding — `4/5`, `25/8` — so a shared InputBase block seam would need a richer, per-side shape; deferred.) - **Two consuming elements, tokenized in place.** Padding lives on the input - (non-multiline) *and* the root (multiline) — and the two never both apply block + (non-multiline) _and_ the root (multiline) — and the two never both apply block padding at once (multiline zeroes the input's). Rather than lift size resolution to a single owner, **each site keeps master's literal-bearing cell and tokenizes in place**: input base + input `{ size: small }`, root `multiline` + @@ -166,7 +178,7 @@ Same three-tier model, with two component-driven differences: reads it also sets it on the same element. **Closing the loop — the floating label.** In a `TextField`, `InputLabel` is a -*preceding sibling* of the input. The resting label must track the block padding +_preceding sibling_ of the input. The resting label must track the block padding or it decenters when density is tuned. True centering is `labelY = padBlock` exactly (`(lineHeight + 2·padBlock)/2 − lineHeight/2`); today's `16px`/`9px` are that with ±0.5px historical rounding. @@ -176,7 +188,7 @@ The bridge must respect the **dependency direction**: `InputLabel` is generic So `InputLabel` only exposes a seam — its outlined resting transform reads `var(--InputLabel-y, )` — and **OutlinedInput owns the bridge**. Because the label precedes the input, OutlinedInput reaches it with `:has` (sibling -combinators only match *following* siblings) and, per size, derives the label +combinators only match _following_ siblings) and, per size, derives the label seam straight from its public sized token (a cross-element rule must reference the public token, not the input's internal `--_padBlock`): @@ -221,12 +233,12 @@ keeps the coupling in the one component that legitimately owns it. Cost: needs ### Accepted trade-offs -| Trade-off | Why we can live with it | -| :-- | :-- | -| `--Button-pad` is public-shaped but not a designer knob in the assembled Button (plumbing) | It's the agnostic seam; the real knob is `--Button--pad`, documented. The name marks the layer boundary. | -| Two vars per property instead of one | Mandatory (see *Why two vars*); the indirection is mechanical and documented. | -| Unprefixed `--_pad` could inherit a foreign value | Every built-in cell plus the root universal default set it on the element; revisit a prefix only if cross-component collisions surface as the pattern spreads. | -| `pad` shorthand is coarse (an override sets all sides) | Button padding is symmetric; tiny token surface; granular logical props can come later. | -| `var()` unresolved in jsdom (no computed-px assertions) | Argos covers default visuals; the chain is declarative and inspectable. | -| Inline still present for custom sizes | Rare; the built-in common case carries zero inline. | -| Per-property boilerplate grows with rollout | Acceptable for the payoff (runtime scoped theming); extract a helper before component #3. | +| Trade-off | Why we can live with it | +| :----------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--Button-pad` is public-shaped but not a designer knob in the assembled Button (plumbing) | It's the agnostic seam; the real knob is `--Button--pad`, documented. The name marks the layer boundary. | +| Two vars per property instead of one | Mandatory (see _Why two vars_); the indirection is mechanical and documented. | +| Unprefixed `--_pad` could inherit a foreign value | Every built-in cell plus the root universal default set it on the element; revisit a prefix only if cross-component collisions surface as the pattern spreads. | +| `pad` shorthand is coarse (an override sets all sides) | Button padding is symmetric; tiny token surface; granular logical props can come later. | +| `var()` unresolved in jsdom (no computed-px assertions) | Argos covers default visuals; the chain is declarative and inspectable. | +| Inline still present for custom sizes | Rare; the built-in common case carries zero inline. | +| Per-property boilerplate grows with rollout | Acceptable for the payoff (runtime scoped theming); extract a helper before component #3. | diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index 89293c169bf5e0..e6f3d7d1cd34aa 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -16,20 +16,38 @@ pixel-identical to today. No `calc` math for users, no `--mui-spacing` dial. Root consumes seam, seam falls to internal default: ```js -padding: 'var(--Button-pad, var(--_pad))' +padding: 'var(--Button-pad, var(--_pad))'; ``` Resolution = **sized-only** for a size-varying axis. Sized token wins -> else internal default. No all-sizes-over-sized base token. **Size-invariant default ≠ base token.** If an axis's default is the same every -size (e.g. OutlinedInput inline `14px`) you *can* skip the size layer — base +size (e.g. OutlinedInput inline `14px`) you _can_ skip the size layer — base token `--Component-`, consumed `var(--Component-, var(--_))`, nothing routes it. But only when per-size override is genuinely meaningless, because a **base token can't be tuned per size from the theme**. If a design system might want that axis denser on small (density!), **size it anyway**: same default both sizes, but expose `--Component--`. OutlinedInput sizes -*both* padBlock and padInline for this reason (inline default `14px` each size). +_both_ padBlock and padInline for this reason (inline default `14px` each size). + +**Boolean `dense` axis (state token).** When compactness is a **boolean** prop +(`dense`) not a `size` enum, don't name the off-state. The **default state is the +plain seam** `--Component-` (base-token-shaped: nothing routes it in the +base; designer sets it directly); **only `dense` is qualified** +`--Component-dense-`, routed in the `dense` variant: + +```js +// base: default state = plain seam, falls to the internal default literal +'--_padBlock': '8px', +paddingTop: 'var(--ListItem-padBlock, var(--_padBlock))', +// { dense } variant: own literal + route the dense token +'--_padBlock': '4px', +'--ListItem-padBlock': 'var(--ListItem-dense-padBlock, var(--_padBlock))', +``` + +No `--Component-normal/regular/default-` — a boolean has no name for "off". +(MenuItem, ListItem, ListItemButton, ListItemText.) ## Recipe A — small component (Button) @@ -51,7 +69,7 @@ One element. `pad` shorthand (all sides move together). ``` 4. Custom size -> route inline (only non-built-in size emits a `style` attr). ```js - const densityVars = ['small','medium','large'].includes(size) + const densityVars = ['small', 'medium', 'large'].includes(size) ? undefined : { '--Button-pad': `var(--Button-${size}-pad, var(--_pad))` }; ``` @@ -60,7 +78,7 @@ One element. `pad` shorthand (all sides move together). ## Recipe B — big component (OutlinedInput) Padding spans 2 elements (root when multiline, input otherwise) + paired sibling -(InputLabel). More dimensions but token model is *simpler*. +(InputLabel). More dimensions but token model is _simpler_. **Pick real axis + shape.** Block (`16.5 -> 8.5`) varies by size -> **sized** `padBlock`. Inline default is `14px` both sizes, but a design system may want @@ -69,7 +87,7 @@ size). Both axes sized, routed per size. Split block/inline forced: they land on different elements/states + zero per adornment. **Two elements, tokenize in place.** Padding lives on the input (non-multiline, -inline gutters) *and* the root (multiline, adornment gutters) — never both on the +inline gutters) _and_ the root (multiline, adornment gutters) — never both on the same side at once (multiline zeroes input padding; an adorned side zeroes the input and gutters from the root). Keep master's split: each site declares its own `--_` + routes the size token, right where the literal was. No lift to a @@ -97,10 +115,10 @@ specific component token. Label exposes own seam: ```js // InputLabel — generic, literal default -transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)' // small: 9px +transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)'; // small: 9px ``` -Specific component owns bridge. Label = *preceding* sibling -> reach via `:has`. +Specific component owns bridge. Label = _preceding_ sibling -> reach via `:has`. Cross-element rule -> derive the label seam straight from the **public sized token** + literal fallback (can't read the input's internal `--_padBlock`): @@ -127,20 +145,20 @@ One knob (`--OutlinedInput--padBlock`) -> input box + label move together. CSS) -> literal leaks to runtime; `(variant×size)` vs size-only writes clobber on one element; lose the seam. - **Uniform consume shape — every axis.** Always `var(--Component-, - var(--_))`, including a size-invariant **base** axis. Two real mistakes to +var(--_))`, including a size-invariant **base** axis. Two real mistakes to avoid: (a) **bare literal default** for a base axis (`var(--seam, 14px)`) — instead define `--_` (e.g. `--_padInline: 14px`) so the default lives in one place and the shape matches sized axes; (b) **dropping a fallback because the seam "is always set"** — keep it; the uniform shape is the contract (Button - `var(--Button-pad, var(--_pad))`; a sized axis carries `--_` in *both* the + `var(--Button-pad, var(--_pad))`; a sized axis carries `--_` in _both_ the routing and the consume — that double-reference is intentional). Consistency over minimalism. - **Unprefixed `--_` safe only if every instance sets own.** Custom prop inherits. Co-located setter (Button) or every root re-sets (OutlinedInput) -> ancestor value never wins. Else prefix it. - **Sibling can't inherit.** Sibling vars need common ancestor. Specific - component reaches sibling via `:has(~ &)`. Note: `+`/`~` match *following* - siblings only -> `:has` makes the *earlier* element the subject. + component reaches sibling via `:has(~ &)`. Note: `+`/`~` match _following_ + siblings only -> `:has` makes the _earlier_ element the subject. - **One element can't see another's internal var.** Label can't read input-root `--_padBlock`. Reference **public** token (visible at `:root`) + literal fallback. Never the internal var across elements. @@ -183,8 +201,11 @@ Screenshot harness `scripts/density-screenshots/` (`maxDiffPixels: 0`): - Key granularity = component's real spacing structure. One shorthand key (`pad`) when sides set together on one element; split per axis only when forced (see gotcha). Per axis: **sized by default** (per-size tunable); base token only - if per-size override is genuinely meaningless — a size-invariant *default* alone + if per-size override is genuinely meaningless — a size-invariant _default_ alone doesn't justify base (size it so density can tune it per size). +- **Boolean toggle (`dense`)** = **state token**: off-state is the plain seam + `--Component-` (don't qualify it); only the on-state is qualified + `--Component-dense-`. Never `--Component-normal/regular/default-`. ## Order to roll out From e7342e4802938706eab26bed90bed5730bdb4536 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 08:10:59 +0700 Subject: [PATCH 014/114] [material-ui] Density adapter: Checkbox, Radio, Switch via SwitchBase SwitchBase (shared agnostic base) consumes one seam: padding var(--SwitchBase-pad, var(--_pad)), --_pad 9px. Checkbox/Radio (styled(SwitchBase)) route per-size public tokens --Checkbox/Radio--pad into the seam; default 9px both sizes (pixel-identical). Switch routes its thumb (SwitchBase) padding via --Switch--pad (9/4); box geometry stays literal (size-coupled). enhanceDensity + fixture wired. --- docs/pages/experiments/density-fixture.tsx | 73 +++++++++++++++++++ .../mui-material/src/Checkbox/Checkbox.js | 10 +++ packages/mui-material/src/Radio/Radio.js | 10 +++ packages/mui-material/src/Switch/Switch.js | 11 ++- .../mui-material/src/internal/SwitchBase.js | 6 +- .../mui-material/src/styles/enhanceDensity.ts | 41 +++++++++++ 6 files changed, 149 insertions(+), 2 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 9247ff193e3c28..d83c64591ad47c 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -34,6 +34,9 @@ import Select from '@mui/material/Select'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; import Badge from '@mui/material/Badge'; +import Checkbox from '@mui/material/Checkbox'; +import Radio from '@mui/material/Radio'; +import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; import FaceIcon from '@mui/icons-material/Face'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -589,6 +592,46 @@ const demos: Record = {
), + Checkbox: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + + ))} + + ), + Radio: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + ))} + + ), + Switch: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + ))} + + ), }; // Per-component density-token overrides for the review levels. `default` is @@ -802,6 +845,36 @@ const scopes: Record> = { ['--Badge-dot-size' as any]: '12px', }, }, + Checkbox: { + dense: { + ['--Checkbox-small-pad' as any]: '3px', + ['--Checkbox-medium-pad' as any]: '5px', + }, + loose: { + ['--Checkbox-small-pad' as any]: '7px', + ['--Checkbox-medium-pad' as any]: '13px', + }, + }, + Radio: { + dense: { + ['--Radio-small-pad' as any]: '3px', + ['--Radio-medium-pad' as any]: '5px', + }, + loose: { + ['--Radio-small-pad' as any]: '7px', + ['--Radio-medium-pad' as any]: '13px', + }, + }, + Switch: { + dense: { + ['--Switch-small-pad' as any]: '2px', + ['--Switch-medium-pad' as any]: '6px', + }, + loose: { + ['--Switch-small-pad' as any]: '7px', + ['--Switch-medium-pad' as any]: '12px', + }, + }, }; // TextField rides the same OutlinedInput tokens; OutlinedInput's `:has` rule // drives the label's --InputLabel-y, so input box + label move together. diff --git a/packages/mui-material/src/Checkbox/Checkbox.js b/packages/mui-material/src/Checkbox/Checkbox.js index ef4cbfc2c328f7..b13141cbe2527e 100644 --- a/packages/mui-material/src/Checkbox/Checkbox.js +++ b/packages/mui-material/src/Checkbox/Checkbox.js @@ -55,6 +55,16 @@ const CheckboxRoot = styled(SwitchBase, { memoTheme(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, variants: [ + // Density: route the per-size public token into SwitchBase's seam. Default + // 9px both sizes (pixel-identical); size enables per-size density tuning. + { + props: { size: 'small' }, + style: { '--SwitchBase-pad': 'var(--Checkbox-small-pad, var(--_pad))' }, + }, + { + props: { size: 'medium' }, + style: { '--SwitchBase-pad': 'var(--Checkbox-medium-pad, var(--_pad))' }, + }, { props: { color: 'default', disableRipple: false }, style: { diff --git a/packages/mui-material/src/Radio/Radio.js b/packages/mui-material/src/Radio/Radio.js index 48e1d7fd57df59..217d972d1449a6 100644 --- a/packages/mui-material/src/Radio/Radio.js +++ b/packages/mui-material/src/Radio/Radio.js @@ -50,6 +50,16 @@ const RadioRoot = styled(SwitchBase, { color: (theme.vars || theme).palette.action.disabled, }, variants: [ + // Density: route the per-size public token into SwitchBase's seam. Default + // 9px both sizes (pixel-identical); size enables per-size density tuning. + { + props: { size: 'small' }, + style: { '--SwitchBase-pad': 'var(--Radio-small-pad, var(--_pad))' }, + }, + { + props: { size: 'medium' }, + style: { '--SwitchBase-pad': 'var(--Radio-medium-pad, var(--_pad))' }, + }, { props: { color: 'default', disabled: false, disableRipple: false }, style: { diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 6c8dd8b059738f..7643610b407a2c 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -64,6 +64,12 @@ const SwitchRoot = styled('span', { '@media print': { colorAdjust: 'exact', }, + // Density: the thumb (SwitchBase) inherits the seam from here — no descendant + // selector needed (custom props inherit; the thumb doesn't redeclare the seam). + // `--_pad` is the thumb's default fallback (the root's own padding stays + // literal — box geometry is size-coupled). + '--_pad': '9px', + '--SwitchBase-pad': 'var(--Switch-medium-pad, var(--_pad))', variants: [ { props: { edge: 'start' }, @@ -79,12 +85,15 @@ const SwitchRoot = styled('span', { width: 40, height: 24, padding: 7, + // Small thumb default is 4 (≠ the thumb's own --_pad 9), so feed it via + // the seam: set --_pad here (inherited as the seam's fallback). + '--_pad': '4px', + '--SwitchBase-pad': 'var(--Switch-small-pad, var(--_pad))', [`& .${switchClasses.thumb}`]: { width: 16, height: 16, }, [`& .${switchClasses.switchBase}`]: { - padding: 4, [`&.${switchClasses.checked}`]: { transform: 'translateX(16px)', }, diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js index 5257bfe688e49e..e837e251205cc8 100644 --- a/packages/mui-material/src/internal/SwitchBase.js +++ b/packages/mui-material/src/internal/SwitchBase.js @@ -25,7 +25,11 @@ const useUtilityClasses = (ownerState) => { const SwitchBaseRoot = styled(ButtonBase, { name: 'MuiSwitchBase', })({ - padding: 9, + // Density adapter (docs/adr/0001): SwitchBase is the agnostic layer shared by + // Checkbox/Radio (and the Switch thumb). It consumes one seam; the Material + // layer (Checkbox/Radio) routes its per-size public token into --SwitchBase-pad. + '--_pad': '9px', + padding: 'var(--SwitchBase-pad, var(--_pad))', borderRadius: '50%', variants: [ { diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 79e48639d5b3c9..b67ce779994368 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -372,6 +372,47 @@ export default function enhanceDensity< ], }, }, + MuiCheckbox: { + ...c?.MuiCheckbox, + styleOverrides: { + ...c?.MuiCheckbox?.styleOverrides, + root: [ + c?.MuiCheckbox?.styleOverrides?.root, + { + // Touch-target padding (9px default both sizes), via SwitchBase. + '--Checkbox-medium-pad': varRefs.sm, + '--Checkbox-small-pad': varRefs.xs, + }, + ], + }, + }, + MuiRadio: { + ...c?.MuiRadio, + styleOverrides: { + ...c?.MuiRadio?.styleOverrides, + root: [ + c?.MuiRadio?.styleOverrides?.root, + { + '--Radio-medium-pad': varRefs.sm, + '--Radio-small-pad': varRefs.xs, + }, + ], + }, + }, + MuiSwitch: { + ...c?.MuiSwitch, + styleOverrides: { + ...c?.MuiSwitch?.styleOverrides, + root: [ + c?.MuiSwitch?.styleOverrides?.root, + { + // Thumb (SwitchBase) padding; box geometry stays literal. + '--Switch-medium-pad': varRefs.sm, + '--Switch-small-pad': varRefs.xxs, + }, + ], + }, + }, }; return theme; From 28d6b6b716010e4af8fd25598ab5fc07166c89e8 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 08:25:19 +0700 Subject: [PATCH 015/114] [docs] Document the shared internal base pattern (Recipe C) SwitchBase owns the agnostic seam consumed once; Checkbox/Radio/Switch route per-component sized tokens into it. Covers the two reader topologies (consumer is the base vs wraps it as a descendant), delivery via custom-property inheritance (no descendant selector), and the --_-shadowing caveat. Added to CONTEXT relationships, ADR 0001 specifics, rollout Recipe C + Done list. --- CONTEXT.md | 13 +++++++ docs/adr/0001-css-var-density-adapter.md | 30 +++++++++++++++ docs/adr/density-adapter-rollout.md | 49 ++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f1312c44701628..bc39ab667dcc32 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -138,6 +138,19 @@ _Avoid_: "density preset" (that is the resulting effect, not the function). literal default). The **specific** component, `OutlinedInput`, owns the bridge: it reaches its preceding-sibling label via `:has(~ &)` and sets `--InputLabel-y` from its public token. Generic never names specific; one knob still drives both. +- **A shared internal base owns the agnostic layer; consumers route their own + tokens into it.** `SwitchBase` consumes the seam once + (`var(--SwitchBase-pad, var(--_pad))`); `Checkbox`/`Radio`/`Switch` (the Material + layer) each route a per-component sized token (`--Checkbox--pad`, …) into + that shared seam, staying independently tunable. The seam keeps the base's name + (plumbing); the knob is the per-component token. Delivery rides **custom-property + inheritance**, no descendant selector: where the consumer _is_ the base + (`styled(SwitchBase)`) it sets the seam on its own root; where it _wraps_ the + base (the Switch thumb), the wrapper root sets the seam and the base inherits it + (the base doesn't redeclare the seam). Caveat: the base _does_ redeclare + `--_` (what makes it unprefixed-safe), so an inherited `--_` is + shadowed — a wrapper needing a different per-state default feeds it through the + seam (set `--_` on the wrapper), not by inheriting `--_`. - **enhanceDensity** (opt-in) connects tier-2 component tokens to the tier-1 **density scale**; un-enhanced, the literal fallbacks reproduce today's pixels. - This experiment does **not** ride `--mui-spacing`; holistic density comes from diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 094d8f51bd3a58..619df2a5bb88cc 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -215,6 +215,36 @@ a generic component name a sibling's token (wrong direction); a flat-scope keeps the coupling in the one component that legitimately owns it. Cost: needs `:has()` (Chrome 105 / Safari 15.4 / Firefox 121). +### Shared internal base (SwitchBase → Checkbox, Radio, Switch) + +When several components share one styled base, the **agnostic layer lives on the +base**: `SwitchBase` consumes the seam once (`padding: var(--SwitchBase-pad, +var(--_pad))`, `--_pad: 9px`), and each consumer — the **Material layer** — routes +its own per-component public token into the shared seam. The seam keeps the +_base's_ name (`--SwitchBase-pad`, plumbing); the designer-facing knob is the +per-component sized token (`--Checkbox--pad`, `--Radio--pad`, +`--Switch--pad`). Each consumer stays independently tunable while the base +holds the one consumption point. + +Two reader topologies, both relying on **custom-property inheritance** (no +descendant selector, no added specificity): + +- **Consumer is the base.** `Checkbox`/`Radio` are `styled(SwitchBase)`, so their + size variants set `--SwitchBase-pad` on the very element that consumes it. +- **Consumer wraps the base.** The `Switch` thumb is a `SwitchBase` _inside_ + `SwitchRoot`; the root sets `--SwitchBase-pad` and the thumb **inherits** it. + This works precisely because `SwitchBase` does not redeclare the seam. + +The inheritance caveat is the mirror of why `--_` is safe unprefixed: the +base **redeclares `--_`** on itself, so an inherited `--_` is shadowed. +The seam inherits (not redeclared); the internal default does not. So when a +wrapper needs a per-state default that differs from the base's (the small Switch +thumb is `4px`, not the base's `9px`), it can't inherit `--_` — it feeds the +value **through the seam** by setting `--_` on the wrapper root, where the +seam's `var(..., var(--_))` fallback resolves. Box geometry that is coupled +to the value (Switch's `width = 34 + 12·2`) is left literal — only the +inheritance-safe padding axis is tokenized. + ## Consequences - **Pixel-identical default & non-breaking.** Literals come from the `variants` diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index e6f3d7d1cd34aa..163e3e9db60449 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -131,6 +131,45 @@ token** + literal fallback (can't read the input's internal `--_padBlock`): One knob (`--OutlinedInput--padBlock`) -> input box + label move together. +## Recipe C — shared internal base (SwitchBase -> Checkbox / Radio / Switch) + +Several components share one styled base. Put the **agnostic layer on the base**: +it consumes the seam once, with the internal default. Each consumer is the +**Material layer** -> routes its _own_ per-component public token into the shared +seam. + +```js +// SwitchBase (shared, agnostic): consume once +'--_pad': '9px', +padding: 'var(--SwitchBase-pad, var(--_pad))', +``` + +The seam keeps the **base's** name (`--SwitchBase-pad`) — it's plumbing; the +public knob is the per-component sized token (`--Checkbox--pad`). + +Two reader topologies: + +- **Consumer _is_ the base** (Checkbox/Radio = `styled(SwitchBase)`): route on + the consumer's own root, same element, no selector. + ```js + { props: { size: 'small' }, style: { '--SwitchBase-pad': 'var(--Checkbox-small-pad, var(--_pad))' } } + ``` +- **Consumer _wraps_ the base** as a descendant (the Switch thumb is a + `SwitchBase` inside `SwitchRoot`): set the seam on the wrapper root — the base + **inherits** it. No descendant selector (custom props inherit; the base doesn't + redeclare the seam). + ```js + // SwitchRoot: thumb inherits this + '--SwitchBase-pad': 'var(--Switch-medium-pad, var(--_pad))', + ``` + +**Inheritance caveat.** The _seam_ inherits because the base doesn't redeclare it. +But the base **does** redeclare `--_` (that's what keeps it unprefixed-safe), +so an inherited `--_` is _shadowed_ on the base. If a wrapper needs a +per-state default different from the base's (Switch small thumb `4` ≠ base `9`), +feed it **through the seam** — set `--_` on the wrapper so the seam's fallback +resolves there; don't expect the base to inherit your `--_`. + ## Gotchas - **Split axes only when the impl forces it.** Differing values per side is NOT @@ -210,6 +249,10 @@ Screenshot harness `scripts/density-screenshots/` (`maxDiffPixels: 0`): ## Order to roll out Small single-element first (prove pattern) -> bigger multi-element -> paired -sibling family. Done: Button, OutlinedInput (+ InputLabel, TextField outlined). -Next candidates: FilledInput, Input (standard) — note asymmetric block padding -(`4/5`, `25/8`) -> need per-side seam, not single `padBlock`. +sibling family. Done: Button, OutlinedInput (+ InputLabel, TextField outlined), +the dashboard set (Chip, IconButton, MenuItem, ListItem(+Button/Icon/Text), +ListSubheader, Toolbar, Tab/Tabs, TablePagination, CardContent, Select, +Breadcrumbs, InputAdornment, Badge), and the SwitchBase family (Checkbox, Radio, +Switch — Recipe C). Next candidates: FilledInput, Input (standard) — note +asymmetric block padding (`4/5`, `25/8`) -> need per-side seam, not single +`padBlock`. From ddfcd906a5e755e9a15cfcb3611a9aab3b473437 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:10:34 +0700 Subject: [PATCH 016/114] [material-ui] Switch density: derive pad/travel from interlocked dims Tokenize Switch's four real dims per size (--Switch--width/height/thumbSize/ touchSize). Derive SwitchBase pad = (touchSize-thumbSize)/2, button top = (height-touchSize)/2, checked travel = width-touchSize, thumb size = thumbSize, so the thumb stays centered on the track (absolute + transform). Replaces the pad-only token that drifted the thumb. Switch dropped from enhanceDensity (geometry isn't spacing-scale-derived). Default pixel-identical. --- docs/pages/experiments/density-fixture.tsx | 22 +++++-- packages/mui-material/src/Switch/Switch.js | 63 +++++++++++-------- .../mui-material/src/styles/enhanceDensity.ts | 17 +---- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index d83c64591ad47c..4f00f846b4cf7f 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -866,13 +866,27 @@ const scopes: Record> = { }, }, Switch: { + // Tune the four interlocked dims; pad/top/travel re-derive (touchSize == height + // keeps the thumb centered). thumbSize < height; width > touchSize. dense: { - ['--Switch-small-pad' as any]: '2px', - ['--Switch-medium-pad' as any]: '6px', + ['--Switch-small-width' as any]: '32px', + ['--Switch-small-height' as any]: '18px', + ['--Switch-small-thumbSize' as any]: '12px', + ['--Switch-small-touchSize' as any]: '18px', + ['--Switch-medium-width' as any]: '44px', + ['--Switch-medium-height' as any]: '24px', + ['--Switch-medium-thumbSize' as any]: '16px', + ['--Switch-medium-touchSize' as any]: '24px', }, loose: { - ['--Switch-small-pad' as any]: '7px', - ['--Switch-medium-pad' as any]: '12px', + ['--Switch-small-width' as any]: '52px', + ['--Switch-small-height' as any]: '32px', + ['--Switch-small-thumbSize' as any]: '24px', + ['--Switch-small-touchSize' as any]: '32px', + ['--Switch-medium-width' as any]: '76px', + ['--Switch-medium-height' as any]: '48px', + ['--Switch-medium-thumbSize' as any]: '34px', + ['--Switch-medium-touchSize' as any]: '48px', }, }, }; diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 7643610b407a2c..2fc56b9dd5fce7 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -51,9 +51,27 @@ const SwitchRoot = styled('span', { ]; }, })({ + // Density (docs/adr/0001): Switch geometry is interlocked, so the meaningful + // knobs are the four dims (width/height/thumbSize/touchSize); the thumb's touch + // padding and travel are *derived* so the thumb stays centered on the track. + // SwitchBase pad = (touchSize - thumbSize) / 2 (centers thumb in the button) + // button top = (height - touchSize) / 2 (centers button in the root) + // checked travel = width - touchSize + // Defaults: touchSize == height -> pad 9/4, top 0, travel 20/16 (pixel-identical). + // The thumb (SwitchBase) and Thumb/Track slots inherit these seams (custom props + // inherit; they don't redeclare them). Track gutter (padding) stays literal. + '--_width': '58px', // 34 (track) + 12 (gutter) * 2 + '--_height': '38px', // 14 (track) + 12 (gutter) * 2 + '--_thumbSize': '20px', + '--_touchSize': '38px', + '--Switch-width': 'var(--Switch-medium-width, var(--_width))', + '--Switch-height': 'var(--Switch-medium-height, var(--_height))', + '--Switch-thumbSize': 'var(--Switch-medium-thumbSize, var(--_thumbSize))', + '--Switch-touchSize': 'var(--Switch-medium-touchSize, var(--_touchSize))', + '--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', display: 'inline-flex', - width: 34 + 12 * 2, - height: 14 + 12 * 2, + width: 'var(--Switch-width, var(--_width))', + height: 'var(--Switch-height, var(--_height))', overflow: 'hidden', padding: 12, boxSizing: 'border-box', @@ -64,12 +82,6 @@ const SwitchRoot = styled('span', { '@media print': { colorAdjust: 'exact', }, - // Density: the thumb (SwitchBase) inherits the seam from here — no descendant - // selector needed (custom props inherit; the thumb doesn't redeclare the seam). - // `--_pad` is the thumb's default fallback (the root's own padding stays - // literal — box geometry is size-coupled). - '--_pad': '9px', - '--SwitchBase-pad': 'var(--Switch-medium-pad, var(--_pad))', variants: [ { props: { edge: 'start' }, @@ -82,22 +94,16 @@ const SwitchRoot = styled('span', { { props: { size: 'small' }, style: { - width: 40, - height: 24, + // Re-route the four dims to the small tokens; pad/top/travel re-derive. + '--_width': '40px', + '--_height': '24px', + '--_thumbSize': '16px', + '--_touchSize': '24px', + '--Switch-width': 'var(--Switch-small-width, var(--_width))', + '--Switch-height': 'var(--Switch-small-height, var(--_height))', + '--Switch-thumbSize': 'var(--Switch-small-thumbSize, var(--_thumbSize))', + '--Switch-touchSize': 'var(--Switch-small-touchSize, var(--_touchSize))', padding: 7, - // Small thumb default is 4 (≠ the thumb's own --_pad 9), so feed it via - // the seam: set --_pad here (inherited as the seam's fallback). - '--_pad': '4px', - '--SwitchBase-pad': 'var(--Switch-small-pad, var(--_pad))', - [`& .${switchClasses.thumb}`]: { - width: 16, - height: 16, - }, - [`& .${switchClasses.switchBase}`]: { - [`&.${switchClasses.checked}`]: { - transform: 'translateX(16px)', - }, - }, }, }, ], @@ -118,7 +124,8 @@ const SwitchSwitchBase = styled(SwitchBase, { })( memoTheme(({ theme }) => ({ position: 'absolute', - top: 0, + // Center the touch target in the root (top 0 when touchSize == height). + top: 'calc((var(--Switch-height, var(--_height)) - var(--Switch-touchSize, var(--_touchSize))) / 2)', left: 0, zIndex: 1, // Render above the focus ripple. color: theme.vars @@ -128,7 +135,9 @@ const SwitchSwitchBase = styled(SwitchBase, { duration: theme.transitions.duration.shortest, }), [`&.${switchClasses.checked}`]: { - transform: 'translateX(20px)', + // Travel = root width - touch target (keeps the thumb symmetric on the track). + transform: + 'translateX(calc(var(--Switch-width, var(--_width)) - var(--Switch-touchSize, var(--_touchSize))))', }, [`&.${switchClasses.disabled}`]: { color: theme.vars @@ -229,8 +238,8 @@ const SwitchThumb = styled('span', { backgroundColor: 'currentColor', boxSizing: 'border-box', border: '1px solid transparent', - width: 20, - height: 20, + width: 'var(--Switch-thumbSize, var(--_thumbSize))', + height: 'var(--Switch-thumbSize, var(--_thumbSize))', borderRadius: '50%', })), ); diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index b67ce779994368..93af977d698f38 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -399,20 +399,9 @@ export default function enhanceDensity< ], }, }, - MuiSwitch: { - ...c?.MuiSwitch, - styleOverrides: { - ...c?.MuiSwitch?.styleOverrides, - root: [ - c?.MuiSwitch?.styleOverrides?.root, - { - // Thumb (SwitchBase) padding; box geometry stays literal. - '--Switch-medium-pad': varRefs.sm, - '--Switch-small-pad': varRefs.xxs, - }, - ], - }, - }, + // Switch is intentionally not wired here: its geometry (width/height/thumbSize/ + // touchSize) is interlocked, not spacing-scale-derived. Tune it per size via + // the public --Switch--* tokens directly. }; return theme; From dd57b714ecb5a35e98612a52bdc1635c5729547a Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:13:29 +0700 Subject: [PATCH 017/114] [docs] Update Switch density: interlocked geometry, derive coupled values Switch tokenizes width/height/thumbSize/touchSize per size and derives pad/top/ travel via calc (thumb stays centered); not the pad-only approach. Corrects the shared-base sections in ADR 0001 + rollout Recipe C. --- docs/adr/0001-css-var-density-adapter.md | 23 +++++++++++++++------- docs/adr/density-adapter-rollout.md | 25 ++++++++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 619df2a5bb88cc..6fffb3c92ff1d5 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -237,13 +237,22 @@ descendant selector, no added specificity): The inheritance caveat is the mirror of why `--_` is safe unprefixed: the base **redeclares `--_`** on itself, so an inherited `--_` is shadowed. -The seam inherits (not redeclared); the internal default does not. So when a -wrapper needs a per-state default that differs from the base's (the small Switch -thumb is `4px`, not the base's `9px`), it can't inherit `--_` — it feeds the -value **through the seam** by setting `--_` on the wrapper root, where the -seam's `var(..., var(--_))` fallback resolves. Box geometry that is coupled -to the value (Switch's `width = 34 + 12·2`) is left literal — only the -inheritance-safe padding axis is tokenized. +The seam inherits (not redeclared); the internal default does not. So a wrapper +that needs a value different from the base's feeds it **through the seam** — set +the seam directly on the wrapper (preferred), not the shadowed `--_`. Switch +does exactly this: it sets `--SwitchBase-pad` to a derived `calc` (below). + +**Interlocked geometry — derive, don't tokenize one axis.** A `Switch`'s width, +height, thumb, touch target and travel all move together; tokenizing the thumb +pad alone drifts the thumb off the track. So Switch tokenizes the four real dims +per size (`--Switch--width/height/thumbSize/touchSize`) and **derives** the +coupled values with `calc`, feeding the shared seam: SwitchBase pad +`= (touchSize − thumbSize) / 2`; the absolutely-positioned button stays centered +via `top = (height − touchSize) / 2` and checked `transform: translateX(width − +touchSize)`; the thumb slot reads `thumbSize`. Defaults (`touchSize == height`) +compute to today's `9/4` pad, `0` top, `20/16` travel — pixel-identical. +`enhanceDensity` skips Switch (geometry isn't spacing-scale-derived); tune it per +size through the public dim tokens. ## Consequences diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index 163e3e9db60449..fc4ed9976d0324 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -165,10 +165,27 @@ Two reader topologies: **Inheritance caveat.** The _seam_ inherits because the base doesn't redeclare it. But the base **does** redeclare `--_` (that's what keeps it unprefixed-safe), -so an inherited `--_` is _shadowed_ on the base. If a wrapper needs a -per-state default different from the base's (Switch small thumb `4` ≠ base `9`), -feed it **through the seam** — set `--_` on the wrapper so the seam's fallback -resolves there; don't expect the base to inherit your `--_`. +so an inherited `--_` is _shadowed_ on the base. A wrapper that needs a value +different from the base's feeds it **through the seam** — set the seam directly on +the wrapper, not the shadowed `--_`. + +**Interlocked geometry -> derive, don't tokenize one axis.** When a component's +dims move together (a `Switch`: width/height/thumb/touch/travel), tokenizing one +(the thumb pad) alone drifts the thumb off the track. Tokenize the real dims per +size and **derive** the coupled values with `calc`, feeding the seam: + +```js +// SwitchRoot, per size: --Switch--{width,height,thumbSize,touchSize} +'--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', +// thumb button (absolute): keep it centered +top: 'calc((var(--Switch-height) - var(--Switch-touchSize)) / 2)', +// checked: travel = width - touch +transform: 'translateX(calc(var(--Switch-width) - var(--Switch-touchSize)))', +``` + +`touchSize == height` by default -> pad `9/4`, top `0`, travel `20/16` +(pixel-identical). Skip such a component in `enhanceDensity` (its dims aren't +spacing-scale-derived) — tune per size via the public dim tokens. ## Gotchas From 7d9679b101c540b9e54acd7ff3878aa0583515c3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:17:10 +0700 Subject: [PATCH 018/114] [material-ui] Switch: tokenize track gutter (--Switch--pad) The root padding (12/7, track inset) is its own axis -> tokenize as --Switch--pad over --_pad, consumed padding: var(--Switch-pad, var(--_pad)). Distinct from the derived thumb SwitchBase pad. Fixture scope + docs updated. --- docs/adr/0001-css-var-density-adapter.md | 7 ++++--- docs/adr/density-adapter-rollout.md | 6 ++++-- docs/pages/experiments/density-fixture.tsx | 9 +++++++-- packages/mui-material/src/Switch/Switch.js | 18 +++++++++++------- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index 6fffb3c92ff1d5..fc95f7b52d716d 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -244,9 +244,10 @@ does exactly this: it sets `--SwitchBase-pad` to a derived `calc` (below). **Interlocked geometry — derive, don't tokenize one axis.** A `Switch`'s width, height, thumb, touch target and travel all move together; tokenizing the thumb -pad alone drifts the thumb off the track. So Switch tokenizes the four real dims -per size (`--Switch--width/height/thumbSize/touchSize`) and **derives** the -coupled values with `calc`, feeding the shared seam: SwitchBase pad +pad alone drifts the thumb off the track. So Switch tokenizes its real dims per +size (`--Switch--width/height/thumbSize/touchSize` + the track gutter +`--Switch--pad`) and **derives** the coupled values with `calc`, feeding the +shared seam: SwitchBase pad `= (touchSize − thumbSize) / 2`; the absolutely-positioned button stays centered via `top = (height − touchSize) / 2` and checked `transform: translateX(width − touchSize)`; the thumb slot reads `thumbSize`. Defaults (`touchSize == height`) diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index fc4ed9976d0324..aba607c8e12373 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -172,10 +172,12 @@ the wrapper, not the shadowed `--_`. **Interlocked geometry -> derive, don't tokenize one axis.** When a component's dims move together (a `Switch`: width/height/thumb/touch/travel), tokenizing one (the thumb pad) alone drifts the thumb off the track. Tokenize the real dims per -size and **derive** the coupled values with `calc`, feeding the seam: +size (incl. the track gutter `pad`) and **derive** the coupled values with `calc`, +feeding the seam: ```js -// SwitchRoot, per size: --Switch--{width,height,thumbSize,touchSize} +// SwitchRoot, per size: --Switch--{width,height,thumbSize,touchSize,pad} +padding: 'var(--Switch-pad, var(--_pad))', // track gutter (own axis) '--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', // thumb button (absolute): keep it centered top: 'calc((var(--Switch-height) - var(--Switch-touchSize)) / 2)', diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 4f00f846b4cf7f..f56b6018e639d6 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -866,27 +866,32 @@ const scopes: Record> = { }, }, Switch: { - // Tune the four interlocked dims; pad/top/travel re-derive (touchSize == height - // keeps the thumb centered). thumbSize < height; width > touchSize. + // Tune the interlocked dims + track gutter (pad); thumb pad/top/travel re-derive + // (touchSize == height keeps the thumb centered). thumbSize < height; width > + // touchSize; pad < height/2. dense: { ['--Switch-small-width' as any]: '32px', ['--Switch-small-height' as any]: '18px', ['--Switch-small-thumbSize' as any]: '12px', ['--Switch-small-touchSize' as any]: '18px', + ['--Switch-small-pad' as any]: '5px', ['--Switch-medium-width' as any]: '44px', ['--Switch-medium-height' as any]: '24px', ['--Switch-medium-thumbSize' as any]: '16px', ['--Switch-medium-touchSize' as any]: '24px', + ['--Switch-medium-pad' as any]: '8px', }, loose: { ['--Switch-small-width' as any]: '52px', ['--Switch-small-height' as any]: '32px', ['--Switch-small-thumbSize' as any]: '24px', ['--Switch-small-touchSize' as any]: '32px', + ['--Switch-small-pad' as any]: '10px', ['--Switch-medium-width' as any]: '76px', ['--Switch-medium-height' as any]: '48px', ['--Switch-medium-thumbSize' as any]: '34px', ['--Switch-medium-touchSize' as any]: '48px', + ['--Switch-medium-pad' as any]: '16px', }, }, }; diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 2fc56b9dd5fce7..0cf46ea9b2c608 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -51,29 +51,32 @@ const SwitchRoot = styled('span', { ]; }, })({ - // Density (docs/adr/0001): Switch geometry is interlocked, so the meaningful - // knobs are the four dims (width/height/thumbSize/touchSize); the thumb's touch - // padding and travel are *derived* so the thumb stays centered on the track. + // Density (docs/adr/0001): Switch geometry is interlocked, so the knobs are the + // dims (width/height/thumbSize/touchSize) + the track gutter (pad); the thumb's + // touch padding and travel are *derived* so the thumb stays centered on the track. // SwitchBase pad = (touchSize - thumbSize) / 2 (centers thumb in the button) // button top = (height - touchSize) / 2 (centers button in the root) // checked travel = width - touchSize // Defaults: touchSize == height -> pad 9/4, top 0, travel 20/16 (pixel-identical). // The thumb (SwitchBase) and Thumb/Track slots inherit these seams (custom props - // inherit; they don't redeclare them). Track gutter (padding) stays literal. + // inherit; they don't redeclare them). `--_pad` here is the root's gutter default + // (the track inset), distinct from the thumb's own SwitchBase `--_pad`. '--_width': '58px', // 34 (track) + 12 (gutter) * 2 '--_height': '38px', // 14 (track) + 12 (gutter) * 2 '--_thumbSize': '20px', '--_touchSize': '38px', + '--_pad': '12px', '--Switch-width': 'var(--Switch-medium-width, var(--_width))', '--Switch-height': 'var(--Switch-medium-height, var(--_height))', '--Switch-thumbSize': 'var(--Switch-medium-thumbSize, var(--_thumbSize))', '--Switch-touchSize': 'var(--Switch-medium-touchSize, var(--_touchSize))', + '--Switch-pad': 'var(--Switch-medium-pad, var(--_pad))', '--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', display: 'inline-flex', width: 'var(--Switch-width, var(--_width))', height: 'var(--Switch-height, var(--_height))', overflow: 'hidden', - padding: 12, + padding: 'var(--Switch-pad, var(--_pad))', boxSizing: 'border-box', position: 'relative', flexShrink: 0, @@ -94,16 +97,17 @@ const SwitchRoot = styled('span', { { props: { size: 'small' }, style: { - // Re-route the four dims to the small tokens; pad/top/travel re-derive. + // Re-route the dims + gutter to the small tokens; pad/top/travel re-derive. '--_width': '40px', '--_height': '24px', '--_thumbSize': '16px', '--_touchSize': '24px', + '--_pad': '7px', '--Switch-width': 'var(--Switch-small-width, var(--_width))', '--Switch-height': 'var(--Switch-small-height, var(--_height))', '--Switch-thumbSize': 'var(--Switch-small-thumbSize, var(--_thumbSize))', '--Switch-touchSize': 'var(--Switch-small-touchSize, var(--_touchSize))', - padding: 7, + '--Switch-pad': 'var(--Switch-small-pad, var(--_pad))', }, }, ], From ada1d5e52e319d50b61c48ddd733fc1c44274550 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:19:47 +0700 Subject: [PATCH 019/114] [material-ui] Switch: derive track borderRadius from height + gutter borderRadius = (height - 2*pad)/2 (full-pill track thickness) instead of literal 14/2, so the track stays rounded when the dims are tuned. Pixel-identical (medium 7px; small clamps to a pill). --- packages/mui-material/src/Switch/Switch.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 0cf46ea9b2c608..03a195b2a1ce65 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -215,7 +215,10 @@ const SwitchTrack = styled('span', { memoTheme(({ theme }) => ({ height: '100%', width: '100%', - borderRadius: 14 / 2, + // Full pill: half the track thickness (height minus the two gutters). Inherits + // the seams from SwitchRoot. Medium -> 7px; small clamps to a pill either way. + borderRadius: + 'calc((var(--Switch-height, var(--_height)) - 2 * var(--Switch-pad, var(--_pad))) / 2)', zIndex: -1, transition: theme.transitions.create(['opacity', 'background-color'], { duration: theme.transitions.duration.shortest, From c8ca3cb44e3bfa56da289eee8084a711de77a8c6 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:29:46 +0700 Subject: [PATCH 020/114] [material-ui] enhanceDensity: add xxl step, wire Switch dims Add an xxl density step (4x spacing unit). Wire MuiSwitch: map per-size width/height/touchSize/thumbSize/pad to scale steps (xxl for the wider track); pad/top/travel/radius re-derive so the geometry stays valid. Docs updated. --- docs/adr/0001-css-var-density-adapter.md | 6 ++-- docs/adr/density-adapter-rollout.md | 4 +-- .../mui-material/src/styles/enhanceDensity.ts | 32 ++++++++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md index fc95f7b52d716d..b9c6eba479d79a 100644 --- a/docs/adr/0001-css-var-density-adapter.md +++ b/docs/adr/0001-css-var-density-adapter.md @@ -252,8 +252,10 @@ shared seam: SwitchBase pad via `top = (height − touchSize) / 2` and checked `transform: translateX(width − touchSize)`; the thumb slot reads `thumbSize`. Defaults (`touchSize == height`) compute to today's `9/4` pad, `0` top, `20/16` travel — pixel-identical. -`enhanceDensity` skips Switch (geometry isn't spacing-scale-derived); tune it per -size through the public dim tokens. +`enhanceDensity` _can_ wire Switch precisely because it derives: it maps the input +dims to scale steps (the `xxl` step covers the wider track) and pad/top/travel/ +radius re-derive, so the geometry stays valid (`touchSize == height` keeps it +centered, `width > touchSize` keeps travel positive). ## Consequences diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md index aba607c8e12373..9739eb25cb91df 100644 --- a/docs/adr/density-adapter-rollout.md +++ b/docs/adr/density-adapter-rollout.md @@ -186,8 +186,8 @@ transform: 'translateX(calc(var(--Switch-width) - var(--Switch-touchSize)))', ``` `touchSize == height` by default -> pad `9/4`, top `0`, travel `20/16` -(pixel-identical). Skip such a component in `enhanceDensity` (its dims aren't -spacing-scale-derived) — tune per size via the public dim tokens. +(pixel-identical). `enhanceDensity` can still wire it: map the input dims to scale +steps and the derived values stay valid (Switch uses `xxl` for the wider track). ## Gotchas diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 93af977d698f38..1f50db894693a3 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -11,6 +11,7 @@ export interface DensityScale { md: string; lg: string; xl: string; + xxl: string; } export interface DensityOptions { @@ -22,7 +23,7 @@ export interface DensityOptions { type DensityKey = keyof DensityScale; -const densityKeys: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl']; +const densityKeys: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; // Default scale: t-shirt steps derived from the theme spacing unit. const defaultMultiplier: Record = { @@ -32,6 +33,7 @@ const defaultMultiplier: Record = { md: 1.5, lg: 2, xl: 3, + xxl: 4, }; const cssVar = (key: DensityKey) => `--mui-density-${key}`; @@ -399,9 +401,31 @@ export default function enhanceDensity< ], }, }, - // Switch is intentionally not wired here: its geometry (width/height/thumbSize/ - // touchSize) is interlocked, not spacing-scale-derived. Tune it per size via - // the public --Switch--* tokens directly. + MuiSwitch: { + ...c?.MuiSwitch, + styleOverrides: { + ...c?.MuiSwitch?.styleOverrides, + root: [ + c?.MuiSwitch?.styleOverrides?.root, + { + // Switch maps its input dims to scale steps; pad/top/travel/radius + // re-derive from them, so the geometry stays valid (touchSize == height + // -> centered; width > touchSize -> positive travel). `xxl` covers the + // wider track. + '--Switch-medium-width': varRefs.xxl, + '--Switch-medium-height': varRefs.xl, + '--Switch-medium-touchSize': varRefs.xl, + '--Switch-medium-thumbSize': varRefs.lg, + '--Switch-medium-pad': varRefs.sm, + '--Switch-small-width': varRefs.xl, + '--Switch-small-height': varRefs.lg, + '--Switch-small-touchSize': varRefs.lg, + '--Switch-small-thumbSize': varRefs.md, + '--Switch-small-pad': varRefs.xs, + }, + ], + }, + }, }; return theme; From 02835001aa7862f0c078e4c1570cf189adb3dcb7 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 09:44:38 +0700 Subject: [PATCH 021/114] [material-ui] enhanceDensity: compose Switch dims to match default sizes Switch dims were mapped to single scale steps, shrinking it. Compose from steps so defaults land on today's px (medium 58/38/20/38/12, small 40/24/16/24/7) and still scale with density: width calc(xxl*2-6), height/touch calc(xxl+xs), thumb calc(lg+xxs), etc. touchSize == height keeps the thumb centered. --- .../mui-material/src/styles/enhanceDensity.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 1f50db894693a3..8f23735b918655 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -408,20 +408,20 @@ export default function enhanceDensity< root: [ c?.MuiSwitch?.styleOverrides?.root, { - // Switch maps its input dims to scale steps; pad/top/travel/radius - // re-derive from them, so the geometry stays valid (touchSize == height - // -> centered; width > touchSize -> positive travel). `xxl` covers the - // wider track. - '--Switch-medium-width': varRefs.xxl, - '--Switch-medium-height': varRefs.xl, - '--Switch-medium-touchSize': varRefs.xl, - '--Switch-medium-thumbSize': varRefs.lg, - '--Switch-medium-pad': varRefs.sm, - '--Switch-small-width': varRefs.xl, - '--Switch-small-height': varRefs.lg, - '--Switch-small-touchSize': varRefs.lg, - '--Switch-small-thumbSize': varRefs.md, - '--Switch-small-pad': varRefs.xs, + // Switch dims are composed from scale steps to land on today's sizes + // at the default scale, then track density proportionally. pad/top/ + // travel/radius re-derive, so the geometry stays valid (touchSize == + // height -> centered; width > touchSize -> positive travel). + '--Switch-medium-width': `calc(${varRefs.xxl} * 2 - 6px)`, // 58 + '--Switch-medium-height': `calc(${varRefs.xxl} + ${varRefs.xs})`, // 38 + '--Switch-medium-touchSize': `calc(${varRefs.xxl} + ${varRefs.xs})`, // 38 (= height) + '--Switch-medium-thumbSize': `calc(${varRefs.lg} + ${varRefs.xxs})`, // 20 + '--Switch-medium-pad': varRefs.md, // 12 + '--Switch-small-width': `calc(${varRefs.xxl} + ${varRefs.sm})`, // 40 + '--Switch-small-height': varRefs.xl, // 24 + '--Switch-small-touchSize': varRefs.xl, // 24 (= height) + '--Switch-small-thumbSize': varRefs.lg, // 16 + '--Switch-small-pad': `calc(${varRefs.sm} - 1px)`, // 7 }, ], }, From d1f3d0d892148b6f4f5ba7144df86b76082973ee Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 11:38:14 +0700 Subject: [PATCH 022/114] [material-ui] Fix density seams: OutlinedInput label, MenuItem icon, Tabs minHeight - enhanceDensity: derive OutlinedInput --InputLabel-y from density step (sibling label can't read the input's padBlock token); per-size via variants - MenuItem: consume --ListItemIcon-minWidth (was hardcoded 36) so density reaches the icon - Tabs: add --Tabs-minHeight base seam (parent can't read child --Tab-minHeight) + wire MuiTabs --- .../mui-material/src/MenuItem/MenuItem.js | 2 +- packages/mui-material/src/Tabs/Tabs.js | 5 ++- .../mui-material/src/styles/enhanceDensity.ts | 37 ++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 3e526ebf204d93..285f37e48d79d6 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -136,7 +136,7 @@ const MenuItemRoot = styled(ButtonBase, { paddingLeft: 36, }, [`& .${listItemIconClasses.root}`]: { - minWidth: 36, + minWidth: 'var(--ListItemIcon-minWidth, 36px)', }, variants: [ { diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js index 24ed5c387ebaf8..d897f445b512e3 100644 --- a/packages/mui-material/src/Tabs/Tabs.js +++ b/packages/mui-material/src/Tabs/Tabs.js @@ -75,7 +75,10 @@ const TabsRoot = styled('div', { })( memoTheme(({ theme }) => ({ overflow: 'hidden', - minHeight: 48, + // Density adapter (docs/adr/0001): base token, 48px literal fallback keeps the + // default pixel-identical. Tabs is the Tab's parent, so it can't read the + // child's `--Tab-minHeight` — it carries its own seam. + minHeight: 'var(--Tabs-minHeight, 48px)', // Add iOS momentum scrolling for iOS < 13.0 WebkitOverflowScrolling: 'touch', display: 'flex', diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 8f23735b918655..e9d49c9ccbd690 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -1,4 +1,5 @@ import { Theme } from './createTheme'; +import inputLabelClasses from '../InputLabel/inputLabelClasses'; /** * Named density steps, surfaced as `--mui-density-*` CSS vars. Components wired @@ -127,8 +128,8 @@ export default function enhanceDensity< root: [ c?.MuiChip?.styleOverrides?.root, { - '--Chip-small-height': varRefs.lg, - '--Chip-medium-height': varRefs.xl, + '--Chip-small-height': varRefs.xl, + '--Chip-medium-height': varRefs.xxl, '--Chip-small-padInline': varRefs.sm, '--Chip-medium-padInline': varRefs.md, }, @@ -273,6 +274,19 @@ export default function enhanceDensity< ], }, }, + MuiTabs: { + ...c?.MuiTabs, + styleOverrides: { + ...c?.MuiTabs?.styleOverrides, + root: [ + c?.MuiTabs?.styleOverrides?.root, + { + // Match Tab's minHeight step so the bar tracks the tabs it contains. + '--Tabs-minHeight': `calc(${varRefs.xl} + ${varRefs.lg})`, + }, + ], + }, + }, MuiTablePagination: { ...c?.MuiTablePagination, styleOverrides: { @@ -370,6 +384,25 @@ export default function enhanceDensity< '--OutlinedInput-small-padBlock': varRefs.sm, '--OutlinedInput-medium-padInline': varRefs.lg, '--OutlinedInput-small-padInline': varRefs.md, + // The outlined label resting-Y tracks the input's block padding, but + // the label is a preceding sibling — it can't read the input's + // `--OutlinedInput-*-padBlock` (custom props don't inherit sibling -> + // sibling). So derive `--InputLabel-y` straight from the density step + // (which the label DOES inherit from `:root`), matching the + // component's -0.5/+0.5 rounding per size. + [`.${inputLabelClasses.root}:has(~ &)`]: { + '--InputLabel-y': `calc(${varRefs.md} - 0.5px)`, + }, + variants: [ + { + props: { size: 'small' }, + style: { + [`.${inputLabelClasses.root}:has(~ &)`]: { + '--InputLabel-y': `calc(${varRefs.sm} + 0.5px)`, + }, + }, + }, + ], }, ], }, From 7c4b598dee5dc48a88b5613e39c52ec8c68f276d Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 11:38:23 +0700 Subject: [PATCH 023/114] [docs] Add density-showcase experiment; share demos with fixture - New /experiments/density-showcase: preset switcher (compact/normal/comfort), live scale readout + per-component token accordion, masonry gallery - Extract shared demos to densityDemos.tsx; fixture imports it - Fixture: --Tabs-minHeight scope, center row Stacks --- docs/pages/experiments/density-fixture.tsx | 633 +----------------- docs/pages/experiments/density-showcase.tsx | 258 ++++++++ docs/src/modules/components/densityDemos.tsx | 642 +++++++++++++++++++ 3 files changed, 907 insertions(+), 626 deletions(-) create mode 100644 docs/pages/experiments/density-showcase.tsx create mode 100644 docs/src/modules/components/densityDemos.tsx diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index f56b6018e639d6..3ccddaea7d40f8 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -2,638 +2,17 @@ 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 OutlinedInput from '@mui/material/OutlinedInput'; -import TextField from '@mui/material/TextField'; -import InputAdornment from '@mui/material/InputAdornment'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import Chip from '@mui/material/Chip'; -import Avatar from '@mui/material/Avatar'; -import IconButton from '@mui/material/IconButton'; -import MenuItem from '@mui/material/MenuItem'; -import MenuList from '@mui/material/MenuList'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListSubheader from '@mui/material/ListSubheader'; -import Toolbar from '@mui/material/Toolbar'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableRow from '@mui/material/TableRow'; -import TablePagination from '@mui/material/TablePagination'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardActions from '@mui/material/CardActions'; -import Select from '@mui/material/Select'; -import Breadcrumbs from '@mui/material/Breadcrumbs'; -import Link from '@mui/material/Link'; -import Badge from '@mui/material/Badge'; -import Checkbox from '@mui/material/Checkbox'; -import Radio from '@mui/material/Radio'; -import Switch from '@mui/material/Switch'; -import Typography from '@mui/material/Typography'; -import FaceIcon from '@mui/icons-material/Face'; -import DeleteIcon from '@mui/icons-material/Delete'; -import InboxIcon from '@mui/icons-material/Inbox'; -import DraftsIcon from '@mui/icons-material/Drafts'; -import FavoriteIcon from '@mui/icons-material/Favorite'; -import VisibilityIcon from '@mui/icons-material/Visibility'; import { createTheme, ThemeProvider } from '@mui/material/styles'; +import demos from 'docs/src/modules/components/densityDemos'; // Local verification fixture for the CSS-var density adapter (docs/adr/0001). // Used by scripts/density-screenshots. Renders one component's load-bearing -// matrix inside #density-scope; the harness sets `level` (default | dense | -// loose), which the scope translates into per-component density-token overrides. -// `level=default` sets no tokens, so the render must be pixel-identical to the -// pre-change baseline. Add a component's matrix to `demos` before verifying it. +// matrix (shared `demos`) inside #density-scope; the harness sets `level` +// (default | dense | loose), which the scope translates into per-component +// density-token overrides. `level=default` sets no tokens, so the render must be +// pixel-identical to the pre-change baseline. const theme = createTheme({ cssVariables: true }); -const demos: Record = { - Button: ( - - {(['small', 'medium', 'large'] as const).map((size) => ( - - - - - - ))} - - ), - OutlinedInput: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - @} - /> - - ))} - - ), - TextField: ( - - {(['medium', 'small'] as const).map((size) => ( - - {`outlined ${size}`} - - - ))} - - $, - endAdornment: kg, - }, - }} - /> - - - ), - Chip: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - A} /> - } /> - {}} /> - {}} - icon={} - /> - - ))} - - ), - IconButton: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - MenuItem: ( - // MenuItem requires a MenuList/Menu ancestor (MenuListContext). - - Default item - Selected item - - - - - With icon - - With divider - No gutters - Dense item - - - - - Dense + icon - - - Dense no gutters - - - ), - ListItem: ( - - - Default item - - - - } - > - With secondary action - - Divider item - - - - - - Dense item - - - - } - > - Dense with action - - Dense, no gutters - - - ), - ListItemButton: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - ListItemIcon: ( - - - - - - - - - - - - - - - - - - - - - - - - - ), - ListItemText: ( - - {([false, true] as const).map((dense) => ( - - - - - - - - - - - - - - - ))} - - ), - ListSubheader: ( - - - Gutters (default) - - - Inset - - - Disable gutters - - - Primary color - - - ), - Toolbar: ( - - {(['regular', 'dense'] as const).map((variant) => ( - - - {variant} gutters - action - - - {variant} no-gutters - action - - - ))} - - ), - Tab: ( - // Tab requires a Tabs ancestor (RovingTabIndexContext). - - {}}> - - - - - {}}> - } label="Top" iconPosition="top" value={0} /> - } label="Bottom" iconPosition="bottom" value={1} /> - } label="Start" iconPosition="start" value={2} /> - } label="End" iconPosition="end" value={3} /> - - {}}> - } aria-label="icon only" value={0} /> - - - - ), - Tabs: ( - - - - - - - - } label="Top" iconPosition="top" /> - } label="Bottom" iconPosition="bottom" /> - } label="Start" iconPosition="start" /> - } label="End" iconPosition="end" /> - - - } aria-label="fav" /> - } aria-label="del" /> - - - ), - TablePagination: ( - - - - - {}} - onRowsPerPageChange={() => {}} - /> - - - {}} - onRowsPerPageChange={() => {}} - /> - - - {}} - onRowsPerPageChange={() => {}} - /> - - -
-
- ), - CardContent: ( - - - - Default - All-sides padding via --CardContent-pad. - - - - - Last-child - - Extra bottom inset (--CardContent-padBottom) since this is the last child. - - - - - - Above actions - Not last child -> base pad only on the bottom. - - - - - - - ), - Select: ( - - - - - - - - ), - Breadcrumbs: ( - - - - Home - - - Catalog - - Shoes - - - - Home - - - Library - - - Data - - Reports - - - - Home - - - Catalog - - - Accessories - - Belts - - - ), - InputAdornment: ( - - {(['small', 'medium'] as const).map((size) => ( - - $ }, - }} - /> - - - - - - ), - }, - }} - /> - kg }, - }} - /> - kg }, - }} - /> - - ))} - - ), - Badge: ( - - - - - - - - - - - - - - - - - - ), - Checkbox: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - - ))} - - ), - Radio: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - ))} - - ), - Switch: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - ))} - - ), -}; - // Per-component density-token overrides for the review levels. `default` is // empty on purpose — that render is the pixel-identical regression gate. const scopes: Record> = { @@ -777,12 +156,14 @@ const scopes: Record> = { }, Tabs: { dense: { + ['--Tabs-minHeight' as any]: '32px', ['--Tab-padBlock' as any]: '4px', ['--Tab-padInline' as any]: '8px', ['--Tab-minHeight' as any]: '32px', ['--Tab-iconSpacing' as any]: '2px', }, loose: { + ['--Tabs-minHeight' as any]: '72px', ['--Tab-padBlock' as any]: '20px', ['--Tab-padInline' as any]: '32px', ['--Tab-minHeight' as any]: '72px', diff --git a/docs/pages/experiments/density-showcase.tsx b/docs/pages/experiments/density-showcase.tsx new file mode 100644 index 00000000000000..53f5df81fbcb07 --- /dev/null +++ b/docs/pages/experiments/density-showcase.tsx @@ -0,0 +1,258 @@ +'use client'; +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import CssBaseline from '@mui/material/CssBaseline'; +import { createTheme, ThemeProvider, enhanceDensity, DensityScale } from '@mui/material/styles'; +import demos from 'docs/src/modules/components/densityDemos'; + +// Client-facing showcase for the CSS-var density adapter (docs/adr/0001). +// Three presets each map to one `enhanceDensity(theme, { scale })` call — the +// only knob is the 7-step density scale. Flip a preset -> the whole gallery +// reflows because every component pulls its sized tokens from `--mui-density-*`. + +type PresetKey = 'compact' | 'normal' | 'comfort'; + +// `normal` = enhanceDensity defaults (theme.spacing) -> pixel-identical to today. +// compact/comfort override every step explicitly. +const presetScales: Record | undefined> = { + compact: { + xxs: '2px', + xs: '4px', + sm: '6px', + md: '8px', + lg: '12px', + xl: '18px', + xxl: '24px', + }, + normal: undefined, + comfort: { + xxs: '6px', + xs: '8px', + sm: '12px', + md: '16px', + lg: '24px', + xl: '32px', + xxl: '40px', + }, +}; + +const presetLabels: Record = { + compact: 'Compact', + normal: 'Normal', + comfort: 'Comfort', +}; + +const scaleKeys: (keyof DensityScale)[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; + +// Build the three enhanced themes once. +const baseTheme = createTheme({ cssVariables: true }); +const themes: Record> = { + compact: enhanceDensity(createTheme({ cssVariables: true }), { scale: presetScales.compact }), + normal: enhanceDensity(createTheme({ cssVariables: true })), + comfort: enhanceDensity(createTheme({ cssVariables: true }), { scale: presetScales.comfort }), +}; + +// Clean px readout for the scale. Under `cssVariables`, `theme.spacing()` returns +// a `calc(... var(--mui-spacing) ...)` string, so for the Normal preset we read +// the resolved px from a non-css-var theme instead. +const displayScales: Record = { + compact: presetScales.compact as DensityScale, + normal: enhanceDensity(createTheme()).density, + comfort: presetScales.comfort as DensityScale, +}; + +// Pull the density-var mappings enhanceDensity injected per component. Each +// component's `styleOverrides.root` is `[originalRoot, { '--Component-*': ... }]`; +// scan every plain-object element for the top-level `--*` token entries. +type VarMap = Record; +function collectComponentVars(theme: ReturnType): Record { + const out: Record = {}; + const components = (theme.components ?? {}) as Record; + Object.keys(components).forEach((name) => { + if (name === 'MuiCssBaseline') { + return; + } + const root = components[name]?.styleOverrides?.root; + const elements = Array.isArray(root) ? root : [root]; + const vars: VarMap = {}; + elements.forEach((el) => { + if (!el || typeof el !== 'object') { + return; + } + Object.keys(el).forEach((key) => { + if (key.startsWith('--')) { + vars[key] = String(el[key]); + } + }); + }); + if (Object.keys(vars).length > 0) { + out[name.replace(/^Mui/, '')] = vars; + } + }); + return out; +} + +const componentVarsByPreset: Record> = { + compact: collectComponentVars(themes.compact), + normal: collectComponentVars(themes.normal), + comfort: collectComponentVars(themes.comfort), +}; + +const mono = { + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', + fontSize: 12, +} as const; + +function ScalePanel({ preset }: { preset: PresetKey }) { + const scale = displayScales[preset]; + return ( +
+ + Density scale + + + {scaleKeys.map((key) => ( + + {`--mui-density-${key}`} + {scale[key]} + + ))} + +
+ ); +} + +function VarsPanel({ preset }: { preset: PresetKey }) { + const byComponent = componentVarsByPreset[preset]; + const names = Object.keys(byComponent); + return ( +
+ + Component tokens ({names.length}) + + + {names.map((name) => ( + + } sx={{ minHeight: 36, px: 0 }}> + {name} + + + + {Object.entries(byComponent[name]).map(([key, value]) => ( + + + {key} + + {': '} + + {value} + + + ))} + + + + ))} + +
+ ); +} + +export default function DensityShowcase() { + const [preset, setPreset] = React.useState('normal'); + + return ( + // Outer shell uses the base theme; the gallery + sidebar readouts use the + // enhanced theme so they reflect the active preset. + + + + + Density presets + + + One scale drives every component. Normal is pixel-identical to today. + + next && setPreset(next)} + sx={{ mb: 2 }} + > + {(Object.keys(presetLabels) as PresetKey[]).map((key) => ( + + {presetLabels[key]} + + ))} + + + + + + + + + + + {Object.keys(demos).map((name) => ( + + + {name} + + {demos[name]} + + ))} + + + + + + ); +} diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx new file mode 100644 index 00000000000000..5ab114a93be066 --- /dev/null +++ b/docs/src/modules/components/densityDemos.tsx @@ -0,0 +1,642 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Chip from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import ListSubheader from '@mui/material/ListSubheader'; +import Toolbar from '@mui/material/Toolbar'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TablePagination from '@mui/material/TablePagination'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import Select from '@mui/material/Select'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Badge from '@mui/material/Badge'; +import Checkbox from '@mui/material/Checkbox'; +import Radio from '@mui/material/Radio'; +import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import FaceIcon from '@mui/icons-material/Face'; +import DeleteIcon from '@mui/icons-material/Delete'; +import InboxIcon from '@mui/icons-material/Inbox'; +import DraftsIcon from '@mui/icons-material/Drafts'; +import FavoriteIcon from '@mui/icons-material/Favorite'; +import VisibilityIcon from '@mui/icons-material/Visibility'; + +// Shared density demo matrices for the CSS-var density adapter (docs/adr/0001). +// Consumed by both the screenshot fixture (density-fixture) and the client +// showcase (density-showcase). Each entry renders one component's load-bearing +// size/variant matrix. `level=default` (no token overrides) must stay +// pixel-identical to the pre-change baseline. +const demos: Record = { + Button: ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + + + + + ))} + + ), + OutlinedInput: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + @} + /> + + ))} + + ), + TextField: ( + + {(['medium', 'small'] as const).map((size) => ( + + {`outlined ${size}`} + + + ))} + + $, + endAdornment: kg, + }, + }} + /> + + + ), + Chip: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + A} /> + } /> + {}} /> + {}} + icon={} + /> + + ))} + + ), + IconButton: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + MenuItem: ( + // MenuItem requires a MenuList/Menu ancestor (MenuListContext). + + Default item + Selected item + + + + + With icon + + With divider + No gutters + Dense item + + + + + Dense + icon + + + Dense no gutters + + + ), + ListItem: ( + + + Default item + + + + } + > + With secondary action + + Divider item + + + + + + Dense item + + + + } + > + Dense with action + + Dense, no gutters + + + ), + ListItemButton: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + ListItemIcon: ( + + + + + + + + + + + + + + + + + + + + + + + + + ), + ListItemText: ( + + {([false, true] as const).map((dense) => ( + + + + + + + + + + + + + + + ))} + + ), + ListSubheader: ( + + + Gutters (default) + + + Inset + + + Disable gutters + + + Primary color + + + ), + Toolbar: ( + + {(['regular', 'dense'] as const).map((variant) => ( + + + {variant} gutters + action + + + {variant} no-gutters + action + + + ))} + + ), + Tab: ( + // Tab requires a Tabs ancestor (RovingTabIndexContext). + + {}}> + + + + + {}}> + } label="Top" iconPosition="top" value={0} /> + } label="Bottom" iconPosition="bottom" value={1} /> + } label="Start" iconPosition="start" value={2} /> + } label="End" iconPosition="end" value={3} /> + + {}}> + } aria-label="icon only" value={0} /> + + + + ), + Tabs: ( + + + + + + + + } label="Top" iconPosition="top" /> + } label="Bottom" iconPosition="bottom" /> + } label="Start" iconPosition="start" /> + } label="End" iconPosition="end" /> + + + } aria-label="fav" /> + } aria-label="del" /> + + + ), + TablePagination: ( + + + + + {}} + onRowsPerPageChange={() => {}} + /> + + + {}} + onRowsPerPageChange={() => {}} + /> + + + {}} + onRowsPerPageChange={() => {}} + /> + + +
+
+ ), + CardContent: ( + + + + Default + All-sides padding via --CardContent-pad. + + + + + Last-child + + Extra bottom inset (--CardContent-padBottom) since this is the last child. + + + + + + Above actions + Not last child -> base pad only on the bottom. + + + + + + + ), + Select: ( + + + + + + Age + + + + + + + ), + Breadcrumbs: ( + + + + Home + + + Catalog + + Shoes + + + + Home + + + Library + + + Data + + Reports + + + + Home + + + Catalog + + + Accessories + + Belts + + + ), + InputAdornment: ( + + {(['small', 'medium'] as const).map((size) => ( + + $ }, + }} + /> + + + + + + ), + }, + }} + /> + kg }, + }} + /> + kg }, + }} + /> + + ))} + + ), + Badge: ( + + + + + + + + + + + + + + + + + + ), + Checkbox: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + + ))} + + ), + Radio: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + ))} + + ), + Switch: ( + + {(['small', 'medium'] as const).map((size) => ( + + + + + + + + ))} + + ), +}; + +export default demos; From 95ab46b384d309a06c6a70a4b4b310ae17afb3fb Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 11:54:05 +0700 Subject: [PATCH 024/114] [material-ui] Chip: scale avatar/icon/deleteIcon with --Chip-height calc(var(--Chip-height) - inset) per size so they track density; insets reproduce today's medium/small sizes (pixel-identical default) --- packages/mui-material/src/Chip/Chip.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index e477ec00fb6508..ca3192f9809596 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -96,22 +96,27 @@ const ChipRoot = styled('div', { opacity: (theme.vars || theme).palette.action.disabledOpacity, pointerEvents: 'none', }, + // Density adapter (docs/adr/0001): avatar/icon/deleteIcon scale with the + // chip height. Each is `calc(var(--Chip-height) - inset)` where the inset + // reproduces today's medium size (height 32: avatar/icon 24, deleteIcon 22); + // the small variant overrides the inset for height 24. [`& .${chipClasses.avatar}`]: { marginLeft: 5, marginRight: -6, - width: 24, - height: 24, + width: 'calc(var(--Chip-height, var(--_height)) - 8px)', + height: 'calc(var(--Chip-height, var(--_height)) - 8px)', color: theme.vars ? theme.vars.palette.Chip.defaultAvatarColor : textColor, fontSize: theme.typography.pxToRem(12), }, [`& .${chipClasses.icon}`]: { marginLeft: 5, marginRight: -6, + fontSize: 'calc(var(--Chip-height, var(--_height)) - 8px)', }, [`& .${chipClasses.deleteIcon}`]: { WebkitTapHighlightColor: 'transparent', color: theme.alpha((theme.vars || theme).palette.text.primary, 0.26), - fontSize: 22, + fontSize: 'calc(var(--Chip-height, var(--_height)) - 10px)', cursor: 'pointer', margin: '0 5px 0 -6px', '&:hover': { @@ -158,17 +163,17 @@ const ChipRoot = styled('div', { [`& .${chipClasses.avatar}`]: { marginLeft: 4, marginRight: -4, - width: 18, - height: 18, + width: 'calc(var(--Chip-height, var(--_height)) - 6px)', + height: 'calc(var(--Chip-height, var(--_height)) - 6px)', fontSize: theme.typography.pxToRem(10), }, [`& .${chipClasses.icon}`]: { - fontSize: 18, + fontSize: 'calc(var(--Chip-height, var(--_height)) - 6px)', marginLeft: 4, marginRight: -4, }, [`& .${chipClasses.deleteIcon}`]: { - fontSize: 16, + fontSize: 'calc(var(--Chip-height, var(--_height)) - 8px)', marginRight: 4, marginLeft: -4, }, From 9ee251e90c37324a3c8a28fc109f68b5418cc7fa Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 8 Jun 2026 11:54:05 +0700 Subject: [PATCH 025/114] [docs] density demos: drop filled-variant inputs (no density support yet) --- docs/src/modules/components/densityDemos.tsx | 29 +------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 5ab114a93be066..865057f24dc78f 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -268,7 +268,7 @@ const demos: Record = { - + @@ -461,15 +461,6 @@ const demos: Record = { Ten Twenty - - - Age - - - + Button + + + + + + Vars mapping · {component} + + {!mappingEnabled && ( + + ⓘ pick a preset to enable steps + + )} + + {SIZES.map((size) => { + const key = `${size}Pad` as MappingKey; + const value = mapping[key]; + const { valid, error } = validateMapping(value); + const showError = mappingEnabled && !valid; + return ( + setField(key, event.target.value)} + slotProps={{ htmlInput: { 'data-mapping-field': key } }} + /> + ); + })} + + + +
+ + {/* CANVAS — wrapped in the density-enhanced theme. */} + + + + + {component} (color="primary") + + + {SIZES.map((size) => { + const key = `${size}Pad` as MappingKey; + const { valid } = validateMapping(mapping[key]); + // TO5/TO6: element-level token wins over the preset's styleOverride. + // At `unset` (or invalid input) emit NO token → falls back to the + // literal `--_pad` default (unset) or the preset's own mapping. + const sx = + mappingEnabled && valid + ? { [buttonVar(size)]: stepsToVar(mapping[key]) } + : undefined; + return ( + + + + {size} + + + + {VARIANTS.map((variant) => ( + + ))} + + + ); + })} + + + +
+
+ ); +} diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 865057f24dc78f..e1954d1f8b8d3f 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -124,7 +124,7 @@ const demos: Record = { ), IconButton: ( - + @@ -135,7 +135,7 @@ const demos: Record = { - + @@ -146,7 +146,7 @@ const demos: Record = { - + @@ -366,7 +366,7 @@ const demos: Record = { ), Tabs: ( - + diff --git a/packages/mui-material/src/Button/index.d.ts b/packages/mui-material/src/Button/index.d.ts index 8946132d8d8701..28b4fd9f743a12 100644 --- a/packages/mui-material/src/Button/index.d.ts +++ b/packages/mui-material/src/Button/index.d.ts @@ -3,3 +3,5 @@ export * from './Button'; export { default as buttonClasses } from './buttonClasses'; export * from './buttonClasses'; + +export { private_buttonVars } from './buttonVars'; diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index cda729dc9e7ca9..1ea1b94fc81219 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -9,7 +9,12 @@ export { CssThemeVariables, } from './createTheme'; export { default as enhanceHighContrast, HighContrastTokens } from './enhanceHighContrast'; -export { default as enhanceDensity, DensityScale, DensityOptions } from './enhanceDensity'; +export { + default as enhanceDensity, + DENSITY_PRESETS, + DensityScale, + DensityOptions, +} from './enhanceDensity'; export { default as adaptV4Theme, DeprecatedThemeOptions } from './adaptV4Theme'; export { Shadows } from './shadows'; export { ZIndex } from './zIndex'; diff --git a/packages/mui-material/src/styles/index.js b/packages/mui-material/src/styles/index.js index c78e02b9604d71..1986b193144490 100644 --- a/packages/mui-material/src/styles/index.js +++ b/packages/mui-material/src/styles/index.js @@ -26,7 +26,7 @@ export function experimental_sx() { } export { default as createTheme } from './createTheme'; export { default as enhanceHighContrast } from './enhanceHighContrast'; -export { default as enhanceDensity } from './enhanceDensity'; +export { default as enhanceDensity, DENSITY_PRESETS } from './enhanceDensity'; export { default as unstable_createMuiStrictModeTheme } from './createMuiStrictModeTheme'; export { default as createStyles } from './createStyles'; export { getUnit as unstable_getUnit, toUnitless as unstable_toUnitless } from './cssUtils'; From 649005ad935c54faef45d61b230836393962bab7 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Tue, 30 Jun 2026 23:29:29 +0700 Subject: [PATCH 033/114] [docs] density experiment: key-shortcut ergonomics (datalist, px legend, live preview) Single-key shorthand (1 key = all sides) and keys-only validation were already in place; add typeahead, active-preset px legend, and per-field live resolved preview (value -> var-string = px) via helperText. --- docs/pages/experiments/density-experiment.tsx | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-experiment.tsx index 3e8ea87adeff06..ef37cf6fc33ce9 100644 --- a/docs/pages/experiments/density-experiment.tsx +++ b/docs/pages/experiments/density-experiment.tsx @@ -66,12 +66,45 @@ function validateMapping(input: string): { valid: boolean; error: string | null return { valid: true, error: null }; } +// Active preset's 7-step scale in px (for the legend + live preview). +// compact/comfort = explicit; `normal` = spacing-derived (unit 8px = enhanceDensity default). +const NORMAL_MULTIPLIER: Record<(typeof SCALE_KEYS)[number], number> = { + xxs: 0.5, + xs: 0.75, + sm: 1, + md: 1.5, + lg: 2, + xl: 3, + xxl: 4, +}; +const SPACING_UNIT = 8; + +function presetScalePx(preset: Preset): Record | null { + if (preset === 'unset') { + return null; + } + if (preset === 'normal') { + return Object.fromEntries( + SCALE_KEYS.map((k) => [k, `${NORMAL_MULTIPLIER[k] * SPACING_UNIT}px`]), + ); + } + return (DENSITY_PRESETS[preset] ?? {}) as Record; +} + +// Resolved var string + px for a valid mapping value under the active scale — +// e.g. `md` → { varStr: 'var(--mui-density-md)', px: '8px' } (compact). +function resolvePreview(value: string, scalePx: Record | null) { + const tokens = value.trim().split(/\s+/).filter(Boolean); + return { varStr: stepsToVar(value), px: scalePx ? tokens.map((t) => scalePx[t]).join(' ') : '' }; +} + export default function DensityExperiment() { const [preset, setPreset] = React.useState('unset'); const [component] = React.useState('Button'); const [mapping, setMapping] = React.useState>(PREFILL); const mappingEnabled = preset !== 'unset'; + const scalePx = presetScalePx(preset); const canvasTheme = React.useMemo(() => { if (preset === 'unset') { @@ -153,12 +186,42 @@ export default function DensityExperiment() { ⓘ pick a preset to enable steps )} + {mappingEnabled && scalePx && ( + + {SCALE_KEYS.map((k) => `${k}=${scalePx[k]}`).join(' · ')} + + )} + {/* Single datalist shared by all fields — key typeahead. */} + + {SCALE_KEYS.map((k) => ( + + ))} + {SIZES.map((size) => { const key = `${size}Pad` as MappingKey; const value = mapping[key]; const { valid, error } = validateMapping(value); const showError = mappingEnabled && !valid; + const preview = resolvePreview(value, scalePx); + let helper = ' '; + if (showError) { + helper = error ?? ' '; + } else if (mappingEnabled && valid) { + helper = `${value.trim()} → ${preview.varStr} = ${preview.px}`; + } return ( setField(key, event.target.value)} - slotProps={{ htmlInput: { 'data-mapping-field': key } }} + slotProps={{ + htmlInput: { + 'data-mapping-field': key, + list: 'density-keys', + }, + }} /> ); })} From e9488e60c1a605d100ab5b305295222f725efbc3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 09:50:42 +0700 Subject: [PATCH 034/114] [docs] density experiment: enable component selector + All option Component dropdown enabled with 'All' (default, at top) + per-component options, driven by a COMPONENT_DEFS registry (Button only today; grows as more families are de-prefixed). 'All' renders every supported component's canvas + mapping group; specific selection filters to one. Mapping state keyed per component. --- docs/pages/experiments/density-experiment.tsx | 244 +++++++++++------- 1 file changed, 152 insertions(+), 92 deletions(-) diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-experiment.tsx index ef37cf6fc33ce9..8629589664070a 100644 --- a/docs/pages/experiments/density-experiment.tsx +++ b/docs/pages/experiments/density-experiment.tsx @@ -33,13 +33,6 @@ const PRESET_LABEL: Record = { comfort: 'comfort', }; -// Canonical Button prefill (matches enhanceDensity's own assignment). -const PREFILL: Record = { - smallPad: 'xxs sm', - mediumPad: 'xs lg', - largePad: 'sm xl', -}; - const buttonVar = (size: Size) => private_buttonVars[`${size}Pad` as MappingKey]; // Keys-only → density-var string. The validator guarantees each token ∈ SCALE_KEYS. @@ -98,13 +91,97 @@ function resolvePreview(value: string, scalePx: Record | null) { return { varStr: stepsToVar(value), px: scalePx ? tokens.map((t) => scalePx[t]).join(' ') : '' }; } +// --------------------------------------------------------------------------- +// Density-component registry. Only Button is de-prefixed/wired in this +// prototype; add entries here as more families gain a static `private_*Vars` +// map — the dropdown, canvas and mapping controls all iterate this registry. +// --------------------------------------------------------------------------- +interface DensityField { + key: string; // mapping-state key, e.g. 'smallPad' + cssVar: string; // e.g. '--Button-small-pad' +} +interface DensityComponentDef { + canvasLabel: string; + fields: DensityField[]; + prefill: Record; + renderMatrix: (args: { mapping: Record; mappingEnabled: boolean }) => React.ReactNode; +} + +function ButtonMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + return ( + + {SIZES.map((size) => { + const key = `${size}Pad`; + const { valid } = validateMapping(mapping[key] ?? ''); + // TO5/TO6: element-level token wins over the preset's styleOverride. + // At `unset` (or invalid input) emit NO token → falls back to the literal + // `--_pad` default (unset) or the preset's own mapping. + const sx = mappingEnabled && valid ? { [buttonVar(size)]: stepsToVar(mapping[key]) } : undefined; + return ( + + + + {size} + + + + {VARIANTS.map((variant) => ( + + ))} + + + ); + })} + + ); +} + +const COMPONENT_DEFS = { + Button: { + canvasLabel: 'Button (color="primary")', + // Canonical prefill matches enhanceDensity's own Button assignment. + fields: SIZES.map((size) => ({ key: `${size}Pad`, cssVar: buttonVar(size) })), + prefill: { smallPad: 'xxs sm', mediumPad: 'xs lg', largePad: 'sm xl' }, + renderMatrix: (args) => , + }, +} satisfies Record; + +type ComponentName = keyof typeof COMPONENT_DEFS; +type Selection = 'All' | ComponentName; + +const COMPONENTS = Object.keys(COMPONENT_DEFS) as ComponentName[]; + +const initialMapping = () => + Object.fromEntries(COMPONENTS.map((c) => [c, { ...COMPONENT_DEFS[c].prefill }])) as unknown as Record< + ComponentName, + Record + >; + export default function DensityExperiment() { const [preset, setPreset] = React.useState('unset'); - const [component] = React.useState('Button'); - const [mapping, setMapping] = React.useState>(PREFILL); + const [selection, setSelection] = React.useState('All'); + const [mapping, setMapping] = React.useState>>( + initialMapping, + ); const mappingEnabled = preset !== 'unset'; const scalePx = presetScalePx(preset); + const visibleComponents: ComponentName[] = selection === 'All' ? COMPONENTS : [selection]; const canvasTheme = React.useMemo(() => { if (preset === 'unset') { @@ -115,10 +192,10 @@ export default function DensityExperiment() { }); }, [preset]); - const setField = (key: MappingKey, value: string) => - setMapping((m) => ({ ...m, [key]: value })); + const setField = (comp: ComponentName, key: string, value: string) => + setMapping((m) => ({ ...m, [comp]: { ...m[comp], [key]: value } })); - const resetMapping = () => setMapping(PREFILL); + const resetMapping = () => setMapping(initialMapping()); return ( @@ -172,14 +249,24 @@ export default function DensityExperiment() { Component - setSelection(event.target.value as Selection)} + slotProps={{ input: { 'data-component-select': true } as Record }} + > + All + {COMPONENTS.map((c) => ( + + {c} + + ))} - Vars mapping · {component} + Vars mapping {!mappingEnabled && ( @@ -198,56 +285,58 @@ export default function DensityExperiment() { )} {/* Single datalist shared by all fields — key typeahead. */} - + {SCALE_KEYS.map((k) => ( ))} - - {SIZES.map((size) => { - const key = `${size}Pad` as MappingKey; - const value = mapping[key]; - const { valid, error } = validateMapping(value); - const showError = mappingEnabled && !valid; - const preview = resolvePreview(value, scalePx); - let helper = ' '; - if (showError) { - helper = error ?? ' '; - } else if (mappingEnabled && valid) { - helper = `${value.trim()} → ${preview.varStr} = ${preview.px}`; - } - return ( - setField(key, event.target.value)} - slotProps={{ - htmlInput: { - 'data-mapping-field': key, - list: 'density-keys', - }, - }} - /> - ); - })} - + {visibleComponents.map((comp) => ( + + + {comp} + + + {COMPONENT_DEFS[comp].fields.map((field) => { + const value = mapping[comp][field.key] ?? ''; + const { valid, error } = validateMapping(value); + const showError = mappingEnabled && !valid; + const preview = resolvePreview(value, scalePx); + let helper = ' '; + if (showError) { + helper = error ?? ' '; + } else if (mappingEnabled && valid) { + helper = `${value.trim()} → ${preview.varStr} = ${preview.px}`; + } + return ( + setField(comp, field.key, event.target.value)} + slotProps={{ + htmlInput: { + 'data-mapping-field': `${comp}-${field.key}`, + list: 'density-keys', + }, + }} + /> + ); + })} + + + ))} @@ -258,44 +347,15 @@ export default function DensityExperiment() { - - {component} (color="primary") - - - {SIZES.map((size) => { - const key = `${size}Pad` as MappingKey; - const { valid } = validateMapping(mapping[key]); - // TO5/TO6: element-level token wins over the preset's styleOverride. - // At `unset` (or invalid input) emit NO token → falls back to the - // literal `--_pad` default (unset) or the preset's own mapping. - const sx = - mappingEnabled && valid - ? { [buttonVar(size)]: stepsToVar(mapping[key]) } - : undefined; - return ( - - - - {size} - - - - {VARIANTS.map((variant) => ( - - ))} - - - ); - })} + + {visibleComponents.map((comp) => ( + + + {COMPONENT_DEFS[comp].canvasLabel} + + {COMPONENT_DEFS[comp].renderMatrix({ mapping: mapping[comp], mappingEnabled })} + + ))} From a633ba9265ce238b3e590b458896f06bc5e97f22 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 09:53:04 +0700 Subject: [PATCH 035/114] [docs] density experiment: mapping helper shows resolved px only Drop the verbose ' -> = ' preview; show just the computed px (e.g. '4px 12px'). --- docs/pages/experiments/density-experiment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-experiment.tsx index 8629589664070a..8ac75355950030 100644 --- a/docs/pages/experiments/density-experiment.tsx +++ b/docs/pages/experiments/density-experiment.tsx @@ -307,7 +307,7 @@ export default function DensityExperiment() { if (showError) { helper = error ?? ' '; } else if (mappingEnabled && valid) { - helper = `${value.trim()} → ${preview.varStr} = ${preview.px}`; + helper = preview.px; } return ( Date: Wed, 1 Jul 2026 10:12:32 +0700 Subject: [PATCH 036/114] [docs] density experiment: plain text mapping inputs (drop datalist) Remove the + list attr so fields render as plain inputs (no native autocomplete caret). Legend + validation + resolved-px helper still guide input. --- docs/pages/experiments/density-experiment.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-experiment.tsx index 8ac75355950030..a7c6aa7a980639 100644 --- a/docs/pages/experiments/density-experiment.tsx +++ b/docs/pages/experiments/density-experiment.tsx @@ -284,14 +284,6 @@ export default function DensityExperiment() { {SCALE_KEYS.map((k) => `${k}=${scalePx[k]}`).join(' · ')} )} - {/* Single datalist shared by all fields — key typeahead. */} - - {SCALE_KEYS.map((k) => ( - - ))} - {visibleComponents.map((comp) => ( @@ -322,7 +314,6 @@ export default function DensityExperiment() { slotProps={{ htmlInput: { 'data-mapping-field': `${comp}-${field.key}`, - list: 'density-keys', }, }} /> From cde0d25217c871dbae29f1a3179450b4f484da33 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 10:39:02 +0700 Subject: [PATCH 037/114] [docs] remove ADRs + CONTEXT.md from prototype PR Drop the density ADRs (0001, 0003, density-adapter-rollout) and CONTEXT.md; they net out of the PR diff (all were added within this branch). --- CONTEXT.md | 187 --------------- docs/adr/0001-css-var-density-adapter.md | 286 ----------------------- docs/adr/0003-density-var-prefix.md | 129 ---------- docs/adr/density-adapter-rollout.md | 277 ---------------------- 4 files changed, 879 deletions(-) delete mode 100644 CONTEXT.md delete mode 100644 docs/adr/0001-css-var-density-adapter.md delete mode 100644 docs/adr/0003-density-var-prefix.md delete mode 100644 docs/adr/density-adapter-rollout.md diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index bc39ab667dcc32..00000000000000 --- a/CONTEXT.md +++ /dev/null @@ -1,187 +0,0 @@ -# Density (CSS-var adapter) - -How Material UI component dimensions (padding / gap / height) are exposed as -hand-authorable CSS variables so a designer can tune component density — per -component, per size, or holistically — without touching component source, doing -`calc` arithmetic, or riding the single `--mui-spacing` dial. - -This is the **adapter** sibling of the earlier `--mui-spacing`-derived -experiment (`feat/components-theme-spacing`): instead of one global dial, each -dimension is an overridable token whose default is a literal px. - -## Layers - -The component is read as **three layers of responsibility**, each owning a slice -of one cascade: - -- **Agnostic** — the styled root, no design meaning (no size/variant/color). Its - spacing surface is one public var it consumes directly, falling back to the - internal default (`padding: var(--Button-pad, var(--_pad))`). -- **Material UI** — Material Design's sizes/variants, all in `variants`: the - `(variant, size)` literal defaults (`--_pad`) and the sized-token routing - (`--Button-pad`) for built-in sizes. A custom size routes inline instead (it - needs the runtime size string). -- **Design system** — tunes the public **sized tokens** the Material UI layer - routes over its default (wired via `enhanceDensity`). - -## Language - -**Agnostic var** (public, the layer-1 surface): -The single per-property variable the styled root consumes, shape -`--Component-` — PascalCase component, short semantic key (`pad`, `gap`), -unprefixed (for example `--Button-pad`). Matches the existing component-var -convention (`--AppBar-background`). The root reads it with the internal default -behind it (`var(--Button-pad, var(--_pad))`); the Material UI layer **sets it in -a per-size `variants` block** to the sized-token routing (inline for custom -sizes). A designer tunes it through the sized token, not by setting it directly. -_Avoid_: literal CSS-property keys (`--Button-padding`), kebab keys, `--mui-`-prefixed component vars, "variable". - -**Sized token** (public, the design-system knob): -A size-scoped override, shape `--Component--` -(for example `--Button-small-pad`). Reflows only that one size. The Material UI -layer routes it over the internal default; when set at any scope it wins. -For a **size-varying** axis, resolution is **sized-only** — no all-sizes base token. -_Avoid_: "size variant token". - -**Base token** (public, only when per-size override is meaningless): -An axis can skip the **size layer** — internal default `--_` + seam, -consumed `var(--Component-, var(--_))`, nothing routes it, so a designer -sets the seam directly. Use this **only when tuning the axis per size makes no -sense**, because a base token can't be size-scoped from the theme. A -size-invariant _default_ is **not** enough: OutlinedInput's inline gutter is -`14px` for both sizes, yet it's a **sized token** (`--OutlinedInput--padInline`, -default `14px` each) so a design system can make small inputs denser inline. -Reach for a base token rarely; default to a **sized token**. -_Avoid_: a base token just because the default is size-invariant; a bare literal -default (use `--_`). - -**State token** (public, for a boolean compactness toggle like `dense`): -When the compactness axis is a **boolean** prop (`dense`) rather than a `size` -enum, the **default (off) state** is exposed through the plain seam -`--Component-` — consumed `var(--Component-, var(--_))`, **nothing -routes it in the base** (the seam _is_ the default knob, base-token-shaped) — and -**only the on state** gets a qualified token `--Component-dense-`, routed in -the `dense` variant over its own `--_` literal. A boolean has no name for -"off", so there is **no** `--Component-normal/regular/default-`: the absence -of the toggle is the plain seam, not an arbitrarily-named size. Contrast a -**sized token**, where every value (including `medium`) is qualified because each -is a real named size. Used by MenuItem, ListItem, ListItemButton, ListItemText. -_Avoid_: naming the off state (`-normal-`, `-regular-`, `-default-`); routing the -seam in the base; treating `dense` as a 2-value size enum. - -**Internal default**: -A private variable, shape `--_` (leading underscore, **no component -prefix**), **set in `variants`** per `(variant, size)` cell (medium defaults -reuse the `{ variant }` blocks), over a **universal default on the root** so a -custom variant/size still renders. It holds the Material default — today's exact -px for that cell. No prefix is needed because every cell that reads it also -sets it on the same element (Button; OutlinedInput's input and root each declare -their own) — so an ancestor's value never wins over a component's own. Lowest priority, so any sized token or -plain `styleOverrides` property still wins. -_Avoid_: exposing it as API, prefixing with the component name, "private token". - -**Token fallback**: -The literal px the internal default carries — today's exact value for that -`(variant, size)` cell. Makes the default render pixel-identical and bundle-light, -at the cost that the single `--mui-spacing` dial no longer reflows the component. -_Avoid_: "default value", "initial". - -**Density scale** (tier-1): -A named, ordered set of density steps (`xxs / xs / sm / md / lg …`), values -derived from `theme.spacing`, surfaced as `--mui-density-*` CSS vars. The shared, -designer-facing holistic-density surface. **Emitted by `enhanceDensity`, not -`createTheme`** (runtime opt-in); its **types ship built-in** -(`theme.vars.density.*` always type-checks). -_Avoid_: "spacing scale" (that is `theme.spacing`), "grid". - -**enhanceDensity**: -A single post-`createTheme` function (mirroring `enhanceHighContrast`) that does -**both**: (a) emits the **density scale** as `--mui-density-*` and populates -`theme.vars.density`, and (b) injects per-component `styleOverrides.root` mapping -**sized tokens** to density steps -(`--Button-medium-pad: theme.vars.density.md`). `createTheme` is untouched. -Opt-in: without it, components render their literal-px defaults; with it, tuning -the density scale (or scoping `--mui-density-*`) reflows every wired component. -_Avoid_: "density preset" (that is the resulting effect, not the function). - -## Relationships - -- The styled root reads **one** agnostic var per property; **no JavaScript - conditional** lives in the styles implementation. The `(variant, size)` → px - matrix and the built-in-size routing are both **`variants` cells**, not a body - lookup table; only custom-size routing is inline. -- **Two vars, not one** (`--Button-pad` over `--_pad`): the cells write the - _value_ (`--_pad`), the routing writes a _reference_ (`--Button-pad`). One var - fails three ways — a self-referencing fallback in the inline bridge (invalid - CSS, forcing the literal back into runtime style), the `(variant×size)` and - size-only write-axes clobbering on one element, and losing the agnostic seam. - Full reasoning in `docs/adr/0001` → _Why two vars_. -- Override priority (high → low): plain `styleOverrides` property → **sized - token** → internal default (literal fallback). -- Custom (user-defined) sizes work for free: when the size isn't built-in, the - inline routing builds the sized-token name from the runtime size string; the - design system supplies the value via that token. -- **Token granularity follows the component's spacing structure; split only when - the impl forces it.** Button sets all sides together via one shorthand on one - element → one `pad` var (even though block 6 ≠ inline 8 — differing values alone - don't force a split). OutlinedInput _is_ forced: block vs inline land on - different elements/states and zero per adornment. **Both axes are sized** - (`--OutlinedInput--padBlock`/`-padInline`) — block defaults vary by size - (16.5/8.5), inline defaults don't (14 both) but it's sized anyway so density can - tune it per size. Its padding spans two elements (input when inline, root when - multiline/adorned — never both on a side at once), so each site tokenizes its - own literal in place rather than lifting size resolution to one owner; smallest - diff from master. -- **Cross-component coordination respects dependency direction.** The outlined - floating label must track the input's `padBlock`, but `InputLabel` is generic - (shared by all input variants) so it only exposes a seam (`--InputLabel-y`, - literal default). The **specific** component, `OutlinedInput`, owns the bridge: - it reaches its preceding-sibling label via `:has(~ &)` and sets `--InputLabel-y` - from its public token. Generic never names specific; one knob still drives both. -- **A shared internal base owns the agnostic layer; consumers route their own - tokens into it.** `SwitchBase` consumes the seam once - (`var(--SwitchBase-pad, var(--_pad))`); `Checkbox`/`Radio`/`Switch` (the Material - layer) each route a per-component sized token (`--Checkbox--pad`, …) into - that shared seam, staying independently tunable. The seam keeps the base's name - (plumbing); the knob is the per-component token. Delivery rides **custom-property - inheritance**, no descendant selector: where the consumer _is_ the base - (`styled(SwitchBase)`) it sets the seam on its own root; where it _wraps_ the - base (the Switch thumb), the wrapper root sets the seam and the base inherits it - (the base doesn't redeclare the seam). Caveat: the base _does_ redeclare - `--_` (what makes it unprefixed-safe), so an inherited `--_` is - shadowed — a wrapper needing a different per-state default feeds it through the - seam (set `--_` on the wrapper), not by inheriting `--_`. -- **enhanceDensity** (opt-in) connects tier-2 component tokens to the tier-1 - **density scale**; un-enhanced, the literal fallbacks reproduce today's pixels. -- This experiment does **not** ride `--mui-spacing`; holistic density comes from - the density scale, not that dial. - -## Example dialogue - -> **Dev:** "How do I shrink the padding of small buttons?" -> **Domain expert:** "Set the **sized token** `--Button-small-pad` at any scope; -> the Material UI layer routes it over its default, so every small button -> reflows. Resolution is sized-only — there's no all-sizes base token, so do it -> per size." -> **Dev:** "And with nothing set?" -> **Domain expert:** "The agnostic `--Button-pad` falls back to the **internal -> default** `--_pad` — the literal px set in the `(variant, size)` `variants` -> cell, pixel-identical to today. The `--mui-spacing` dial does nothing here; for -> holistic density you run **enhanceDensity** and tune the **density scale**." - -## Flagged ambiguities - -- "spacing token" meant both a `theme.spacing` key and a per-component value — - resolved: `theme.spacing` is untouched; per-component vars are the **agnostic - var** (layer-1 surface) and **sized tokens** (the design-system knob). -- "spacing scale" (earlier draft, tier-1) — renamed **density scale** and moved - to `theme.density`, to disambiguate from `theme.spacing`. -- Base (all-sizes-over-sized) token — dropped for **size-varying** axes; - resolution is sized-only, tune per size. A base token applies only when per-size - override is meaningless — _not_ merely when the default is size-invariant - (OutlinedInput's `14px` inline gutter is still a **sized** token so density can - tune it per size). -- Var key — single `pad` shorthand only when the impl sets all sides together on - one element (Button); split per axis (`padBlock`/`padInline`) when forced — - axes on different elements/states or different shapes (OutlinedInput). Sides are - symmetric within an axis, so `padding: ` stays RTL-safe. diff --git a/docs/adr/0001-css-var-density-adapter.md b/docs/adr/0001-css-var-density-adapter.md deleted file mode 100644 index b9c6eba479d79a..00000000000000 --- a/docs/adr/0001-css-var-density-adapter.md +++ /dev/null @@ -1,286 +0,0 @@ -# Component density via a CSS-var adapter, resolved in variants - -Component dimensions are exposed as public CSS variables with **literal-px -fallbacks**, resolved through an **internal var whose value lives in `variants`** -(inline style bridges only the custom-size case), instead of riding the single -`--mui-spacing` dial (`feat/components-theme-spacing`) or emitting a static -per-(variant, size) token matrix (`poc/css-vars-map`). - -## Context - -We want designers to tune density — per component, per size, or holistically — -without editing component source, writing `calc`, or accepting that every -dimension reflows off one global `--mui-spacing` value. - -Constraints that shaped the design: - -- **Pixel-identical default.** The un-configured theme must render today's exact - px for every `(variant, size)` cell (Argos zero-diff). -- **Literal defaults in `variants`, not the body.** The `(variant, size)` → - px matrix uses the idiomatic `variants` mechanism (Button already has these - cells for `fontSize`); no JS lookup table lives in the component body. -- **Support user-provided sizes.** A custom `size` added via the theme must get - the same tunability as built-in sizes — built-in routing is per-size `variants` - blocks; a custom size falls back to inline routing (dynamic size string). -- **No JavaScript conditionals in the styles implementation.** The `styled()` body must - not branch on `ownerState.size`/`variant` to pick a value. -- **Non-breaking.** Existing variant/size padding and existing - `styleOverrides`/`sx` overrides must keep working unchanged. - -## Decision - -The component is read as **three layers of responsibility**, each owning a -distinct slice of the same cascade: - -1. **Agnostic** — the styled root with no design meaning (no size/variant/color). - Its whole spacing surface is one public var it consumes directly, falling back - to the internal default: `padding: var(--Button-pad, var(--_pad))`. -2. **Material UI** — Material Design's sizes/variants, all in `variants`: the - `(variant, size)` literal **defaults** (`--_pad`) and the **sized-token - routing** for the built-in sizes (`--Button-pad`). No JS lookup in the body. - Custom (non-built-in) sizes route inline instead — the one case that needs the - runtime size string. -3. **Design system** — overrides through the public **sized token** the Material - UI layer routes over its default (driven by `enhanceDensity`). - -Per property, the chain (inline padding on Button): - -- **Agnostic var** `--Button-pad` (public) — the styled root's only spacing - consumption point; **set by a built-in-size `variants` block** to the - sized-token routing (inline for custom sizes), falling back to `--_pad`. -- **Sized token** `--Button--pad` (public) — the design-system knob; - reflows one size. -- **Internal default** `--_pad` (private, leading underscore) — the Material - default, **set in `variants`** per `(variant, size)`, with a universal default - on the root so a custom variant/size still renders a sane value. - -Resolution for a **size-varying** axis is **sized-only** (no all-sizes base -token): the sized token wins, else the Material default. An axis whose default is -the same every size can still be **sized** — and usually should be, so a design -system can tune it per size (density). A plain **base token** `--Component-` -over an internal default `--_` (consumed `var(--Component-, var(--_))`, -no size layer/routing) is reserved for the rare axis where per-size override is -genuinely meaningless. - -When the compactness axis is a **boolean** prop (`dense`) rather than a `size` -enum, use a **state token**: the default (off) state is the plain seam -`--Component-` (base-token-shaped — nothing routes it in the base), and only -the on state is qualified `--Component-dense-`, routed in the `dense` -variant. A boolean has no name for "off", so there is no -`--Component-normal/regular/default-` — unlike a size enum, where every value -(including `medium`) is qualified because each is a real named size. Used by -MenuItem, ListItem, ListItemButton, and ListItemText. - -The styled root has **one** consumption point per property and **no conditional**; -the defaults and built-in-size routing are plain `variants` entries: - -```js -const ButtonRoot = styled(ButtonBase)({ - '--_pad': '6px 16px', // universal default (today's root padding) - padding: 'var(--Button-pad, var(--_pad))', // agnostic layer - variants: [ - // routing for built-in sizes (deduped CSS) - { - props: { size: 'small' }, - style: { '--Button-pad': 'var(--Button-small-pad, var(--_pad))' }, - }, - // literal default per (variant, size); medium lives in the { variant } blocks (DRY) - { props: { variant: 'text', size: 'small' }, style: { '--_pad': '4px 5px' } }, - ], -}); -``` - -Only **custom sizes** route inline — the one case needing the runtime size -string (so custom sizes stay tunable without registering a variant): - -```js -const buttonSizes = ['small', 'medium', 'large']; -const densityVars = buttonSizes.includes(size) - ? undefined // built-in: routed via variants above - : { '--Button-pad': `var(--Button-${size}-pad, var(--_pad))` }; -; -``` - -### Why two vars (`--Button-pad` and `--_pad`), not one - -Three reasons, all pointing the same way: - -1. **Values belong in `variants`; the inline bridge must stay value-free.** The - literal px is a design decision — it must live in `variants`, co-located with - the rest of the variant's styling, statically deduped, smallest diff from - today. Inline style is only a **bridge** for the one dynamic case (a custom - size's token name) and must carry **no values**, only routing. Two vars allow - that: cells write the value (`--_pad`), the bridge writes a _reference_ - (`--Button-pad: var(--Button--pad, var(--_pad))`). With a single - `--_pad`, the custom-size bridge would have to write - `--_pad: var(--Button--pad, var(--_pad))` — a property referencing - **itself**, which CSS treats as guaranteed-invalid. The only escape is - embedding the literal back into the inline string, dragging the value into - runtime style — exactly what we moved out. -2. **Two write-axes on one element clobber if they share a name.** The literal - varies by **(variant × size)** (the cells); the token interception varies by - **size only** (`--Button--pad`, the routing). A single `--_pad` keeps - exactly one: routing-wins loses the per-variant literal (and the size block - can't supply the right fallback — it doesn't know the variant); literal-wins - never consults the token, so no override. Two names let each axis write - independently; `--Button-pad` chains to `--_pad`, the root reads `--Button-pad`. -3. **Layer seam (naming).** `--Button-pad` is public-shaped because it's the - **agnostic-layer seam** — the var a no-design consumer of the bare root would - set; Material UI takes it over to inject token routing. `--_pad` is private: - Material UI's internal default, not a contract. - -Holistic density is a separate, opt-in layer driven by a **single** -`enhanceDensity(theme)` function (mirroring `enhanceHighContrast`) that does -both jobs: it **emits** the density scale as `--mui-density-*` (and populates -`theme.vars.density`), and **maps** sized tokens to density steps via injected -`styleOverrides.root` (`--Button-medium-pad: var(--mui-density-md)`). -`createTheme` is left untouched. Types for `theme.vars.density` ship built-in; -the vars exist at runtime only after `enhanceDensity` runs. - -We considered making `density` a first-class `createTheme` node so the normal -css-var generator emits the vars. That is more "correct" (the vars participate -in the standard generation and can be re-scoped at any level), but it requires -`createTheme`/css-vars surgery. For an experiment we chose the self-contained -function: easy to A/B, easy to delete, no core change. The cost is that -post-hoc-emitted vars live outside the standard `theme.vars` pipeline. - -Scope: **Button** and the **outlined input family** (OutlinedInput, with -InputBase/TextField to follow) for this experiment. - -### OutlinedInput specifics - -Same three-tier model, with two component-driven differences: - -- **Both axes are sized.** Block (`16.5px`→`8.5px`) varies by size → sized token - `--OutlinedInput--padBlock`. The `14px` inline gutter is _constant_ across - sizes, but it's **sized too** → `--OutlinedInput--padInline` (default - `14px` each size) so a design system can make small inputs denser inline. (We - first modeled inline as a single size-invariant **base token**, but that can't - be size-scoped from the theme — a flaw for density — so we promoted it; a base - token is now reserved for axes where per-size override is meaningless.) Block and - inline are still split because the impl applies them separately — different - elements/states, per-adornment side-zeroing. Each axis is routed per size **in - place** on the element/variant that consumes it (input + root cells), so sizing - inline adds a `&& size === 'small'` re-route beside each size-agnostic adornment - variant — no lift, both axes wired identically. (Filled/Standard have asymmetric - block padding — `4/5`, `25/8` — so a shared InputBase block seam would need a - richer, per-side shape; deferred.) -- **Two consuming elements, tokenized in place.** Padding lives on the input - (non-multiline) _and_ the root (multiline) — and the two never both apply block - padding at once (multiline zeroes the input's). Rather than lift size resolution - to a single owner, **each site keeps master's literal-bearing cell and - tokenizes in place**: input base + input `{ size: small }`, root `multiline` + - root `{ multiline && small }`, each declaring its own `--_padBlock` and routing - the size token. This keeps the smallest diff from master (no restructuring, no - inheritance reliance, no dropped variants); the minor cost is the size routing - written twice (input vs root-multiline), which is honest — they are genuinely - separate code paths. Unprefixed `--_padBlock` stays safe because every cell that - reads it also sets it on the same element. - -**Closing the loop — the floating label.** In a `TextField`, `InputLabel` is a -_preceding sibling_ of the input. The resting label must track the block padding -or it decenters when density is tuned. True centering is `labelY = padBlock` -exactly (`(lineHeight + 2·padBlock)/2 − lineHeight/2`); today's `16px`/`9px` are -that with ±0.5px historical rounding. - -The bridge must respect the **dependency direction**: `InputLabel` is generic -(shared by outlined/filled/standard) and must not name a specific input's token. -So `InputLabel` only exposes a seam — its outlined resting transform reads -`var(--InputLabel-y, )` — and **OutlinedInput owns the bridge**. Because -the label precedes the input, OutlinedInput reaches it with `:has` (sibling -combinators only match _following_ siblings) and, per size, derives the label -seam straight from its public sized token (a cross-element rule must reference the -public token, not the input's internal `--_padBlock`): - -```js -// InputLabel — generic seam, literal default -transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)' // small: 9px - -// OutlinedInputRoot — base (medium) + size:small variant -[`.${inputLabelClasses.root}:has(~ &)`]: { - '--InputLabel-y': 'calc(var(--OutlinedInput-medium-padBlock, 16.5px) - 0.5px)', // small: small token + 0.5px = 9px -}, -``` - -Defaults compute to exactly `16px`/`9px` (Argos zero-diff); setting -`--OutlinedInput--padBlock` reflows input and label together — single knob, -no FormControl, no `enhanceDensity`. The **shrunk** label (`-9px`, in the notch on -the top border) is padding-independent and stays literal. InputBase needs no -change — OutlinedInput fully overrides its padding. - -Why `:has` and not the alternatives: putting the calc in `InputLabel` would make -a generic component name a sibling's token (wrong direction); a flat-scope -`--InputLabel-y` can't be size-specific for mixed-size pages; routing through -`enhanceDensity` defers single-knob to the design-system layer. The `:has` rule -keeps the coupling in the one component that legitimately owns it. Cost: needs -`:has()` (Chrome 105 / Safari 15.4 / Firefox 121). - -### Shared internal base (SwitchBase → Checkbox, Radio, Switch) - -When several components share one styled base, the **agnostic layer lives on the -base**: `SwitchBase` consumes the seam once (`padding: var(--SwitchBase-pad, -var(--_pad))`, `--_pad: 9px`), and each consumer — the **Material layer** — routes -its own per-component public token into the shared seam. The seam keeps the -_base's_ name (`--SwitchBase-pad`, plumbing); the designer-facing knob is the -per-component sized token (`--Checkbox--pad`, `--Radio--pad`, -`--Switch--pad`). Each consumer stays independently tunable while the base -holds the one consumption point. - -Two reader topologies, both relying on **custom-property inheritance** (no -descendant selector, no added specificity): - -- **Consumer is the base.** `Checkbox`/`Radio` are `styled(SwitchBase)`, so their - size variants set `--SwitchBase-pad` on the very element that consumes it. -- **Consumer wraps the base.** The `Switch` thumb is a `SwitchBase` _inside_ - `SwitchRoot`; the root sets `--SwitchBase-pad` and the thumb **inherits** it. - This works precisely because `SwitchBase` does not redeclare the seam. - -The inheritance caveat is the mirror of why `--_` is safe unprefixed: the -base **redeclares `--_`** on itself, so an inherited `--_` is shadowed. -The seam inherits (not redeclared); the internal default does not. So a wrapper -that needs a value different from the base's feeds it **through the seam** — set -the seam directly on the wrapper (preferred), not the shadowed `--_`. Switch -does exactly this: it sets `--SwitchBase-pad` to a derived `calc` (below). - -**Interlocked geometry — derive, don't tokenize one axis.** A `Switch`'s width, -height, thumb, touch target and travel all move together; tokenizing the thumb -pad alone drifts the thumb off the track. So Switch tokenizes its real dims per -size (`--Switch--width/height/thumbSize/touchSize` + the track gutter -`--Switch--pad`) and **derives** the coupled values with `calc`, feeding the -shared seam: SwitchBase pad -`= (touchSize − thumbSize) / 2`; the absolutely-positioned button stays centered -via `top = (height − touchSize) / 2` and checked `transform: translateX(width − -touchSize)`; the thumb slot reads `thumbSize`. Defaults (`touchSize == height`) -compute to today's `9/4` pad, `0` top, `20/16` travel — pixel-identical. -`enhanceDensity` _can_ wire Switch precisely because it derives: it maps the input -dims to scale steps (the `xxl` step covers the wider track) and pad/top/travel/ -radius re-derive, so the geometry stays valid (`touchSize == height` keeps it -centered, `width > touchSize` keeps travel positive). - -## Consequences - -- **Pixel-identical default & non-breaking.** Literals come from the `variants` - cells (`--_pad`) over a universal root default, so a custom variant/size still - renders; public tokens, `styleOverrides`, and `sx` all still win via the cascade. -- **No inline for built-in sizes.** Routing for `small`/`medium`/`large` lives in - `variants` (deduped CSS), so the common case carries no per-instance `style` - attr. Only a **custom size** routes inline. -- **Custom sizes work for free** — the inline routing builds the sized-token name - from the runtime size string; the design system supplies the value via that - token, no variant registration needed. -- **No `--mui-spacing` reflow.** Components opt out of the global dial; holistic - density flows through the density scale + `enhanceDensity` instead. -- **calc resolves only in a real browser** (jsdom does not), so density - assertions belong in browser/visual tests, not jsdom unit tests. - -### Accepted trade-offs - -| Trade-off | Why we can live with it | -| :----------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--Button-pad` is public-shaped but not a designer knob in the assembled Button (plumbing) | It's the agnostic seam; the real knob is `--Button--pad`, documented. The name marks the layer boundary. | -| Two vars per property instead of one | Mandatory (see _Why two vars_); the indirection is mechanical and documented. | -| Unprefixed `--_pad` could inherit a foreign value | Every built-in cell plus the root universal default set it on the element; revisit a prefix only if cross-component collisions surface as the pattern spreads. | -| `pad` shorthand is coarse (an override sets all sides) | Button padding is symmetric; tiny token surface; granular logical props can come later. | -| `var()` unresolved in jsdom (no computed-px assertions) | Argos covers default visuals; the chain is declarative and inspectable. | -| Inline still present for custom sizes | Rare; the built-in common case carries zero inline. | -| Per-property boilerplate grows with rollout | Acceptable for the payoff (runtime scoped theming); extract a helper before component #3. | diff --git a/docs/adr/0003-density-var-prefix.md b/docs/adr/0003-density-var-prefix.md deleted file mode 100644 index 1fc4f05ecf6bdb..00000000000000 --- a/docs/adr/0003-density-var-prefix.md +++ /dev/null @@ -1,129 +0,0 @@ -# ADR 0003 — Density token variable prefix & access - -> Status: **experiment** (branch `exp/density-var-prefix`, off `exp/css-var-density-adapter` / PR #48624) -> Feeds the RFC's open question 1 ("Variable prefix + var-access API"). Goal of the branch: -> find the *cleanest* way to make prefixing work, so the team can judge whether the added -> complexity is acceptable — i.e. measure the real floor, not an inflated cost. - -## Decision - -The **public** density tokens (the Material UI layer's designer knobs) **carry the theme -prefix, and the prefix tracks the css-var feature**: - -- `createTheme({ cssVariables: true })` → `theme.cssVarPrefix` is `'mui'` (or custom) → `--mui-Button-small-pad`. -- plain `createTheme()` → no `cssVarPrefix` → bare `--Button-small-pad`. - -The **agnostic seam** and the **internal default** are a different layer — see below. - -### The three layers map to three naming rules - -The adapter reads each component as three layers of responsibility (ADR-0001). The prefix -decision applies **per layer**, not uniformly: - -| Layer | Owner | Token | Prefixed? | In `buttonVars`? | -|---|---|---|---|---| -| **Agnostic** | the bare styled root, no design opinion | `--comp-` | **no** — literal | **no** | -| **Material UI** | the public per-size designer knob | `--mui-Button--` | yes (tracks feature) | **yes** | -| **Internal default** | today's `(variant,size)` literal, in `variants` | `--_` | no — literal | no | - -- The **agnostic seam** is the styled root's single consumption point. It must stay design-system- - agnostic, so it's the generic, literal `--comp-` (e.g. `--comp-pad`, `--comp-padBlock`) — - **not** `--Button-pad`, which wrongly wore the component name and the theme prefix. It is **not** - a public designer knob, so it does **not** belong in `buttonVars` (which is the Material UI layer). -- `buttonVars` therefore holds **only the public sized tokens** (`smallPad`, …) — the things a design - system actually tunes. - -```text ---comp-pad agnostic seam ← styled root's consumption point; literal, unprefixed ---mui-Button--pad public sized token ← designer knob; Material UI layer; tracks the prefix ---_pad internal default ← Material literal (in `variants`); literal, unprefixed -``` - -Consumption is unchanged in shape — the seam falls back to the internal default: -`padding: var(--comp-pad, var(--_pad))`; each size variant routes the public token over the default -into the seam: `'--comp-pad': var(--mui-Button-small-pad, var(--_pad))`. - -> **Generic seam + custom sizes.** A `--comp-` seam is set on the root for every **built-in** -> size (default + each size variant), so it never reads an inherited value there — a same-named seam on -> an ancestor is shadowed. The only seam-unset path is a **custom** (theme-added) size, and those are -> deliberately **out of density scope** (no inline routing; they render the literal `--_pad`). So the -> theoretical "two components share a css key and a custom-size inner one inherits the outer's seam" -> case is a documented non-goal, not a live risk. (A design system that adds a dense custom size sets -> the literal seam itself.) - -## How - -One resolver, used by **both** the styled component and the consumer, so the emitted name -and the targeted name are produced by the same function on the same theme and cannot drift: - -```ts -// styles/tokenAccess.ts -export function makeComponentVars(keyMap) { // cached by cssVarPrefix - const cache = new Map(); - return (theme) => { - const prefix = theme?.cssVarPrefix ?? ''; - let vars = cache.get(prefix); - if (!vars) { vars = Object.freeze(mapNames(keyMap, prefix)); cache.set(prefix, vars); } - return vars; - }; -} -``` - -```ts -// Button/buttonVars.ts — the typed handle lives WITH the component (public knobs only) -export const buttonVars = { smallPad: 'Button-small-pad', /* mediumPad, largePad */ } as const; -export const getButtonVars = makeComponentVars(buttonVars); -``` - -- **Component internals** read `const buttonVars = getButtonVars(theme)` inside the styled fn - (already `memoTheme`-cached), consume the literal seam `var(--comp-pad, var(--_pad))`, and route - the resolved public token into it per size. No `theme.vars` branch, no `|| 'mui'` fallback. -- **Consumers** call the same `getButtonVars(theme)` for the bare public-token name and set it: - `sx={{ [getButtonVars(theme).smallPad]: '2px 8px' }}`. - -## Why these shapes (rejected alternatives) - -- **`theme.vars.Button.smallPad` (rejected).** A core-assembled `theme.vars` node would force - `createTheme` to import every component's var map — a dependency inversion (core → leaf - components), kills tree-shaking, and a `Proxy` workaround fights Pigment's theme - serialization. The component's `buttonVars` key map is the typed handle instead. -- **`theme.getCssVar` (rejected for tokens).** It returns the wrapped read form - `var(--mui-…)`, which is invalid as a custom-property *key* — useless for the dominant - action (setting a token). It's also absent on a plain `createTheme()` theme. `getButtonVars` - returns the bare name, works in both modes, and covers set + (wrap-it-yourself) read. -- **Forcing `cssVarPrefix: 'mui'` on every theme (rejected).** Would give one stable name in - all modes, but bolts a css-var concept onto non-css-var themes and needs a `createThemeNoVars` - change. Since both sides share `getButtonVars`, names match without it. -- **Custom-size inline token routing (dropped — custom sizes are out of density scope).** The - unprefixed prototype routed custom sizes inline (`style={{ '--comp-pad': 'var(--Button-xl-pad)' }}`) - for free, since the token name was a static string. Prefixing makes that inline name - theme-dependent (a `useTheme()` in render). Rather than pay that for a rare path, custom sizes are - excluded from density: they render `--_pad`; only built-in sizes get per-size tokens (in `variants`, - which already have `theme`). This is itself a small, concrete **cost of prefixing** — in the - unprefixed design custom-size routing is free. - -## Cost the team is being asked to accept - -1. **Mode-dependent public name** — `--mui-Button-*` with `cssVariables`, `--Button-*` without. - Fine within one app; the casualty is cross-mode preset portability (a preset authored as - `--mui-*` does nothing pasted into a no-vars app), and docs must show the name per mode. -2. **Tokens are theme-resolved, not static strings** — a consumer must call `getButtonVars(theme)` - rather than hand-type a constant. Mitigated: it's one cached call; bare names; typed keys. - -Everything else that looked expensive in the first cut (a hook per render, a parallel naming -scheme, a `theme.vars` assembly, build-time questions) was removable. This is the floor. - -## Performance - -`getButtonVars` is memoized by `cssVarPrefix` (domain ≈ `{ '', 'mui' }`): first call builds + -freezes the map, every later call is an O(1) `Map.get` returning the shared object — no -per-call allocation, multi-field free. Component internals are additionally inside `memoTheme`. - -## Branch staleness - -Pre-existing density demos/harness (`density-tokens`, `density-showcase`, `density-fixture`, -`scripts/density-screenshots`) hand-author tokens as `--Button-*` / `--OutlinedInput-*`. Under -`cssVariables: true` the components now emit `--mui-*`, so those override scopes no longer drive -the three converted components on this branch. The **default** (no-token) render is unchanged -(a consistent rename, not a value change) → Argos zero-diff holds. Only the explicit-override -scopes need re-pointing if this direction is adopted. diff --git a/docs/adr/density-adapter-rollout.md b/docs/adr/density-adapter-rollout.md deleted file mode 100644 index 9739eb25cb91df..00000000000000 --- a/docs/adr/density-adapter-rollout.md +++ /dev/null @@ -1,277 +0,0 @@ -# Density adapter — rollout goal - -Spread CSS-var density adapter to more components. Decision/spec: `0001-css-var-density-adapter.md`. - -**Goal:** each density dimension = one hand-authorable CSS var. Default render -pixel-identical to today. No `calc` math for users, no `--mui-spacing` dial. - -## The 3 vars (per density axis) - -```text ---Component-- public sized token (designer knob, enhanceDensity target) ---Component- agnostic seam (styled root consumes this) ---_ internal default (Material literal, lives in variants) -``` - -Root consumes seam, seam falls to internal default: - -```js -padding: 'var(--Button-pad, var(--_pad))'; -``` - -Resolution = **sized-only** for a size-varying axis. Sized token wins -> else -internal default. No all-sizes-over-sized base token. - -**Size-invariant default ≠ base token.** If an axis's default is the same every -size (e.g. OutlinedInput inline `14px`) you _can_ skip the size layer — base -token `--Component-`, consumed `var(--Component-, var(--_))`, -nothing routes it. But only when per-size override is genuinely meaningless, -because a **base token can't be tuned per size from the theme**. If a design -system might want that axis denser on small (density!), **size it anyway**: same -default both sizes, but expose `--Component--`. OutlinedInput sizes -_both_ padBlock and padInline for this reason (inline default `14px` each size). - -**Boolean `dense` axis (state token).** When compactness is a **boolean** prop -(`dense`) not a `size` enum, don't name the off-state. The **default state is the -plain seam** `--Component-` (base-token-shaped: nothing routes it in the -base; designer sets it directly); **only `dense` is qualified** -`--Component-dense-`, routed in the `dense` variant: - -```js -// base: default state = plain seam, falls to the internal default literal -'--_padBlock': '8px', -paddingTop: 'var(--ListItem-padBlock, var(--_padBlock))', -// { dense } variant: own literal + route the dense token -'--_padBlock': '4px', -'--ListItem-padBlock': 'var(--ListItem-dense-padBlock, var(--_padBlock))', -``` - -No `--Component-normal/regular/default-` — a boolean has no name for "off". -(MenuItem, ListItem, ListItemButton, ListItemText.) - -## Recipe A — small component (Button) - -One element. `pad` shorthand (all sides move together). - -1. Root: universal default + consume. - ```js - '--_pad': '6px 16px', - padding: 'var(--Button-pad, var(--_pad))', - ``` -2. Variants — literal default per `(variant, size)`. - ```js - { props: { variant: 'text', size: 'small' }, style: { '--_pad': '4px 5px' } } - // medium defaults reuse the { variant } blocks (DRY) - ``` -3. Variants — built-in size routing (deduped CSS, no inline). - ```js - { props: { size: 'small' }, style: { '--Button-pad': 'var(--Button-small-pad, var(--_pad))' } } - ``` -4. Custom size -> route inline (only non-built-in size emits a `style` attr). - ```js - const densityVars = ['small', 'medium', 'large'].includes(size) - ? undefined - : { '--Button-pad': `var(--Button-${size}-pad, var(--_pad))` }; - ``` -5. `enhanceDensity` maps `--Button--pad` -> `--mui-density-*` step. - -## Recipe B — big component (OutlinedInput) - -Padding spans 2 elements (root when multiline, input otherwise) + paired sibling -(InputLabel). More dimensions but token model is _simpler_. - -**Pick real axis + shape.** Block (`16.5 -> 8.5`) varies by size -> **sized** -`padBlock`. Inline default is `14px` both sizes, but a design system may want -per-size inline density -> **size it too** (`padInline`, same `14px` default each -size). Both axes sized, routed per size. Split block/inline forced: they land on -different elements/states + zero per adornment. - -**Two elements, tokenize in place.** Padding lives on the input (non-multiline, -inline gutters) _and_ the root (multiline, adornment gutters) — never both on the -same side at once (multiline zeroes input padding; an adorned side zeroes the -input and gutters from the root). Keep master's split: each site declares its own -`--_` + routes the size token, right where the literal was. No lift to a -single owner, no inheritance, no dropped variants — smallest diff from master. - -```js -// input base + root multiline cell: -'--_padBlock': '16.5px', -'--_padInline': '14px', -'--OutlinedInput-padBlock': 'var(--OutlinedInput-medium-padBlock, var(--_padBlock))', -'--OutlinedInput-padInline': 'var(--OutlinedInput-medium-padInline, var(--_padInline))', -padding: 'var(--OutlinedInput-padBlock, var(--_padBlock)) var(--OutlinedInput-padInline, var(--_padInline))', -// each size-small cell re-routes its axis to the small token: -// input { size: small } -> padBlock + padInline small (input owns both) -// root { multiline && small } -> padBlock + padInline small -// root { startAdornment/endAdornment && small } -> padInline small (gutter) -``` - -Cost of sizing inline in place: the size-agnostic adornment gutters need a small -re-route, so each adornment variant gains a `&& size === 'small'` sibling. Cheap -(one line each), and keeps both axes wired identically (no lift). - -**Paired sibling component (the label).** Generic component must not name -specific component token. Label exposes own seam: - -```js -// InputLabel — generic, literal default -transform: 'translate(14px, var(--InputLabel-y, 16px)) scale(1)'; // small: 9px -``` - -Specific component owns bridge. Label = _preceding_ sibling -> reach via `:has`. -Cross-element rule -> derive the label seam straight from the **public sized -token** + literal fallback (can't read the input's internal `--_padBlock`): - -```js -// OutlinedInputRoot, per size -[`.${inputLabelClasses.root}:has(~ &)`]: { - '--InputLabel-y': 'calc(var(--OutlinedInput-medium-padBlock, 16.5px) - 0.5px)', // small: small token + 0.5px -} -``` - -One knob (`--OutlinedInput--padBlock`) -> input box + label move together. - -## Recipe C — shared internal base (SwitchBase -> Checkbox / Radio / Switch) - -Several components share one styled base. Put the **agnostic layer on the base**: -it consumes the seam once, with the internal default. Each consumer is the -**Material layer** -> routes its _own_ per-component public token into the shared -seam. - -```js -// SwitchBase (shared, agnostic): consume once -'--_pad': '9px', -padding: 'var(--SwitchBase-pad, var(--_pad))', -``` - -The seam keeps the **base's** name (`--SwitchBase-pad`) — it's plumbing; the -public knob is the per-component sized token (`--Checkbox--pad`). - -Two reader topologies: - -- **Consumer _is_ the base** (Checkbox/Radio = `styled(SwitchBase)`): route on - the consumer's own root, same element, no selector. - ```js - { props: { size: 'small' }, style: { '--SwitchBase-pad': 'var(--Checkbox-small-pad, var(--_pad))' } } - ``` -- **Consumer _wraps_ the base** as a descendant (the Switch thumb is a - `SwitchBase` inside `SwitchRoot`): set the seam on the wrapper root — the base - **inherits** it. No descendant selector (custom props inherit; the base doesn't - redeclare the seam). - ```js - // SwitchRoot: thumb inherits this - '--SwitchBase-pad': 'var(--Switch-medium-pad, var(--_pad))', - ``` - -**Inheritance caveat.** The _seam_ inherits because the base doesn't redeclare it. -But the base **does** redeclare `--_` (that's what keeps it unprefixed-safe), -so an inherited `--_` is _shadowed_ on the base. A wrapper that needs a value -different from the base's feeds it **through the seam** — set the seam directly on -the wrapper, not the shadowed `--_`. - -**Interlocked geometry -> derive, don't tokenize one axis.** When a component's -dims move together (a `Switch`: width/height/thumb/touch/travel), tokenizing one -(the thumb pad) alone drifts the thumb off the track. Tokenize the real dims per -size (incl. the track gutter `pad`) and **derive** the coupled values with `calc`, -feeding the seam: - -```js -// SwitchRoot, per size: --Switch--{width,height,thumbSize,touchSize,pad} -padding: 'var(--Switch-pad, var(--_pad))', // track gutter (own axis) -'--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', -// thumb button (absolute): keep it centered -top: 'calc((var(--Switch-height) - var(--Switch-touchSize)) / 2)', -// checked: travel = width - touch -transform: 'translateX(calc(var(--Switch-width) - var(--Switch-touchSize)))', -``` - -`touchSize == height` by default -> pad `9/4`, top `0`, travel `20/16` -(pixel-identical). `enhanceDensity` can still wire it: map the input dims to scale -steps and the derived values stay valid (Switch uses `xxl` for the wider track). - -## Gotchas - -- **Split axes only when the impl forces it.** Differing values per side is NOT - enough. If all sides are set together via one shorthand on one element, keep one - key — Button uses `pad` even though block 6 ≠ inline 8. Split per axis - (`padBlock`/`padInline`) only when the impl applies them separately: different - elements/states (OutlinedInput block on input vs root-multiline), independent - side-zeroing (adornments), or different token shapes (sized block + base inline). - OutlinedInput is forced; Button is not. Don't over-tokenize. -- **Two vars, not one.** Cells write value (`--_pad`), routing writes reference - (`--Button-pad`). One var fails 3 ways: inline bridge self-references (invalid - CSS) -> literal leaks to runtime; `(variant×size)` vs size-only writes clobber - on one element; lose the seam. -- **Uniform consume shape — every axis.** Always `var(--Component-, -var(--_))`, including a size-invariant **base** axis. Two real mistakes to - avoid: (a) **bare literal default** for a base axis (`var(--seam, 14px)`) — - instead define `--_` (e.g. `--_padInline: 14px`) so the default lives in - one place and the shape matches sized axes; (b) **dropping a fallback because - the seam "is always set"** — keep it; the uniform shape is the contract (Button - `var(--Button-pad, var(--_pad))`; a sized axis carries `--_` in _both_ the - routing and the consume — that double-reference is intentional). Consistency - over minimalism. -- **Unprefixed `--_` safe only if every instance sets own.** Custom prop - inherits. Co-located setter (Button) or every root re-sets (OutlinedInput) -> - ancestor value never wins. Else prefix it. -- **Sibling can't inherit.** Sibling vars need common ancestor. Specific - component reaches sibling via `:has(~ &)`. Note: `+`/`~` match _following_ - siblings only -> `:has` makes the _earlier_ element the subject. -- **One element can't see another's internal var.** Label can't read input-root - `--_padBlock`. Reference **public** token (visible at `:root`) + literal - fallback. Never the internal var across elements. -- **Inline padding = outer gutter, not the adornment↔input gap.** The inline - token (`padInline`) is the border→first/last content inset (border→adornment - when adorned). The adornment↔text gap is the adornment's own margin - (`InputAdornment` marginRight/Left, ~8px), separate and untouched. Don't read - the gutter as the gap, and don't expect tokenizing it to move that gap. -- **Check if component defaults `size`.** Most components destructure a default - (Button: `size = 'medium'`) -> `ownerState.size` always valid -> `{ size: medium }` - variant matches, fine. But context-driven ones (InputBase/OutlinedInput read - `size` from FormControl, **no** default) -> `size` can be `undefined` -> put - medium routing in **base**, not a `{ size: medium }` variant (won't match - undefined). Tell: does the component have a `{ size: medium }` variant today? -- **Shorthand vs longhand.** Use `padding` shorthand to set block + override - earlier longhand (e.g. InputBase `paddingTop`); zero sides after with - `paddingLeft: 0`. -- **Pixel-identical = exact calc.** `calc` must compute today's px exactly - (`16.5 - 0.5 = 16` exact -> Argos zero-diff). Per-size sign trick when offset - flips (med `-0.5`, small `+0.5`). -- **`:has()` support** — Chrome 105 / Safari 15.4 / Firefox 121. Fine for - experiment; confirm baseline before ship. -- **`calc`/`var` resolve in browser only, not jsdom.** Assert density in - visual/screenshot tests, not unit. - -## Verify (per component) - -Screenshot harness `scripts/density-screenshots/` (`maxDiffPixels: 0`): - -1. Add matrix to `density-fixture.tsx` `demos` (+ token overrides to `scopes`). -2. Baseline from **master** (default unchanged by design): - `git checkout master -- ` -> `COMPONENT=X pnpm density:shot:update` -> restore. -3. `COMPONENT=X pnpm density:shot` -> default == baseline (gate) + dense/loose for eyeball. - -## Naming - -- Public seam/token: `--Component-` / `--Component--`. PascalCase - component, short semantic key (`pad`, `gap`, `padBlock`). Matches `--AppBar-background`. -- Internal: `--_` (leading underscore, no prefix). -- Key granularity = component's real spacing structure. One shorthand key - (`pad`) when sides set together on one element; split per axis only when forced - (see gotcha). Per axis: **sized by default** (per-size tunable); base token only - if per-size override is genuinely meaningless — a size-invariant _default_ alone - doesn't justify base (size it so density can tune it per size). -- **Boolean toggle (`dense`)** = **state token**: off-state is the plain seam - `--Component-` (don't qualify it); only the on-state is qualified - `--Component-dense-`. Never `--Component-normal/regular/default-`. - -## Order to roll out - -Small single-element first (prove pattern) -> bigger multi-element -> paired -sibling family. Done: Button, OutlinedInput (+ InputLabel, TextField outlined), -the dashboard set (Chip, IconButton, MenuItem, ListItem(+Button/Icon/Text), -ListSubheader, Toolbar, Tab/Tabs, TablePagination, CardContent, Select, -Breadcrumbs, InputAdornment, Badge), and the SwitchBase family (Checkbox, Radio, -Switch — Recipe C). Next candidates: FilledInput, Input (standard) — note -asymmetric block padding (`4/5`, `25/8`) -> need per-side seam, not single -`padBlock`. From afea56fdbacdb7b8f551506ecc0cb6d72436f22e Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 10:46:20 +0700 Subject: [PATCH 038/114] [docs] drop dangling ADR references from density comments --- docs/pages/experiments/density-fixture.tsx | 2 +- docs/pages/experiments/density-showcase.tsx | 2 +- docs/pages/experiments/density-tokens.tsx | 2 +- docs/src/modules/components/densityDemos.tsx | 2 +- packages/mui-material/src/Button/Button.js | 2 +- packages/mui-material/src/Button/buttonVars.ts | 2 +- packages/mui-material/src/Chip/Chip.js | 8 ++++---- packages/mui-material/src/IconButton/IconButton.js | 4 ++-- packages/mui-material/src/ListItemText/ListItemText.js | 2 +- packages/mui-material/src/MenuItem/MenuItem.js | 2 +- packages/mui-material/src/OutlinedInput/OutlinedInput.js | 4 ++-- .../mui-material/src/OutlinedInput/outlinedInputVars.ts | 2 +- packages/mui-material/src/Switch/Switch.js | 2 +- packages/mui-material/src/Tabs/Tabs.js | 2 +- packages/mui-material/src/internal/SwitchBase.js | 2 +- packages/mui-material/src/styles/enhanceDensity.ts | 4 ++-- packages/mui-material/src/styles/tokenAccess.ts | 2 +- 17 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 3ccddaea7d40f8..2e81b3c6e95168 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -5,7 +5,7 @@ import Box from '@mui/material/Box'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import demos from 'docs/src/modules/components/densityDemos'; -// Local verification fixture for the CSS-var density adapter (docs/adr/0001). +// Local verification fixture for the CSS-var density adapter. // Used by scripts/density-screenshots. Renders one component's load-bearing // matrix (shared `demos`) inside #density-scope; the harness sets `level` // (default | dense | loose), which the scope translates into per-component diff --git a/docs/pages/experiments/density-showcase.tsx b/docs/pages/experiments/density-showcase.tsx index 298cbafd7ee9ef..922df9539b1059 100644 --- a/docs/pages/experiments/density-showcase.tsx +++ b/docs/pages/experiments/density-showcase.tsx @@ -17,7 +17,7 @@ import CssBaseline from '@mui/material/CssBaseline'; import { createTheme, ThemeProvider, enhanceDensity, DensityScale } from '@mui/material/styles'; import demos from 'docs/src/modules/components/densityDemos'; -// Client-facing showcase for the CSS-var density adapter (docs/adr/0001). +// Client-facing showcase for the CSS-var density adapter. // Three presets each map to one `enhanceDensity(theme, { scale })` call — the // only knob is the 7-step density scale. Flip a preset -> the whole gallery // reflows because every component pulls its sized tokens from `--mui-density-*`. diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx index 4738c3dce98eaa..df2c6cf06f4b36 100644 --- a/docs/pages/experiments/density-tokens.tsx +++ b/docs/pages/experiments/density-tokens.tsx @@ -18,7 +18,7 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; -// Density experiment — CSS-var adapter (docs/adr/0001-css-var-density-adapter.md). +// Density experiment — CSS-var adapter. // Agnostic layer: Button consumes `var(--Button-pad, var(--_pad))`. Material UI // layer sets the (variant, size) literal default `--_pad` and the built-in-size // routing `--Button-pad: var(--Button--pad, var(--_pad))` in `variants` diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index e1954d1f8b8d3f..d46274c4e0c749 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -43,7 +43,7 @@ import DraftsIcon from '@mui/icons-material/Drafts'; import FavoriteIcon from '@mui/icons-material/Favorite'; import VisibilityIcon from '@mui/icons-material/Visibility'; -// Shared density demo matrices for the CSS-var density adapter (docs/adr/0001). +// Shared density demo matrices for the CSS-var density adapter. // Consumed by both the screenshot fixture (density-fixture) and the client // showcase (density-showcase). Each entry renders one component's load-bearing // size/variant matrix. `level=default` (no token overrides) must stay diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index 4c6a505815031b..cf0c5a1bdc20f8 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -95,7 +95,7 @@ const ButtonRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => { // Material UI layer: the internal sized tokens are the static, unprefixed - // `private_buttonVars` map (ADR-0003), imported here and by `enhanceDensity` + // `private_buttonVars` map, imported here and by `enhanceDensity` // so emitted and targeted names can't drift. The agnostic seam (`--comp-pad`) // and internal default (`--_pad`) are literal and unprefixed. const inheritContainedBackgroundColor = diff --git a/packages/mui-material/src/Button/buttonVars.ts b/packages/mui-material/src/Button/buttonVars.ts index 1659d7ea312722..faab6c88093d20 100644 --- a/packages/mui-material/src/Button/buttonVars.ts +++ b/packages/mui-material/src/Button/buttonVars.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ /** - * Button density token identities (ADR-0003) — the Material UI layer's internal + * Button density token identities — the Material UI layer's internal * designer knobs (`private_*` per the density RFC). Static, unprefixed literals * imported by both the styled component AND `enhanceDensity`, so the emitted and * targeted names can't drift. The agnostic seam (`--comp-pad`) and internal diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index 251ab9350d1e49..1348181b10f6af 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -75,7 +75,7 @@ const ChipRoot = styled('div', { justifyContent: 'center', // Agnostic seam: the styled root reads `--Chip-height`; `--_height` is the // universal default (today's medium height). Size variants route the public - // sized token over it. See docs/adr/0001. + // sized token over it. '--_height': '32px', height: 'var(--Chip-height, var(--_height))', lineHeight: 1.5, @@ -97,7 +97,7 @@ const ChipRoot = styled('div', { opacity: (theme.vars || theme).palette.action.disabledOpacity, pointerEvents: 'none', }, - // Density adapter (docs/adr/0001): avatar/icon/deleteIcon scale with the + // Density adapter: avatar/icon/deleteIcon scale with the // chip height. Each is `calc(var(--Chip-height) - inset)` where the inset // reproduces today's medium size (height 32: avatar/icon 24, deleteIcon 22); // the small variant overrides the inset for height 24. @@ -356,7 +356,7 @@ const ChipLabel = styled('span', { // Agnostic seam: the label reads `--Chip-padInline` on both sides; `--_padInline` // is the universal default (today's medium filled inline padding). Variants // specialize the default per (variant, size) and size variants route the public - // sized token over it. See docs/adr/0001. + // sized token over it. '--_padInline': '12px', paddingLeft: 'var(--Chip-padInline, var(--_padInline))', paddingRight: 'var(--Chip-padInline, var(--_padInline))', @@ -481,7 +481,7 @@ const Chip = React.forwardRef(function Chip(inProps, ref) { // Material UI layer: built-in sizes route the public sized tokens via variants // (deduped CSS). A custom size has no such variant, so route it inline on the // root — the tokens inherit down to the label. The `--_*` defaults live in the - // variants. See docs/adr/0001. + // variants. const densityVars = size === 'small' || size === 'medium' ? undefined diff --git a/packages/mui-material/src/IconButton/IconButton.js b/packages/mui-material/src/IconButton/IconButton.js index 2adc1ca1832f79..e2cfe9b88496e1 100644 --- a/packages/mui-material/src/IconButton/IconButton.js +++ b/packages/mui-material/src/IconButton/IconButton.js @@ -58,7 +58,7 @@ const IconButtonRoot = styled(ButtonBase, { fontSize: theme.typography.pxToRem(24), // Agnostic layer: the only spacing surface the styled root reads. `--_pad` // is the universal default (today's medium padding); size variants specialize - // it, so a custom size still gets a sane value. See docs/adr/0001. + // it, so a custom size still gets a sane value. '--_pad': '8px', padding: 'var(--IconButton-pad, var(--_pad))', borderRadius: '50%', @@ -223,7 +223,7 @@ const IconButton = React.forwardRef(function IconButton(inProps, ref) { // Material UI layer: built-in sizes route the public sized token via variants // (deduped CSS). A custom size has no such variant, so route it inline — the // token name carries the runtime size string, keeping custom sizes tunable for - // free. The `--_pad` default lives in the variants. See docs/adr/0001. + // free. The `--_pad` default lives in the variants. const densityVars = iconButtonSizes.includes(size) ? undefined : { '--IconButton-pad': `var(--IconButton-${size}-pad, var(--_pad))` }; diff --git a/packages/mui-material/src/ListItemText/ListItemText.js b/packages/mui-material/src/ListItemText/ListItemText.js index 954c99c9b78ee9..b836d2ce9806fc 100644 --- a/packages/mui-material/src/ListItemText/ListItemText.js +++ b/packages/mui-material/src/ListItemText/ListItemText.js @@ -40,7 +40,7 @@ const ListItemTextRoot = styled('div', { })({ flex: '1 1 auto', minWidth: 0, - // Density adapter (docs/adr/0001): each spacing literal becomes + // Density adapter: each spacing literal becomes // `var(--seam, var(--_))`, tokenized in place. The compactness dimension // is `dense` (boolean) — default state = plain seam `--ListItemText-` over // `--_`; the dense variant re-routes the seam to its own token. diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 285f37e48d79d6..b388d65a0c6441 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -61,7 +61,7 @@ const MenuItemRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => ({ ...theme.typography.body1, - // Density adapter (docs/adr/0001): `dense` is the compactness axis (boolean). + // Density adapter: `dense` is the compactness axis (boolean). // The default (non-dense) state is the plain seam `--MenuItem-` over the // internal default `--_`; the `dense` variant re-routes the seam to the // `--MenuItem-dense-` token. Block + min-height live on the root diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index ba13af290a4162..d22907f3abf97b 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -49,10 +49,10 @@ const OutlinedInputRoot = styled(InputBaseRoot, { theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; // Material UI layer: public sized tokens honor cssVarPrefix (default "mui"). // Agnostic seams (`--comp-padBlock`, `--comp-padInline`, `--comp-labelY`) and - // internal `--_*` defaults are literal and unprefixed. See ADR-0003. + // internal `--_*` defaults are literal and unprefixed. const v = getOutlinedInputVars(theme); return { - // Density adapter (docs/adr/0001): each padding literal becomes + // Density adapter: each padding literal becomes // `var(--seam, var(--_))`, tokenized in place. Both axes are sized — // each seam routes the per-size public token (block + inline). The internal // defaults live in the variants that consume them (below), like Button's diff --git a/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts b/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts index 79ea06e9006167..c439b40085f0ec 100644 --- a/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts +++ b/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts @@ -1,7 +1,7 @@ import { makeComponentVars } from '../styles/tokenAccess'; /** - * OutlinedInput density token identities (ADR-0003) — the Material UI layer's + * OutlinedInput density token identities — the Material UI layer's * public sized knobs (block + inline padding, prefixed via * `getOutlinedInputVars`). The agnostic seams (`--comp-padBlock`, * `--comp-padInline`, and the `--comp-labelY` the `:has` bridge drives on the diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 1b7202fbbbc60d..27d6d2b3b10630 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -52,7 +52,7 @@ const SwitchRoot = styled('span', { ]; }, })({ - // Density (docs/adr/0001): Switch geometry is interlocked, so the knobs are the + // Density: Switch geometry is interlocked, so the knobs are the // dims (width/height/thumbSize/touchSize) + the track gutter (pad); the thumb's // touch padding and travel are *derived* so the thumb stays centered on the track. // SwitchBase pad = (touchSize - thumbSize) / 2 (centers thumb in the button) diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js index a0cd9b3a42dce9..cd2223e6323087 100644 --- a/packages/mui-material/src/Tabs/Tabs.js +++ b/packages/mui-material/src/Tabs/Tabs.js @@ -77,7 +77,7 @@ const TabsRoot = styled('div', { })( memoTheme(({ theme }) => ({ overflow: 'hidden', - // Density adapter (docs/adr/0001): base token, 48px literal fallback keeps the + // Density adapter: base token, 48px literal fallback keeps the // default pixel-identical. Tabs is the Tab's parent, so it can't read the // child's `--Tab-minHeight` — it carries its own seam. minHeight: 'var(--Tabs-minHeight, 48px)', diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js index e837e251205cc8..3dc1e3f70dc176 100644 --- a/packages/mui-material/src/internal/SwitchBase.js +++ b/packages/mui-material/src/internal/SwitchBase.js @@ -25,7 +25,7 @@ const useUtilityClasses = (ownerState) => { const SwitchBaseRoot = styled(ButtonBase, { name: 'MuiSwitchBase', })({ - // Density adapter (docs/adr/0001): SwitchBase is the agnostic layer shared by + // Density adapter: SwitchBase is the agnostic layer shared by // Checkbox/Radio (and the Switch thumb). It consumes one seam; the Material // layer (Checkbox/Radio) routes its per-size public token into --SwitchBase-pad. '--_pad': '9px', diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 06f143417b4e5e..3c30add368bffc 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -62,7 +62,7 @@ const cssVar = (key: DensityKey) => `--mui-density-${key}`; * `styleOverrides.root` (e.g. `--Button-medium-pad: var(--mui-density-xs) var(--mui-density-lg)`). * * `createTheme` is left untouched; without this function components render their - * literal-px defaults. See `docs/adr/0001-css-var-density-adapter.md`. + * literal-px defaults. * * @param themeInput - The created theme to enhance. * @param options - Override the density scale. @@ -96,7 +96,7 @@ export default function enhanceDensity< return acc; }, {} as DensityScale); - // Public token name, prefix-guarded to match the component resolver (ADR-0003): + // Public token name, prefix-guarded to match the component resolver: // prefixed when the theme has a cssVarPrefix, bare otherwise. Only the converted // components (Button, OutlinedInput, InputLabel) use it; the rest stay unprefixed. const prefix = (themeInput as any).cssVarPrefix; diff --git a/packages/mui-material/src/styles/tokenAccess.ts b/packages/mui-material/src/styles/tokenAccess.ts index 2e69935a1cc569..fa24e5c8b3c834 100644 --- a/packages/mui-material/src/styles/tokenAccess.ts +++ b/packages/mui-material/src/styles/tokenAccess.ts @@ -1,5 +1,5 @@ /** - * Density token name resolution (ADR-0003). The prefix tracks the css-var + * Density token name resolution. The prefix tracks the css-var * feature: a theme created with `cssVariables` carries `cssVarPrefix` (default * `mui`), so tokens resolve to `--mui-Button-pad`; a plain `createTheme()` has * no prefix, so they resolve to `--Button-pad`. The component internals and the From 5389da7b45290bc9b4cad6686adde91e79514f6b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 10:52:52 +0700 Subject: [PATCH 039/114] [docs] scope density prototype to Button only Revert the ~22 non-Button component density adapters to master (Chip, Switch, OutlinedInput, MenuItem, Tab(s), ListItem*, Toolbar, Select, Badge, Checkbox, Radio, IconButton, etc.); trim enhanceDensity to the Button block + :root scale; drop tokenAccess/outlinedInputVars and the multi-component showcase/tokens pages; reduce the fixture + demos to Button. --- docs/pages/experiments/density-fixture.tsx | 260 +------- docs/pages/experiments/density-showcase.tsx | 282 --------- docs/pages/experiments/density-tokens.tsx | 280 --------- docs/src/modules/components/densityDemos.tsx | 593 +----------------- packages/mui-material/src/Badge/Badge.js | 20 +- .../src/Breadcrumbs/Breadcrumbs.js | 5 +- .../src/CardContent/CardContent.js | 6 +- .../mui-material/src/Checkbox/Checkbox.js | 10 - packages/mui-material/src/Chip/Chip.js | 92 +-- .../mui-material/src/IconButton/IconButton.js | 40 +- .../src/InputAdornment/InputAdornment.js | 20 +- .../mui-material/src/InputLabel/InputLabel.js | 6 +- .../mui-material/src/ListItem/ListItem.js | 24 +- .../src/ListItemButton/ListItemButton.js | 33 +- .../src/ListItemIcon/ListItemIcon.js | 3 +- .../src/ListItemText/ListItemText.js | 30 +- .../src/ListSubheader/ListSubheader.js | 13 +- .../mui-material/src/MenuItem/MenuItem.js | 41 +- .../src/OutlinedInput/OutlinedInput.js | 150 ++--- .../mui-material/src/OutlinedInput/index.js | 2 - .../src/OutlinedInput/outlinedInputVars.ts | 17 - packages/mui-material/src/Radio/Radio.js | 10 - .../mui-material/src/Select/SelectInput.js | 7 +- packages/mui-material/src/Switch/Switch.js | 67 +- packages/mui-material/src/Tab/Tab.js | 31 +- packages/mui-material/src/Tab/Tab.test.js | 5 +- .../src/TablePagination/TablePagination.js | 17 +- packages/mui-material/src/Tabs/Tabs.js | 5 +- packages/mui-material/src/Toolbar/Toolbar.js | 18 +- .../mui-material/src/internal/SwitchBase.js | 6 +- .../mui-material/src/styles/enhanceDensity.ts | 345 ---------- .../mui-material/src/styles/tokenAccess.ts | 41 -- 32 files changed, 170 insertions(+), 2309 deletions(-) delete mode 100644 docs/pages/experiments/density-showcase.tsx delete mode 100644 docs/pages/experiments/density-tokens.tsx delete mode 100644 packages/mui-material/src/OutlinedInput/outlinedInputVars.ts delete mode 100644 packages/mui-material/src/styles/tokenAccess.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 2e81b3c6e95168..70a2f20c058159 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -5,11 +5,11 @@ import Box from '@mui/material/Box'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import demos from 'docs/src/modules/components/densityDemos'; -// Local verification fixture for the CSS-var density adapter. -// Used by scripts/density-screenshots. Renders one component's load-bearing -// matrix (shared `demos`) inside #density-scope; the harness sets `level` -// (default | dense | loose), which the scope translates into per-component -// density-token overrides. `level=default` sets no tokens, so the render must be +// Local verification fixture for the CSS-var density adapter (Button only). +// Used by scripts/density-screenshots. Renders Button's load-bearing matrix +// (shared `demos`) inside #density-scope; the harness sets `level` +// (default | dense | loose), which the scope translates into density-token +// overrides. `level=default` sets no tokens, so the render must be // pixel-identical to the pre-change baseline. const theme = createTheme({ cssVariables: true }); @@ -28,257 +28,7 @@ const scopes: Record> = { ['--Button-large-pad' as any]: '16px 30px', }, }, - OutlinedInput: { - dense: { - ['--OutlinedInput-small-padBlock' as any]: '4px', - ['--OutlinedInput-medium-padBlock' as any]: '10px', - ['--OutlinedInput-small-padInline' as any]: '6px', - ['--OutlinedInput-medium-padInline' as any]: '8px', - }, - loose: { - ['--OutlinedInput-small-padBlock' as any]: '14px', - ['--OutlinedInput-medium-padBlock' as any]: '28px', - ['--OutlinedInput-small-padInline' as any]: '20px', - ['--OutlinedInput-medium-padInline' as any]: '24px', - }, - }, - Chip: { - dense: { - ['--Chip-small-height' as any]: '18px', - ['--Chip-medium-height' as any]: '24px', - ['--Chip-small-padInline' as any]: '4px', - ['--Chip-medium-padInline' as any]: '6px', - }, - loose: { - ['--Chip-small-height' as any]: '32px', - ['--Chip-medium-height' as any]: '44px', - ['--Chip-small-padInline' as any]: '14px', - ['--Chip-medium-padInline' as any]: '20px', - }, - }, - IconButton: { - dense: { - ['--IconButton-small-pad' as any]: '1px', - ['--IconButton-medium-pad' as any]: '3px', - ['--IconButton-large-pad' as any]: '6px', - }, - loose: { - ['--IconButton-small-pad' as any]: '10px', - ['--IconButton-medium-pad' as any]: '16px', - ['--IconButton-large-pad' as any]: '22px', - }, - }, - MenuItem: { - dense: { - ['--MenuItem-minHeight' as any]: '36px', - ['--MenuItem-dense-minHeight' as any]: '24px', - ['--MenuItem-padBlock' as any]: '2px', - ['--MenuItem-dense-padBlock' as any]: '1px', - ['--MenuItem-padInline' as any]: '8px', - ['--MenuItem-dense-padInline' as any]: '6px', - }, - loose: { - ['--MenuItem-minHeight' as any]: '64px', - ['--MenuItem-dense-minHeight' as any]: '48px', - ['--MenuItem-padBlock' as any]: '14px', - ['--MenuItem-dense-padBlock' as any]: '10px', - ['--MenuItem-padInline' as any]: '28px', - ['--MenuItem-dense-padInline' as any]: '24px', - }, - }, - ListItemButton: { - dense: { - ['--ListItemButton-padBlock' as any]: '2px', - ['--ListItemButton-dense-padBlock' as any]: '0px', - ['--ListItemButton-padInline' as any]: '8px', - ['--ListItemButton-dense-padInline' as any]: '4px', - }, - loose: { - ['--ListItemButton-padBlock' as any]: '16px', - ['--ListItemButton-dense-padBlock' as any]: '12px', - ['--ListItemButton-padInline' as any]: '32px', - ['--ListItemButton-dense-padInline' as any]: '24px', - }, - }, - ListItemIcon: { - dense: { ['--ListItemIcon-minWidth' as any]: '24px' }, - loose: { ['--ListItemIcon-minWidth' as any]: '56px' }, - }, - ListItemText: { - dense: { - ['--ListItemText-marginBlock' as any]: '1px', - ['--ListItemText-dense-marginBlock' as any]: '0px', - ['--ListItemText-insetPad' as any]: '32px', - ['--ListItemText-dense-insetPad' as any]: '24px', - }, - loose: { - ['--ListItemText-marginBlock' as any]: '12px', - ['--ListItemText-dense-marginBlock' as any]: '8px', - ['--ListItemText-insetPad' as any]: '72px', - ['--ListItemText-dense-insetPad' as any]: '64px', - }, - }, - ListSubheader: { - dense: { - ['--ListSubheader-height' as any]: '32px', - ['--ListSubheader-padInline' as any]: '8px', - ['--ListSubheader-inset' as any]: '48px', - }, - loose: { - ['--ListSubheader-height' as any]: '64px', - ['--ListSubheader-padInline' as any]: '28px', - ['--ListSubheader-inset' as any]: '96px', - }, - }, - Toolbar: { - dense: { - ['--Toolbar-dense-minHeight' as any]: '32px', - ['--Toolbar-padInline' as any]: '8px', - }, - loose: { - ['--Toolbar-dense-minHeight' as any]: '72px', - ['--Toolbar-padInline' as any]: '40px', - }, - }, - Tab: { - dense: { - ['--Tab-padBlock' as any]: '4px', - ['--Tab-padInline' as any]: '8px', - ['--Tab-minHeight' as any]: '32px', - ['--Tab-iconSpacing' as any]: '2px', - }, - loose: { - ['--Tab-padBlock' as any]: '20px', - ['--Tab-padInline' as any]: '28px', - ['--Tab-minHeight' as any]: '72px', - ['--Tab-iconSpacing' as any]: '14px', - }, - }, - Tabs: { - dense: { - ['--Tabs-minHeight' as any]: '32px', - ['--Tab-padBlock' as any]: '4px', - ['--Tab-padInline' as any]: '8px', - ['--Tab-minHeight' as any]: '32px', - ['--Tab-iconSpacing' as any]: '2px', - }, - loose: { - ['--Tabs-minHeight' as any]: '72px', - ['--Tab-padBlock' as any]: '20px', - ['--Tab-padInline' as any]: '32px', - ['--Tab-minHeight' as any]: '72px', - ['--Tab-iconSpacing' as any]: '14px', - }, - }, - TablePagination: { - dense: { - ['--TablePagination-minHeight' as any]: '36px', - ['--TablePagination-actionsSpacing' as any]: '8px', - ['--TablePagination-selectSpacing' as any]: '12px', - }, - loose: { - ['--TablePagination-minHeight' as any]: '72px', - ['--TablePagination-actionsSpacing' as any]: '40px', - ['--TablePagination-selectSpacing' as any]: '56px', - }, - }, - CardContent: { - dense: { - ['--CardContent-pad' as any]: '8px', - ['--CardContent-padBottom' as any]: '10px', - }, - loose: { - ['--CardContent-pad' as any]: '32px', - ['--CardContent-padBottom' as any]: '40px', - }, - }, - Select: { - dense: { ['--Select-minHeight' as any]: '0.8em' }, - loose: { ['--Select-minHeight' as any]: '2.4em' }, - }, - Breadcrumbs: { - dense: { ['--Breadcrumbs-separatorGap' as any]: '2px' }, - loose: { ['--Breadcrumbs-separatorGap' as any]: '20px' }, - }, - InputAdornment: { - dense: { - ['--InputAdornment-small-gap' as any]: '2px', - ['--InputAdornment-medium-gap' as any]: '3px', - ['--InputAdornment-small-marginTop' as any]: '6px', - ['--InputAdornment-medium-marginTop' as any]: '10px', - }, - loose: { - ['--InputAdornment-small-gap' as any]: '16px', - ['--InputAdornment-medium-gap' as any]: '24px', - ['--InputAdornment-small-marginTop' as any]: '24px', - ['--InputAdornment-medium-marginTop' as any]: '32px', - }, - }, - Badge: { - dense: { - ['--Badge-standard-pad' as any]: '0 3px', - ['--Badge-standard-size' as any]: '14px', - ['--Badge-dot-size' as any]: '5px', - }, - loose: { - ['--Badge-standard-pad' as any]: '0 10px', - ['--Badge-standard-size' as any]: '28px', - ['--Badge-dot-size' as any]: '12px', - }, - }, - Checkbox: { - dense: { - ['--Checkbox-small-pad' as any]: '3px', - ['--Checkbox-medium-pad' as any]: '5px', - }, - loose: { - ['--Checkbox-small-pad' as any]: '7px', - ['--Checkbox-medium-pad' as any]: '13px', - }, - }, - Radio: { - dense: { - ['--Radio-small-pad' as any]: '3px', - ['--Radio-medium-pad' as any]: '5px', - }, - loose: { - ['--Radio-small-pad' as any]: '7px', - ['--Radio-medium-pad' as any]: '13px', - }, - }, - Switch: { - // Tune the interlocked dims + track gutter (pad); thumb pad/top/travel re-derive - // (touchSize == height keeps the thumb centered). thumbSize < height; width > - // touchSize; pad < height/2. - dense: { - ['--Switch-small-width' as any]: '32px', - ['--Switch-small-height' as any]: '18px', - ['--Switch-small-thumbSize' as any]: '12px', - ['--Switch-small-touchSize' as any]: '18px', - ['--Switch-small-pad' as any]: '5px', - ['--Switch-medium-width' as any]: '44px', - ['--Switch-medium-height' as any]: '24px', - ['--Switch-medium-thumbSize' as any]: '16px', - ['--Switch-medium-touchSize' as any]: '24px', - ['--Switch-medium-pad' as any]: '8px', - }, - loose: { - ['--Switch-small-width' as any]: '52px', - ['--Switch-small-height' as any]: '32px', - ['--Switch-small-thumbSize' as any]: '24px', - ['--Switch-small-touchSize' as any]: '32px', - ['--Switch-small-pad' as any]: '10px', - ['--Switch-medium-width' as any]: '76px', - ['--Switch-medium-height' as any]: '48px', - ['--Switch-medium-thumbSize' as any]: '34px', - ['--Switch-medium-touchSize' as any]: '48px', - ['--Switch-medium-pad' as any]: '16px', - }, - }, }; -// TextField rides the same OutlinedInput tokens; OutlinedInput's `:has` rule -// drives the label's --InputLabel-y, so input box + label move together. -scopes.TextField = scopes.OutlinedInput; export default function DensityFixture() { const router = useRouter(); diff --git a/docs/pages/experiments/density-showcase.tsx b/docs/pages/experiments/density-showcase.tsx deleted file mode 100644 index 922df9539b1059..00000000000000 --- a/docs/pages/experiments/density-showcase.tsx +++ /dev/null @@ -1,282 +0,0 @@ -'use client'; -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Paper from '@mui/material/Paper'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Divider from '@mui/material/Divider'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Accordion from '@mui/material/Accordion'; -import AccordionSummary from '@mui/material/AccordionSummary'; -import AccordionDetails from '@mui/material/AccordionDetails'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Switch from '@mui/material/Switch'; -import CssBaseline from '@mui/material/CssBaseline'; -import { createTheme, ThemeProvider, enhanceDensity, DensityScale } from '@mui/material/styles'; -import demos from 'docs/src/modules/components/densityDemos'; - -// Client-facing showcase for the CSS-var density adapter. -// Three presets each map to one `enhanceDensity(theme, { scale })` call — the -// only knob is the 7-step density scale. Flip a preset -> the whole gallery -// reflows because every component pulls its sized tokens from `--mui-density-*`. - -type PresetKey = 'compact' | 'normal' | 'comfort'; - -// `normal` = enhanceDensity defaults (theme.spacing) -> pixel-identical to today. -// compact/comfort override every step explicitly. -const presetScales: Record | undefined> = { - compact: { - xxs: '2px', - xs: '4px', - sm: '6px', - md: '8px', - lg: '12px', - xl: '18px', - xxl: '24px', - }, - normal: undefined, - comfort: { - xxs: '6px', - xs: '8px', - sm: '12px', - md: '16px', - lg: '24px', - xl: '32px', - xxl: '40px', - }, -}; - -const presetLabels: Record = { - compact: 'Compact', - normal: 'Normal', - comfort: 'Comfort', -}; - -const scaleKeys: (keyof DensityScale)[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; - -// Build the three enhanced themes once. -const baseTheme = createTheme({ cssVariables: true }); -const themes: Record> = { - compact: enhanceDensity(createTheme({ cssVariables: true }), { scale: presetScales.compact }), - normal: enhanceDensity(createTheme({ cssVariables: true })), - comfort: enhanceDensity(createTheme({ cssVariables: true }), { scale: presetScales.comfort }), -}; - -// Clean px readout for the scale. Under `cssVariables`, `theme.spacing()` returns -// a `calc(... var(--mui-spacing) ...)` string, so for the Normal preset we read -// the resolved px from a non-css-var theme instead. -const displayScales: Record = { - compact: presetScales.compact as DensityScale, - normal: enhanceDensity(createTheme()).density, - comfort: presetScales.comfort as DensityScale, -}; - -// Pull the density-var mappings enhanceDensity injected per component. Each -// component's `styleOverrides.root` is `[originalRoot, { '--Component-*': ... }]`; -// scan every plain-object element for the top-level `--*` token entries. -type VarMap = Record; -function collectComponentVars(theme: ReturnType): Record { - const out: Record = {}; - const components = (theme.components ?? {}) as Record; - Object.keys(components).forEach((name) => { - if (name === 'MuiCssBaseline') { - return; - } - const root = components[name]?.styleOverrides?.root; - const elements = Array.isArray(root) ? root : [root]; - const vars: VarMap = {}; - elements.forEach((el) => { - if (!el || typeof el !== 'object') { - return; - } - Object.keys(el).forEach((key) => { - if (key.startsWith('--')) { - vars[key] = String(el[key]); - } - }); - }); - if (Object.keys(vars).length > 0) { - out[name.replace(/^Mui/, '')] = vars; - } - }); - return out; -} - -const componentVarsByPreset: Record> = { - compact: collectComponentVars(themes.compact), - normal: collectComponentVars(themes.normal), - comfort: collectComponentVars(themes.comfort), -}; - -const mono = { - fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', - fontSize: 12, -} as const; - -// Outline every box inside a demo so the density effect (padding/height shifts) -// is visible at a glance. `outline` is drawn outside the box and takes no layout -// space, so toggling it never reflows the gallery — the geometry stays honest. -const demoOutlineSx = { - '& *': { - outline: '1px solid rgba(244, 67, 54, 0.5)', - outlineOffset: '-1px', - }, -} as const; - -function ScalePanel({ preset }: { preset: PresetKey }) { - const scale = displayScales[preset]; - return ( -
- - Density scale - - - {scaleKeys.map((key) => ( - - {`--mui-density-${key}`} - {scale[key]} - - ))} - -
- ); -} - -function VarsPanel({ preset }: { preset: PresetKey }) { - const byComponent = componentVarsByPreset[preset]; - const names = Object.keys(byComponent); - return ( -
- - Component tokens ({names.length}) - - - {names.map((name) => ( - - } sx={{ minHeight: 36, px: 0 }}> - {name} - - - - {Object.entries(byComponent[name]).map(([key, value]) => ( - - - {key} - - {': '} - - {value} - - - ))} - - - - ))} - -
- ); -} - -export default function DensityShowcase() { - const [preset, setPreset] = React.useState('normal'); - const [outline, setOutline] = React.useState(false); - - return ( - // Outer shell uses the base theme; the gallery + sidebar readouts use the - // enhanced theme so they reflect the active preset. - - - - - Density presets - - - One scale drives every component. Normal is pixel-identical to today. - - next && setPreset(next)} - sx={{ mb: 2 }} - > - {(Object.keys(presetLabels) as PresetKey[]).map((key) => ( - - {presetLabels[key]} - - ))} - - setOutline(event.target.checked)} - /> - } - label="Outline demos" - sx={{ mb: 2, display: 'flex' }} - /> - - - - - - - - - - {Object.keys(demos).map((name) => ( - - - {name} - - {demos[name]} - - ))} - - - - - - ); -} diff --git a/docs/pages/experiments/density-tokens.tsx b/docs/pages/experiments/density-tokens.tsx deleted file mode 100644 index df2c6cf06f4b36..00000000000000 --- a/docs/pages/experiments/density-tokens.tsx +++ /dev/null @@ -1,280 +0,0 @@ -'use client'; -import * as React from 'react'; -import { createTheme, ThemeProvider, enhanceDensity } from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import InputAdornment from '@mui/material/InputAdornment'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Paper from '@mui/material/Paper'; -import Slider from '@mui/material/Slider'; -import Stack from '@mui/material/Stack'; -import Switch from '@mui/material/Switch'; -import TextField from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; - -// Density experiment — CSS-var adapter. -// Agnostic layer: Button consumes `var(--Button-pad, var(--_pad))`. Material UI -// layer sets the (variant, size) literal default `--_pad` and the built-in-size -// routing `--Button-pad: var(--Button--pad, var(--_pad))` in `variants` -// (custom sizes route inline). `enhanceDensity` wires the sized tokens to the -// `--mui-density-*` scale. OutlinedInput applies the same model block-only: the -// root routes `--OutlinedInput-padBlock`, the input inherits it. - -const VARIANTS = ['text', 'outlined', 'contained'] as const; -const SIZES = ['small', 'medium', 'large'] as const; - -const theme = enhanceDensity(createTheme({ cssVariables: true })); - -function ButtonMatrix() { - return ( - - {VARIANTS.map((variant) => ( - - - {variant} - - - {SIZES.map((size) => ( - - ))} - - - ))} - - ); -} - -function OutlinedInputMatrix() { - return ( - - {(['small', 'medium'] as const).map((size) => ( - - - - @} - /> - - {`label ${size}`} - - - - ))} - - ); -} - -function Panel({ - title, - caption, - style, - children, -}: { - title: string; - caption: string; - style?: React.CSSProperties; - children: React.ReactNode; -}) { - return ( - - {title} - - {caption} - - {children} - - ); -} - -export default function DensityTokens() { - // --mui-density-* live retune (overrides the scale at this scope). - const [densityXs, setDensityXs] = React.useState(6); - const [densityLg, setDensityLg] = React.useState(16); - // Per-token overrides (sized-only). - const [smallPad, setSmallPad] = React.useState(''); - const [largePad, setLargePad] = React.useState(''); - // OutlinedInput block-density overrides. - const [smallPadBlock, setSmallPadBlock] = React.useState(''); - const [mediumPadBlock, setMediumPadBlock] = React.useState(''); - - const densityScope: React.CSSProperties = { - // Retunes every enhanced button without rebuilding the theme. - ['--mui-density-xs' as any]: `${densityXs}px`, - ['--mui-density-lg' as any]: `${densityLg}px`, - }; - - const tokenScope: React.CSSProperties = { - ...(smallPad ? { ['--Button-small-pad' as any]: smallPad } : null), - ...(largePad ? { ['--Button-large-pad' as any]: largePad } : null), - }; - - const inputTokenScope: React.CSSProperties = { - ...(smallPadBlock ? { ['--OutlinedInput-small-padBlock' as any]: smallPadBlock } : null), - ...(mediumPadBlock ? { ['--OutlinedInput-medium-padBlock' as any]: mediumPadBlock } : null), - }; - - return ( - - - - - - Density tokens — CSS-var adapter - - - The agnostic layer consumes --Button-pad; the Material UI layer feeds it - inline through the sized token --Button-<size>-pad over an internal - literal default, so the default is pixel-identical. Resolution is sized-only (no all-sizes - base token). enhanceDensity wires the sized tokens to the{' '} - --mui-density-* scale. - - - - - - - - - - - - - - Density scale - - - --mui-density-xs (medium block): {densityXs}px - - setDensityXs(value as number)} - /> - - - - --mui-density-lg (medium inline): {densityLg}px - - setDensityLg(value as number)} - /> - - - - - - - Per-token override (granular) - setSmallPad(event.target.value)} - /> - setLargePad(event.target.value)} - /> - - - Scoped preview - - - - - - - - - - OutlinedInput — block density - - - Input density is vertical only: the root routes the size-resolved{' '} - --OutlinedInput-padBlock and the input inherits it; the{' '} - 14px inline gutter is constant. Set{' '} - --OutlinedInput-<size>-padBlock to retune — it reflows the input - (non-multiline) and the root (multiline) together, across adornments. The last column - is a FormControl + InputLabel + OutlinedInput: OutlinedInput - reaches its sibling label via :has and sets --InputLabel-y from - the same token, so the resting label stays centered under override. - - - - - - - - - - - setSmallPadBlock(event.target.value)} - /> - setMediumPadBlock(event.target.value)} - /> - - - - enhanceDensity toggle - - } - label="enhanceDensity is applied to this page's theme (createTheme is untouched)." - /> - - - ); -} diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index d46274c4e0c749..309bad12e6616b 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -1,53 +1,10 @@ import * as React from 'react'; -import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import TextField from '@mui/material/TextField'; -import InputAdornment from '@mui/material/InputAdornment'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import Chip from '@mui/material/Chip'; -import Avatar from '@mui/material/Avatar'; -import IconButton from '@mui/material/IconButton'; -import MenuItem from '@mui/material/MenuItem'; -import MenuList from '@mui/material/MenuList'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListSubheader from '@mui/material/ListSubheader'; -import Toolbar from '@mui/material/Toolbar'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableRow from '@mui/material/TableRow'; -import TablePagination from '@mui/material/TablePagination'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardActions from '@mui/material/CardActions'; -import Select from '@mui/material/Select'; -import Breadcrumbs from '@mui/material/Breadcrumbs'; -import Link from '@mui/material/Link'; -import Badge from '@mui/material/Badge'; -import Checkbox from '@mui/material/Checkbox'; -import Radio from '@mui/material/Radio'; -import Switch from '@mui/material/Switch'; -import Typography from '@mui/material/Typography'; -import FaceIcon from '@mui/icons-material/Face'; -import DeleteIcon from '@mui/icons-material/Delete'; -import InboxIcon from '@mui/icons-material/Inbox'; -import DraftsIcon from '@mui/icons-material/Drafts'; -import FavoriteIcon from '@mui/icons-material/Favorite'; -import VisibilityIcon from '@mui/icons-material/Visibility'; -// Shared density demo matrices for the CSS-var density adapter. -// Consumed by both the screenshot fixture (density-fixture) and the client -// showcase (density-showcase). Each entry renders one component's load-bearing -// size/variant matrix. `level=default` (no token overrides) must stay -// pixel-identical to the pre-change baseline. +// Shared density demo matrix for the CSS-var density adapter (Button only). +// Consumed by the screenshot fixture (density-fixture). `level=default` (no token +// overrides) must stay pixel-identical to the pre-change baseline. const demos: Record = { Button: ( @@ -66,550 +23,6 @@ const demos: Record = { ))} ), - OutlinedInput: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - @} - /> - - ))} - - ), - TextField: ( - - {(['medium', 'small'] as const).map((size) => ( - - {`outlined ${size}`} - - - ))} - - $, - endAdornment: kg, - }, - }} - /> - - - ), - Chip: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - A} /> - } /> - {}} /> - {}} - icon={} - /> - - ))} - - ), - IconButton: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - MenuItem: ( - // MenuItem requires a MenuList/Menu ancestor (MenuListContext). - - Default item - Selected item - - - - - With icon - - With divider - No gutters - Dense item - - - - - Dense + icon - - - Dense no gutters - - - ), - ListItem: ( - - - Default item - - - - } - > - With secondary action - - Divider item - - - - - - Dense item - - - - } - > - Dense with action - - Dense, no gutters - - - ), - ListItemButton: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - ListItemIcon: ( - - - - - - - - - - - - - - - - - - - - - - - - - ), - ListItemText: ( - - {([false, true] as const).map((dense) => ( - - - - - - - - - - - - - - - ))} - - ), - ListSubheader: ( - - - Gutters (default) - - - Inset - - - Disable gutters - - - Primary color - - - ), - Toolbar: ( - - {(['regular', 'dense'] as const).map((variant) => ( - - - {variant} gutters - action - - - {variant} no-gutters - action - - - ))} - - ), - Tab: ( - // Tab requires a Tabs ancestor (RovingTabIndexContext). - - {}}> - - - - - {}}> - } label="Top" iconPosition="top" value={0} /> - } label="Bottom" iconPosition="bottom" value={1} /> - } label="Start" iconPosition="start" value={2} /> - } label="End" iconPosition="end" value={3} /> - - {}}> - } aria-label="icon only" value={0} /> - - - - ), - Tabs: ( - - - - - - - - } label="Top" iconPosition="top" /> - } label="Bottom" iconPosition="bottom" /> - } label="Start" iconPosition="start" /> - } label="End" iconPosition="end" /> - - - } aria-label="fav" /> - } aria-label="del" /> - - - ), - TablePagination: ( - - - - - {}} - onRowsPerPageChange={() => {}} - /> - - - {}} - onRowsPerPageChange={() => {}} - /> - - - {}} - onRowsPerPageChange={() => {}} - /> - - -
-
- ), - CardContent: ( - - - - Default - All-sides padding via --CardContent-pad. - - - - - Last-child - - Extra bottom inset (--CardContent-padBottom) since this is the last child. - - - - - - Above actions - Not last child -> base pad only on the bottom. - - - - - - - ), - Select: ( - - - - - - - ), - Breadcrumbs: ( - - - - Home - - - Catalog - - Shoes - - - - Home - - - Library - - - Data - - Reports - - - - Home - - - Catalog - - - Accessories - - Belts - - - ), - InputAdornment: ( - - {(['small', 'medium'] as const).map((size) => ( - - $ }, - }} - /> - - - - - - ), - }, - }} - /> - - ))} - - ), - Badge: ( - - - - - - - - - - - - - - - - - - ), - Checkbox: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - - ))} - - ), - Radio: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - ))} - - ), - Switch: ( - - {(['small', 'medium'] as const).map((size) => ( - - - - - - - - ))} - - ), }; export default demos; diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 5ebf4072cb4e77..45f29b514875a6 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -80,14 +80,10 @@ const BadgeBadge = styled('span', { fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontWeightMedium, fontSize: theme.typography.pxToRem(12), - '--_pad': '0 6px', - '--_size': `${RADIUS_STANDARD * 2}px`, - '--Badge-pad': 'var(--Badge-standard-pad, var(--_pad))', - '--Badge-size': 'var(--Badge-standard-size, var(--_size))', - minWidth: 'var(--Badge-size, var(--_size))', + minWidth: RADIUS_STANDARD * 2, lineHeight: 1, - padding: 'var(--Badge-pad, var(--_pad))', - height: 'var(--Badge-size, var(--_size))', + padding: '0 6px', + height: RADIUS_STANDARD * 2, borderRadius: RADIUS_STANDARD, zIndex: 1, // Render the badge on top of potential ripples. '@media (forced-colors: active)': { @@ -110,14 +106,10 @@ const BadgeBadge = styled('span', { { props: { variant: 'dot' }, style: { - '--_pad': '0px', - '--_size': `${RADIUS_DOT * 2}px`, - '--Badge-pad': 'var(--Badge-dot-pad, var(--_pad))', - '--Badge-size': 'var(--Badge-dot-size, var(--_size))', borderRadius: RADIUS_DOT, - height: 'var(--Badge-size, var(--_size))', - minWidth: 'var(--Badge-size, var(--_size))', - padding: 'var(--Badge-pad, var(--_pad))', + height: RADIUS_DOT * 2, + minWidth: RADIUS_DOT * 2, + padding: 0, }, }, { diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js index 91e2f297f9e326..63cb6bae7f3538 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js @@ -49,11 +49,10 @@ const BreadcrumbsSeparator = styled('li', { name: 'MuiBreadcrumbs', slot: 'Separator', })({ - '--_separatorGap': '8px', display: 'flex', userSelect: 'none', - marginLeft: 'var(--Breadcrumbs-separatorGap, var(--_separatorGap))', - marginRight: 'var(--Breadcrumbs-separatorGap, var(--_separatorGap))', + marginLeft: 8, + marginRight: 8, }); function insertSeparators(items, className, separator, ownerState) { diff --git a/packages/mui-material/src/CardContent/CardContent.js b/packages/mui-material/src/CardContent/CardContent.js index 6e447fd831cfa2..769163a78edbad 100644 --- a/packages/mui-material/src/CardContent/CardContent.js +++ b/packages/mui-material/src/CardContent/CardContent.js @@ -21,11 +21,9 @@ const CardContentRoot = styled('div', { name: 'MuiCardContent', slot: 'Root', })({ - '--_pad': '16px', - '--_padBottom': '24px', - padding: 'var(--CardContent-pad, var(--_pad))', + padding: 16, '&:last-child': { - paddingBottom: 'var(--CardContent-padBottom, var(--_padBottom))', + paddingBottom: 24, }, }); diff --git a/packages/mui-material/src/Checkbox/Checkbox.js b/packages/mui-material/src/Checkbox/Checkbox.js index b13141cbe2527e..ef4cbfc2c328f7 100644 --- a/packages/mui-material/src/Checkbox/Checkbox.js +++ b/packages/mui-material/src/Checkbox/Checkbox.js @@ -55,16 +55,6 @@ const CheckboxRoot = styled(SwitchBase, { memoTheme(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, variants: [ - // Density: route the per-size public token into SwitchBase's seam. Default - // 9px both sizes (pixel-identical); size enables per-size density tuning. - { - props: { size: 'small' }, - style: { '--SwitchBase-pad': 'var(--Checkbox-small-pad, var(--_pad))' }, - }, - { - props: { size: 'medium' }, - style: { '--SwitchBase-pad': 'var(--Checkbox-medium-pad, var(--_pad))' }, - }, { props: { color: 'default', disableRipple: false }, style: { diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index 1348181b10f6af..2f35de5dfc8d04 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -73,11 +73,7 @@ const ChipRoot = styled('div', { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - // Agnostic seam: the styled root reads `--Chip-height`; `--_height` is the - // universal default (today's medium height). Size variants route the public - // sized token over it. - '--_height': '32px', - height: 'var(--Chip-height, var(--_height))', + height: 32, lineHeight: 1.5, color: (theme.vars || theme).palette.text.primary, backgroundColor: (theme.vars || theme).palette.action.selected, @@ -97,27 +93,22 @@ const ChipRoot = styled('div', { opacity: (theme.vars || theme).palette.action.disabledOpacity, pointerEvents: 'none', }, - // Density adapter: avatar/icon/deleteIcon scale with the - // chip height. Each is `calc(var(--Chip-height) - inset)` where the inset - // reproduces today's medium size (height 32: avatar/icon 24, deleteIcon 22); - // the small variant overrides the inset for height 24. [`& .${chipClasses.avatar}`]: { marginLeft: 5, marginRight: -6, - width: 'calc(var(--Chip-height, var(--_height)) - 8px)', - height: 'calc(var(--Chip-height, var(--_height)) - 8px)', + width: 24, + height: 24, color: theme.vars ? theme.vars.palette.Chip.defaultAvatarColor : textColor, fontSize: theme.typography.pxToRem(12), }, [`& .${chipClasses.icon}`]: { marginLeft: 5, marginRight: -6, - fontSize: 'calc(var(--Chip-height, var(--_height)) - 8px)', }, [`& .${chipClasses.deleteIcon}`]: { WebkitTapHighlightColor: 'transparent', color: theme.alpha((theme.vars || theme).palette.text.primary, 0.26), - fontSize: 'calc(var(--Chip-height, var(--_height)) - 10px)', + fontSize: 22, cursor: 'pointer', margin: '0 5px 0 -6px', '&:hover': { @@ -125,16 +116,6 @@ const ChipRoot = styled('div', { }, }, variants: [ - // Built-in size routing (CSS, deduped) — exposes the public sized token - // over the internal default. Custom sizes are routed inline instead. - { - props: { size: 'small' }, - style: { '--Chip-height': 'var(--Chip-small-height, var(--_height))' }, - }, - { - props: { size: 'medium' }, - style: { '--Chip-height': 'var(--Chip-medium-height, var(--_height))' }, - }, { props: { color: 'primary', @@ -160,21 +141,21 @@ const ChipRoot = styled('div', { { props: { size: 'small' }, style: { - '--_height': '24px', // small default; medium default lives in base + height: 24, [`& .${chipClasses.avatar}`]: { marginLeft: 4, marginRight: -4, - width: 'calc(var(--Chip-height, var(--_height)) - 6px)', - height: 'calc(var(--Chip-height, var(--_height)) - 6px)', + width: 18, + height: 18, fontSize: theme.typography.pxToRem(10), }, [`& .${chipClasses.icon}`]: { - fontSize: 'calc(var(--Chip-height, var(--_height)) - 6px)', + fontSize: 18, marginLeft: 4, marginRight: -4, }, [`& .${chipClasses.deleteIcon}`]: { - fontSize: 'calc(var(--Chip-height, var(--_height)) - 8px)', + fontSize: 16, marginRight: 4, marginLeft: -4, }, @@ -219,9 +200,7 @@ const ChipRoot = styled('div', { [`&.${chipClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.focusOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, ), }, }, @@ -247,17 +226,13 @@ const ChipRoot = styled('div', { '&:hover': { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.hoverOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, ), }, [`&.${chipClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.action.selected, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.focusOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, ), }, '&:active': { @@ -353,40 +328,29 @@ const ChipLabel = styled('span', { })({ overflow: 'hidden', textOverflow: 'ellipsis', - // Agnostic seam: the label reads `--Chip-padInline` on both sides; `--_padInline` - // is the universal default (today's medium filled inline padding). Variants - // specialize the default per (variant, size) and size variants route the public - // sized token over it. - '--_padInline': '12px', - paddingLeft: 'var(--Chip-padInline, var(--_padInline))', - paddingRight: 'var(--Chip-padInline, var(--_padInline))', + paddingLeft: 12, + paddingRight: 12, whiteSpace: 'nowrap', variants: [ - // Built-in size routing (CSS, deduped). Custom sizes are routed inline. - { - props: { size: 'small' }, - style: { '--Chip-padInline': 'var(--Chip-small-padInline, var(--_padInline))' }, - }, - { - props: { size: 'medium' }, - style: { '--Chip-padInline': 'var(--Chip-medium-padInline, var(--_padInline))' }, - }, { props: { variant: 'outlined' }, style: { - '--_padInline': '11px', // medium outlined default + paddingLeft: 11, + paddingRight: 11, }, }, { props: { size: 'small' }, style: { - '--_padInline': '8px', // small filled default + paddingLeft: 8, + paddingRight: 8, }, }, { props: { size: 'small', variant: 'outlined' }, style: { - '--_padInline': '7px', // small outlined default + paddingLeft: 7, + paddingRight: 7, }, }, ], @@ -478,21 +442,6 @@ const Chip = React.forwardRef(function Chip(inProps, ref) { const classes = useUtilityClasses(ownerState); - // Material UI layer: built-in sizes route the public sized tokens via variants - // (deduped CSS). A custom size has no such variant, so route it inline on the - // root — the tokens inherit down to the label. The `--_*` defaults live in the - // variants. - const densityVars = - size === 'small' || size === 'medium' - ? undefined - : { - '--Chip-height': `var(--Chip-${size}-height, var(--_height))`, - // padInline is consumed on the label (a child); the root doesn't own - // `--_padInline`, so a cross-element route must fall back to a literal, - // never the internal var (else a missing token resolves to invalid -> 0). - '--Chip-padInline': `var(--Chip-${size}-padInline, 12px)`, - }; - const moreProps = component === ButtonBase ? { @@ -560,7 +509,6 @@ const Chip = React.forwardRef(function Chip(inProps, ref) { disabled: clickable && disabled ? true : undefined, tabIndex: skipFocusWhenDisabled && disabled ? -1 : tabIndex, ...moreProps, - ...(densityVars && { style: densityVars }), }, getSlotProps: (handlers) => ({ ...handlers, diff --git a/packages/mui-material/src/IconButton/IconButton.js b/packages/mui-material/src/IconButton/IconButton.js index e2cfe9b88496e1..7bc22dce98e66b 100644 --- a/packages/mui-material/src/IconButton/IconButton.js +++ b/packages/mui-material/src/IconButton/IconButton.js @@ -15,9 +15,6 @@ import capitalize from '../utils/capitalize'; import iconButtonClasses, { getIconButtonUtilityClass } from './iconButtonClasses'; import { getTransitionStyles } from '../transitions/utils'; -// Built-in sizes route padding via variants; any other size routes inline. -const iconButtonSizes = ['small', 'medium', 'large']; - const useUtilityClasses = (ownerState) => { const { classes, disabled, color, edge, size, loading } = ownerState; @@ -56,31 +53,13 @@ const IconButtonRoot = styled(ButtonBase, { textAlign: 'center', flex: '0 0 auto', fontSize: theme.typography.pxToRem(24), - // Agnostic layer: the only spacing surface the styled root reads. `--_pad` - // is the universal default (today's medium padding); size variants specialize - // it, so a custom size still gets a sane value. - '--_pad': '8px', - padding: 'var(--IconButton-pad, var(--_pad))', + padding: 8, borderRadius: '50%', color: (theme.vars || theme).palette.action.active, ...getTransitionStyles(theme, 'background-color', { duration: theme.transitions.duration.shortest, }), variants: [ - // Built-in size routing (CSS, deduped) — exposes the public sized token - // over the internal default. Custom sizes are routed inline instead. - { - props: { size: 'small' }, - style: { '--IconButton-pad': 'var(--IconButton-small-pad, var(--_pad))' }, - }, - { - props: { size: 'medium' }, - style: { '--IconButton-pad': 'var(--IconButton-medium-pad, var(--_pad))' }, - }, - { - props: { size: 'large' }, - style: { '--IconButton-pad': 'var(--IconButton-large-pad, var(--_pad))' }, - }, { props: (props) => !props.disableRipple, style: { @@ -146,14 +125,14 @@ const IconButtonRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - '--_pad': '5px', + padding: 5, fontSize: theme.typography.pxToRem(18), }, }, { props: { size: 'large' }, style: { - '--_pad': '12px', + padding: 12, fontSize: theme.typography.pxToRem(28), }, }, @@ -220,14 +199,6 @@ const IconButton = React.forwardRef(function IconButton(inProps, ref) { const classes = useUtilityClasses(ownerState); - // Material UI layer: built-in sizes route the public sized token via variants - // (deduped CSS). A custom size has no such variant, so route it inline — the - // token name carries the runtime size string, keeping custom sizes tunable for - // free. The `--_pad` default lives in the variants. - const densityVars = iconButtonSizes.includes(size) - ? undefined - : { '--IconButton-pad': `var(--IconButton-${size}-pad, var(--_pad))` }; - return ( {typeof loading === 'boolean' && ( @@ -358,10 +328,6 @@ IconButton.propTypes /* remove-proptypes */ = { PropTypes.oneOf(['small', 'medium', 'large']), PropTypes.string, ]), - /** - * @ignore - */ - style: PropTypes.object, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ diff --git a/packages/mui-material/src/InputAdornment/InputAdornment.js b/packages/mui-material/src/InputAdornment/InputAdornment.js index d268c3e07a5388..abb06efb23735f 100644 --- a/packages/mui-material/src/InputAdornment/InputAdornment.js +++ b/packages/mui-material/src/InputAdornment/InputAdornment.js @@ -50,21 +50,7 @@ const InputAdornmentRoot = styled('div', { alignItems: 'center', whiteSpace: 'nowrap', color: (theme.vars || theme).palette.action.active, - // Internal defaults (Material literals). `size` is read from FormControl with no - // default (can be undefined) -> medium routing lives in base, not a `{ size: 'medium' }` variant. - '--_gap': '8px', - '--_marginTop': '16px', - '--InputAdornment-gap': 'var(--InputAdornment-medium-gap, var(--_gap))', - '--InputAdornment-marginTop': 'var(--InputAdornment-medium-marginTop, var(--_marginTop))', variants: [ - // Built-in size routing — exposes the public sized token over the internal default. - { - props: { size: 'small' }, - style: { - '--InputAdornment-gap': 'var(--InputAdornment-small-gap, var(--_gap))', - '--InputAdornment-marginTop': 'var(--InputAdornment-small-marginTop, var(--_marginTop))', - }, - }, { props: { variant: 'filled', @@ -72,7 +58,7 @@ const InputAdornmentRoot = styled('div', { style: { [`&.${inputAdornmentClasses.positionStart}&:not(.${inputAdornmentClasses.hiddenLabel})`]: { - marginTop: 'var(--InputAdornment-marginTop, var(--_marginTop))', + marginTop: 16, }, }, }, @@ -81,7 +67,7 @@ const InputAdornmentRoot = styled('div', { position: 'start', }, style: { - marginRight: 'var(--InputAdornment-gap, var(--_gap))', + marginRight: 8, }, }, { @@ -89,7 +75,7 @@ const InputAdornmentRoot = styled('div', { position: 'end', }, style: { - marginLeft: 'var(--InputAdornment-gap, var(--_gap))', + marginLeft: 8, }, }, { diff --git a/packages/mui-material/src/InputLabel/InputLabel.js b/packages/mui-material/src/InputLabel/InputLabel.js index b9f4bbda2819a4..f62e71601fce90 100644 --- a/packages/mui-material/src/InputLabel/InputLabel.js +++ b/packages/mui-material/src/InputLabel/InputLabel.js @@ -145,9 +145,7 @@ const InputLabelRoot = styled(FormLabel, { // see comment above on filled.zIndex zIndex: 1, pointerEvents: 'none', - // Resting Y is a generic seam; the sibling input owns its value (see - // OutlinedInput's `:has` rule). Default is today's literal. - transform: 'translate(14px, var(--comp-labelY, 16px)) scale(1)', + transform: 'translate(14px, 16px) scale(1)', maxWidth: 'calc(100% - 24px)', }, }, @@ -157,7 +155,7 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - transform: 'translate(14px, var(--comp-labelY, 9px)) scale(1)', + transform: 'translate(14px, 9px) scale(1)', }, }, { diff --git a/packages/mui-material/src/ListItem/ListItem.js b/packages/mui-material/src/ListItem/ListItem.js index ecc8ee8c65d812..bc9d4b0946a4c3 100644 --- a/packages/mui-material/src/ListItem/ListItem.js +++ b/packages/mui-material/src/ListItem/ListItem.js @@ -58,38 +58,26 @@ export const ListItemRoot = styled('div', { width: '100%', boxSizing: 'border-box', textAlign: 'left', - // Density adapter: `dense` is the compactness axis (boolean). Default state = - // plain seam `--ListItem-` over `--_`; the `dense` variant re-routes - // the seam to `--ListItem-dense-`. - '--_padBlock': '8px', - '--_padInline': '16px', variants: [ { props: ({ ownerState }) => !ownerState.disablePadding, style: { - paddingTop: 'var(--ListItem-padBlock, var(--_padBlock))', - paddingBottom: 'var(--ListItem-padBlock, var(--_padBlock))', + paddingTop: 8, + paddingBottom: 8, }, }, { props: ({ ownerState }) => !ownerState.disablePadding && ownerState.dense, style: { - '--_padBlock': '4px', - '--ListItem-padBlock': 'var(--ListItem-dense-padBlock, var(--_padBlock))', + paddingTop: 4, + paddingBottom: 4, }, }, { props: ({ ownerState }) => !ownerState.disablePadding && !ownerState.disableGutters, style: { - paddingLeft: 'var(--ListItem-padInline, var(--_padInline))', - paddingRight: 'var(--ListItem-padInline, var(--_padInline))', - }, - }, - { - props: ({ ownerState }) => - !ownerState.disablePadding && !ownerState.disableGutters && ownerState.dense, - style: { - '--ListItem-padInline': 'var(--ListItem-dense-padInline, var(--_padInline))', + paddingLeft: 16, + paddingRight: 16, }, }, { diff --git a/packages/mui-material/src/ListItemButton/ListItemButton.js b/packages/mui-material/src/ListItemButton/ListItemButton.js index 80dfd2c4b9dd6a..32e6699882e90e 100644 --- a/packages/mui-material/src/ListItemButton/ListItemButton.js +++ b/packages/mui-material/src/ListItemButton/ListItemButton.js @@ -65,13 +65,8 @@ const ListItemButtonRoot = styled(ButtonBase, { minWidth: 0, boxSizing: 'border-box', textAlign: 'left', - // Density adapter: `dense` is the compactness axis (boolean). Default state = - // plain seam `--ListItemButton-` over `--_`; the `dense` variant - // re-routes the seam to `--ListItemButton-dense-`. - '--_padBlock': '8px', - '--_padInline': '16px', - paddingTop: 'var(--ListItemButton-padBlock, var(--_padBlock))', - paddingBottom: 'var(--ListItemButton-padBlock, var(--_padBlock))', + paddingTop: 8, + paddingBottom: 8, ...getTransitionStyles(theme, 'background-color', { duration: theme.transitions.duration.shortest, }), @@ -91,18 +86,14 @@ const ListItemButtonRoot = styled(ButtonBase, { [`&.${listItemButtonClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.focusOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, ), }, }, [`&.${listItemButtonClasses.selected}:hover`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.hoverOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, ), // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { @@ -137,23 +128,15 @@ const ListItemButtonRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - // gutters owns the inline axis (default state = plain seam). - paddingLeft: 'var(--ListItemButton-padInline, var(--_padInline))', - paddingRight: 'var(--ListItemButton-padInline, var(--_padInline))', + paddingLeft: 16, + paddingRight: 16, }, }, { props: ({ ownerState }) => ownerState.dense, style: { - '--_padBlock': '4px', // dense default; routes the dense block token - '--ListItemButton-padBlock': 'var(--ListItemButton-dense-padBlock, var(--_padBlock))', - }, - }, - { - props: ({ ownerState }) => ownerState.dense && !ownerState.disableGutters, - style: { - // gutters + dense: re-route inline to the dense token (block already routed above). - '--ListItemButton-padInline': 'var(--ListItemButton-dense-padInline, var(--_padInline))', + paddingTop: 4, + paddingBottom: 4, }, }, ], diff --git a/packages/mui-material/src/ListItemIcon/ListItemIcon.js b/packages/mui-material/src/ListItemIcon/ListItemIcon.js index fd51b98c1f6353..81aeb8629aede4 100644 --- a/packages/mui-material/src/ListItemIcon/ListItemIcon.js +++ b/packages/mui-material/src/ListItemIcon/ListItemIcon.js @@ -29,8 +29,7 @@ const ListItemIconRoot = styled('div', { }, })( memoTheme(({ theme }) => ({ - '--_minWidth': theme.spacing(4.5), - minWidth: 'var(--ListItemIcon-minWidth, var(--_minWidth))', + minWidth: theme.spacing(4.5), color: (theme.vars || theme).palette.action.active, flexShrink: 0, display: 'inline-flex', diff --git a/packages/mui-material/src/ListItemText/ListItemText.js b/packages/mui-material/src/ListItemText/ListItemText.js index b836d2ce9806fc..8b81317e5d2955 100644 --- a/packages/mui-material/src/ListItemText/ListItemText.js +++ b/packages/mui-material/src/ListItemText/ListItemText.js @@ -40,16 +40,8 @@ const ListItemTextRoot = styled('div', { })({ flex: '1 1 auto', minWidth: 0, - // Density adapter: each spacing literal becomes - // `var(--seam, var(--_))`, tokenized in place. The compactness dimension - // is `dense` (boolean) — default state = plain seam `--ListItemText-` over - // `--_`; the dense variant re-routes the seam to its own token. - // `marginBlock` (top+bottom move together) varies by `multiline`, so its literal - // default is set per (dense × multiline) cell while routing keys on dense only. - // `insetPad` is the inset indentation. - '--_marginBlock': '4px', - marginTop: 'var(--ListItemText-marginBlock, var(--_marginBlock))', - marginBottom: 'var(--ListItemText-marginBlock, var(--_marginBlock))', + 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', @@ -58,29 +50,17 @@ const ListItemTextRoot = styled('div', { display: 'block', }, variants: [ - { - props: ({ ownerState }) => ownerState.dense, - style: { - '--ListItemText-marginBlock': 'var(--ListItemText-dense-marginBlock, var(--_marginBlock))', - }, - }, { props: ({ ownerState }) => ownerState.primary && ownerState.secondary, style: { - '--_marginBlock': '6px', + marginTop: 6, + marginBottom: 6, }, }, { props: ({ ownerState }) => ownerState.inset, style: { - '--_insetPad': '56px', - paddingLeft: 'var(--ListItemText-insetPad, var(--_insetPad))', - }, - }, - { - props: ({ ownerState }) => ownerState.inset && ownerState.dense, - style: { - '--ListItemText-insetPad': 'var(--ListItemText-dense-insetPad, var(--_insetPad))', + paddingLeft: 56, }, }, ], diff --git a/packages/mui-material/src/ListSubheader/ListSubheader.js b/packages/mui-material/src/ListSubheader/ListSubheader.js index cb80bf40e478bf..36bd3865891efe 100644 --- a/packages/mui-material/src/ListSubheader/ListSubheader.js +++ b/packages/mui-material/src/ListSubheader/ListSubheader.js @@ -42,12 +42,7 @@ const ListSubheaderRoot = styled('li', { })( memoTheme(({ theme }) => ({ boxSizing: 'border-box', - // Internal defaults (Material literals). Base tokens: ListSubheader has no - // size prop, so per-size tuning is meaningless — density tunes these directly. - '--_height': '48px', - '--_padInline': '16px', - '--_inset': '72px', - lineHeight: 'var(--ListSubheader-height, var(--_height))', + lineHeight: '48px', listStyle: 'none', color: (theme.vars || theme).palette.text.secondary, fontFamily: theme.typography.fontFamily, @@ -73,14 +68,14 @@ const ListSubheaderRoot = styled('li', { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 'var(--ListSubheader-padInline, var(--_padInline))', - paddingRight: 'var(--ListSubheader-padInline, var(--_padInline))', + paddingLeft: 16, + paddingRight: 16, }, }, { props: ({ ownerState }) => ownerState.inset, style: { - paddingLeft: 'var(--ListSubheader-inset, var(--_inset))', + paddingLeft: 72, }, }, { diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index b388d65a0c6441..68df44938b4404 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -61,21 +61,14 @@ const MenuItemRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => ({ ...theme.typography.body1, - // Density adapter: `dense` is the compactness axis (boolean). - // The default (non-dense) state is the plain seam `--MenuItem-` over the - // internal default `--_`; the `dense` variant re-routes the seam to the - // `--MenuItem-dense-` token. Block + min-height live on the root - // unconditionally; inline gutters live on the !disableGutters variant. - '--_minHeight': '48px', - '--_padBlock': '6px', display: 'flex', justifyContent: 'flex-start', alignItems: 'center', position: 'relative', textDecoration: 'none', - minHeight: 'var(--MenuItem-minHeight, var(--_minHeight))', - paddingTop: 'var(--MenuItem-padBlock, var(--_padBlock))', - paddingBottom: 'var(--MenuItem-padBlock, var(--_padBlock))', + minHeight: 48, + paddingTop: 6, + paddingBottom: 6, boxSizing: 'border-box', whiteSpace: 'nowrap', '&:hover': { @@ -94,18 +87,14 @@ const MenuItemRoot = styled(ButtonBase, { [`&.${menuItemClasses.focusVisible}`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.focusOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`, ), }, }, [`&.${menuItemClasses.selected}:hover`]: { backgroundColor: theme.alpha( (theme.vars || theme).palette.primary.main, - `${(theme.vars || theme).palette.action.selectedOpacity} + ${ - (theme.vars || theme).palette.action.hoverOpacity - }`, + `${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`, ), // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { @@ -136,21 +125,14 @@ const MenuItemRoot = styled(ButtonBase, { paddingLeft: 36, }, [`& .${listItemIconClasses.root}`]: { - minWidth: 'var(--ListItemIcon-minWidth, 36px)', + minWidth: 36, }, variants: [ { props: ({ ownerState }) => !ownerState.disableGutters, style: { - '--_padInline': '16px', - paddingLeft: 'var(--MenuItem-padInline, var(--_padInline))', - paddingRight: 'var(--MenuItem-padInline, var(--_padInline))', - }, - }, - { - props: ({ ownerState }) => !ownerState.disableGutters && ownerState.dense, - style: { - '--MenuItem-padInline': 'var(--MenuItem-dense-padInline, var(--_padInline))', + paddingLeft: 16, + paddingRight: 16, }, }, { @@ -171,10 +153,9 @@ const MenuItemRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.dense, style: { - '--_minHeight': '32px', // https://m2.material.io/components/menus#specs > Dense - '--_padBlock': '4px', - '--MenuItem-minHeight': 'var(--MenuItem-dense-minHeight, var(--_minHeight))', - '--MenuItem-padBlock': 'var(--MenuItem-dense-padBlock, var(--_padBlock))', + minHeight: 32, // https://m2.material.io/components/menus#specs > Dense + paddingTop: 4, + paddingBottom: 4, ...theme.typography.body2, [`& .${listItemIconClasses.root} svg`]: { fontSize: '1.25rem', diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index d22907f3abf97b..bb851995a8224d 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -11,8 +11,6 @@ import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import { useDefaultProps } from '../DefaultPropsProvider'; import outlinedInputClasses, { getOutlinedInputUtilityClass } from './outlinedInputClasses'; -import inputLabelClasses from '../InputLabel/inputLabelClasses'; -import { getOutlinedInputVars } from './outlinedInputVars'; import InputBase, { rootOverridesResolver as inputBaseRootOverridesResolver, inputOverridesResolver as inputBaseInputOverridesResolver, @@ -47,23 +45,7 @@ const OutlinedInputRoot = styled(InputBaseRoot, { memoTheme(({ theme }) => { const borderColor = theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; - // Material UI layer: public sized tokens honor cssVarPrefix (default "mui"). - // Agnostic seams (`--comp-padBlock`, `--comp-padInline`, `--comp-labelY`) and - // internal `--_*` defaults are literal and unprefixed. - const v = getOutlinedInputVars(theme); return { - // Density adapter: each padding literal becomes - // `var(--seam, var(--_))`, tokenized in place. Both axes are sized — - // each seam routes the per-size public token (block + inline). The internal - // defaults live in the variants that consume them (below), like Button's - // `--_pad`. Inline default is 14px for both sizes; the per-size inline - // tokens let a design system tune it per size anyway. - // The outlined label centers on the input's block padding. It's a preceding - // sibling, so reach it via :has and derive its resting-Y seam straight from - // the public sized token. Medium resolves to 16px (16.5 - 0.5 rounding). - [`.${inputLabelClasses.root}:has(~ &)`]: { - '--comp-labelY': `calc(var(${v.mediumPadBlock}, 16.5px) - 0.5px)`, - }, position: 'relative', borderRadius: (theme.vars || theme).shape.borderRadius, [`&:hover .${outlinedInputClasses.notchedOutline}`]: { @@ -102,60 +84,28 @@ const OutlinedInputRoot = styled(InputBaseRoot, { }, }, }, - { - props: { size: 'small' }, - style: { - // Small label resolves to 9px (8.5 + 0.5). - [`.${inputLabelClasses.root}:has(~ &)`]: { - '--comp-labelY': `calc(var(${v.smallPadBlock}, 8.5px) + 0.5px)`, - }, - }, - }, { props: ({ ownerState }) => ownerState.startAdornment, style: { - '--_padInline': '14px', - '--comp-padInline': `var(${v.mediumPadInline}, var(--_padInline))`, - paddingLeft: 'var(--comp-padInline, var(--_padInline))', - }, - }, - { - props: ({ ownerState, size }) => ownerState.startAdornment && size === 'small', - style: { - '--comp-padInline': `var(${v.smallPadInline}, var(--_padInline))`, + paddingLeft: 14, }, }, { props: ({ ownerState }) => ownerState.endAdornment, style: { - '--_padInline': '14px', - '--comp-padInline': `var(${v.mediumPadInline}, var(--_padInline))`, - paddingRight: 'var(--comp-padInline, var(--_padInline))', - }, - }, - { - props: ({ ownerState, size }) => ownerState.endAdornment && size === 'small', - style: { - '--comp-padInline': `var(${v.smallPadInline}, var(--_padInline))`, + paddingRight: 14, }, }, { props: ({ ownerState }) => ownerState.multiline, style: { - '--_padBlock': '16.5px', - '--_padInline': '14px', - '--comp-padBlock': `var(${v.mediumPadBlock}, var(--_padBlock))`, - '--comp-padInline': `var(${v.mediumPadInline}, var(--_padInline))`, - padding: - 'var(--comp-padBlock, var(--_padBlock)) var(--comp-padInline, var(--_padInline))', + padding: '16.5px 14px', }, }, { props: ({ ownerState, size }) => ownerState.multiline && size === 'small', style: { - '--_padBlock': '8.5px', - '--comp-padBlock': `var(${v.smallPadBlock}, var(--_padBlock))`, - '--comp-padInline': `var(${v.smallPadInline}, var(--_padInline))`, + padding: '8.5px 14px', }, }, ], @@ -183,61 +133,51 @@ const OutlinedInputInput = styled(InputBaseInput, { slot: 'Input', overridesResolver: inputBaseInputOverridesResolver, })( - memoTheme(({ theme }) => { - const v = getOutlinedInputVars(theme); - return { - // Both axes route the per-size public token over the internal default into - // the literal agnostic seam (`var(--comp-, var(--_))`), specialized - // by the size variant below. Defaults are the Material px (inline 14px). - '--_padBlock': '16.5px', - '--_padInline': '14px', - '--comp-padBlock': `var(${v.mediumPadBlock}, var(--_padBlock))`, - '--comp-padInline': `var(${v.mediumPadInline}, var(--_padInline))`, - padding: 'var(--comp-padBlock, var(--_padBlock)) var(--comp-padInline, var(--_padInline))', - '&:-webkit-autofill': { - ...(!theme.vars && { - WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', - WebkitTextFillColor: theme.palette.mode === 'light' ? null : '#fff', - caretColor: theme.palette.mode === 'light' ? null : '#fff', - }), - borderRadius: 'inherit', - ...(theme.vars && - theme.applyStyles('dark', { - WebkitBoxShadow: '0 0 0 100px #266798 inset', - WebkitTextFillColor: '#fff', - caretColor: '#fff', - })), - }, - variants: [ - { - props: { size: 'small' }, - style: { - '--_padBlock': '8.5px', - '--comp-padBlock': `var(${v.smallPadBlock}, var(--_padBlock))`, - '--comp-padInline': `var(${v.smallPadInline}, var(--_padInline))`, - }, + memoTheme(({ theme }) => ({ + padding: '16.5px 14px', + '&:-webkit-autofill': { + ...(!theme.vars && { + WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', + WebkitTextFillColor: theme.palette.mode === 'light' ? null : '#fff', + caretColor: theme.palette.mode === 'light' ? null : '#fff', + }), + borderRadius: 'inherit', + ...(theme.vars && + theme.applyStyles('dark', { + WebkitBoxShadow: '0 0 0 100px #266798 inset', + WebkitTextFillColor: '#fff', + caretColor: '#fff', + })), + }, + variants: [ + { + props: { + size: 'small', }, - { - props: ({ ownerState }) => ownerState.multiline, - style: { - padding: 0, - }, + style: { + padding: '8.5px 14px', }, - { - props: ({ ownerState }) => ownerState.startAdornment, - style: { - paddingLeft: 0, - }, + }, + { + props: ({ ownerState }) => ownerState.multiline, + style: { + padding: 0, }, - { - props: ({ ownerState }) => ownerState.endAdornment, - style: { - paddingRight: 0, - }, + }, + { + props: ({ ownerState }) => ownerState.startAdornment, + style: { + paddingLeft: 0, }, - ], - }; - }), + }, + { + props: ({ ownerState }) => ownerState.endAdornment, + style: { + paddingRight: 0, + }, + }, + ], + })), ); const OutlinedInput = React.forwardRef(function OutlinedInput(inProps, ref) { diff --git a/packages/mui-material/src/OutlinedInput/index.js b/packages/mui-material/src/OutlinedInput/index.js index 5ae8cff54b5cc5..4877ca68a9f8f5 100644 --- a/packages/mui-material/src/OutlinedInput/index.js +++ b/packages/mui-material/src/OutlinedInput/index.js @@ -2,5 +2,3 @@ export { default } from './OutlinedInput'; export { default as outlinedInputClasses } from './outlinedInputClasses'; export * from './outlinedInputClasses'; - -export { outlinedInputVars, getOutlinedInputVars } from './outlinedInputVars'; diff --git a/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts b/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts deleted file mode 100644 index c439b40085f0ec..00000000000000 --- a/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { makeComponentVars } from '../styles/tokenAccess'; - -/** - * OutlinedInput density token identities — the Material UI layer's - * public sized knobs (block + inline padding, prefixed via - * `getOutlinedInputVars`). The agnostic seams (`--comp-padBlock`, - * `--comp-padInline`, and the `--comp-labelY` the `:has` bridge drives on the - * floating label) are literal and unprefixed, so they live outside this map. - */ -export const outlinedInputVars = { - smallPadBlock: 'OutlinedInput-small-padBlock', - mediumPadBlock: 'OutlinedInput-medium-padBlock', - smallPadInline: 'OutlinedInput-small-padInline', - mediumPadInline: 'OutlinedInput-medium-padInline', -} as const; - -export const getOutlinedInputVars = makeComponentVars(outlinedInputVars); diff --git a/packages/mui-material/src/Radio/Radio.js b/packages/mui-material/src/Radio/Radio.js index 217d972d1449a6..48e1d7fd57df59 100644 --- a/packages/mui-material/src/Radio/Radio.js +++ b/packages/mui-material/src/Radio/Radio.js @@ -50,16 +50,6 @@ const RadioRoot = styled(SwitchBase, { color: (theme.vars || theme).palette.action.disabled, }, variants: [ - // Density: route the per-size public token into SwitchBase's seam. Default - // 9px both sizes (pixel-identical); size enables per-size density tuning. - { - props: { size: 'small' }, - style: { '--SwitchBase-pad': 'var(--Radio-small-pad, var(--_pad))' }, - }, - { - props: { size: 'medium' }, - style: { '--SwitchBase-pad': 'var(--Radio-medium-pad, var(--_pad))' }, - }, { props: { color: 'default', disabled: false, disableRipple: false }, style: { diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index 63df96964ef203..d56648e4afb3d2 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -88,13 +88,8 @@ const SelectSelect = styled(StyledSelectSelect, { })({ // Win specificity over the input base [`&.${selectClasses.select}`]: { - // Density seam: base axis (size-invariant — keeps select content box matched - // to the input line box for text-field height consistency; per-size - // compactness comes from the input root padding). Consume shape stays uniform - // with sized axes: var(seam, var(internal default)). - '--_minHeight': '1.4375em', // Required for select\text-field height consistency height: 'auto', // Resets for multiple select with chips - minHeight: 'var(--Select-minHeight, var(--_minHeight))', + minHeight: '1.4375em', // Required for select\text-field height consistency textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', diff --git a/packages/mui-material/src/Switch/Switch.js b/packages/mui-material/src/Switch/Switch.js index 27d6d2b3b10630..adbc8603dae33a 100644 --- a/packages/mui-material/src/Switch/Switch.js +++ b/packages/mui-material/src/Switch/Switch.js @@ -52,32 +52,11 @@ const SwitchRoot = styled('span', { ]; }, })({ - // Density: Switch geometry is interlocked, so the knobs are the - // dims (width/height/thumbSize/touchSize) + the track gutter (pad); the thumb's - // touch padding and travel are *derived* so the thumb stays centered on the track. - // SwitchBase pad = (touchSize - thumbSize) / 2 (centers thumb in the button) - // button top = (height - touchSize) / 2 (centers button in the root) - // checked travel = width - touchSize - // Defaults: touchSize == height -> pad 9/4, top 0, travel 20/16 (pixel-identical). - // The thumb (SwitchBase) and Thumb/Track slots inherit these seams (custom props - // inherit; they don't redeclare them). `--_pad` here is the root's gutter default - // (the track inset), distinct from the thumb's own SwitchBase `--_pad`. - '--_width': '58px', // 34 (track) + 12 (gutter) * 2 - '--_height': '38px', // 14 (track) + 12 (gutter) * 2 - '--_thumbSize': '20px', - '--_touchSize': '38px', - '--_pad': '12px', - '--Switch-width': 'var(--Switch-medium-width, var(--_width))', - '--Switch-height': 'var(--Switch-medium-height, var(--_height))', - '--Switch-thumbSize': 'var(--Switch-medium-thumbSize, var(--_thumbSize))', - '--Switch-touchSize': 'var(--Switch-medium-touchSize, var(--_touchSize))', - '--Switch-pad': 'var(--Switch-medium-pad, var(--_pad))', - '--SwitchBase-pad': 'calc((var(--Switch-touchSize) - var(--Switch-thumbSize)) / 2)', display: 'inline-flex', - width: 'var(--Switch-width, var(--_width))', - height: 'var(--Switch-height, var(--_height))', + width: 34 + 12 * 2, + height: 14 + 12 * 2, overflow: 'hidden', - padding: 'var(--Switch-pad, var(--_pad))', + padding: 12, boxSizing: 'border-box', position: 'relative', flexShrink: 0, @@ -98,17 +77,19 @@ const SwitchRoot = styled('span', { { props: { size: 'small' }, style: { - // Re-route the dims + gutter to the small tokens; pad/top/travel re-derive. - '--_width': '40px', - '--_height': '24px', - '--_thumbSize': '16px', - '--_touchSize': '24px', - '--_pad': '7px', - '--Switch-width': 'var(--Switch-small-width, var(--_width))', - '--Switch-height': 'var(--Switch-small-height, var(--_height))', - '--Switch-thumbSize': 'var(--Switch-small-thumbSize, var(--_thumbSize))', - '--Switch-touchSize': 'var(--Switch-small-touchSize, var(--_touchSize))', - '--Switch-pad': 'var(--Switch-small-pad, var(--_pad))', + width: 40, + height: 24, + padding: 7, + [`& .${switchClasses.thumb}`]: { + width: 16, + height: 16, + }, + [`& .${switchClasses.switchBase}`]: { + padding: 4, + [`&.${switchClasses.checked}`]: { + transform: 'translateX(16px)', + }, + }, }, }, ], @@ -129,8 +110,7 @@ const SwitchSwitchBase = styled(SwitchBase, { })( memoTheme(({ theme }) => ({ position: 'absolute', - // Center the touch target in the root (top 0 when touchSize == height). - top: 'calc((var(--Switch-height, var(--_height)) - var(--Switch-touchSize, var(--_touchSize))) / 2)', + top: 0, left: 0, zIndex: 1, // Render above the focus ripple. color: theme.vars @@ -140,9 +120,7 @@ const SwitchSwitchBase = styled(SwitchBase, { duration: theme.transitions.duration.shortest, }), [`&.${switchClasses.checked}`]: { - // Travel = root width - touch target (keeps the thumb symmetric on the track). - transform: - 'translateX(calc(var(--Switch-width, var(--_width)) - var(--Switch-touchSize, var(--_touchSize))))', + transform: 'translateX(20px)', }, [`&.${switchClasses.disabled}`]: { color: theme.vars @@ -216,10 +194,7 @@ const SwitchTrack = styled('span', { memoTheme(({ theme }) => ({ height: '100%', width: '100%', - // Full pill: half the track thickness (height minus the two gutters). Inherits - // the seams from SwitchRoot. Medium -> 7px; small clamps to a pill either way. - borderRadius: - 'calc((var(--Switch-height, var(--_height)) - 2 * var(--Switch-pad, var(--_pad))) / 2)', + borderRadius: 14 / 2, zIndex: -1, ...getTransitionStyles(theme, ['opacity', 'background-color'], { duration: theme.transitions.duration.shortest, @@ -246,8 +221,8 @@ const SwitchThumb = styled('span', { backgroundColor: 'currentColor', boxSizing: 'border-box', border: '1px solid transparent', - width: 'var(--Switch-thumbSize, var(--_thumbSize))', - height: 'var(--Switch-thumbSize, var(--_thumbSize))', + width: 20, + height: 20, borderRadius: '50%', })), ); diff --git a/packages/mui-material/src/Tab/Tab.js b/packages/mui-material/src/Tab/Tab.js index 0522b119c94d27..05ddee7f3a0ba7 100644 --- a/packages/mui-material/src/Tab/Tab.js +++ b/packages/mui-material/src/Tab/Tab.js @@ -54,15 +54,9 @@ const TabRoot = styled(ButtonBase, { maxWidth: 360, minWidth: 90, position: 'relative', - // Density seams: Tab has no `size` prop, so each axis is a base token - // (`--Tab-`) over an internal default (`--_`). The labelIcon state - // owns its own block/min-height literals; everything else reads these. - '--_padBlock': '12px', - '--_padInline': '16px', - '--_minHeight': '48px', - minHeight: 'var(--Tab-minHeight, var(--_minHeight))', + minHeight: 48, flexShrink: 0, - padding: 'var(--Tab-padBlock, var(--_padBlock)) var(--Tab-padInline, var(--_padInline))', + padding: '12px 16px', overflow: 'hidden', whiteSpace: 'normal', textAlign: 'center', @@ -88,12 +82,9 @@ const TabRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.icon && ownerState.label, style: { - // labelIcon owns its own compactness + block padding literals; inline - // padding stays from the base shorthand (unchanged at 16px). - '--_minHeight': '72px', - '--_padBlock': '9px', - paddingTop: 'var(--Tab-padBlock, var(--_padBlock))', - paddingBottom: 'var(--Tab-padBlock, var(--_padBlock))', + minHeight: 72, + paddingTop: 9, + paddingBottom: 9, }, }, { @@ -101,8 +92,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'top', style: { [`& > .${tabClasses.icon}`]: { - '--_iconSpacing': '6px', - marginBottom: 'var(--Tab-iconSpacing, var(--_iconSpacing))', + marginBottom: 6, }, }, }, @@ -111,8 +101,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'bottom', style: { [`& > .${tabClasses.icon}`]: { - '--_iconSpacing': '6px', - marginTop: 'var(--Tab-iconSpacing, var(--_iconSpacing))', + marginTop: 6, }, }, }, @@ -121,8 +110,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'start', style: { [`& > .${tabClasses.icon}`]: { - '--_iconSpacing': theme.spacing(1), - marginRight: 'var(--Tab-iconSpacing, var(--_iconSpacing))', + marginRight: theme.spacing(1), }, }, }, @@ -131,8 +119,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'end', style: { [`& > .${tabClasses.icon}`]: { - '--_iconSpacing': theme.spacing(1), - marginLeft: 'var(--Tab-iconSpacing, var(--_iconSpacing))', + marginLeft: theme.spacing(1), }, }, }, diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 065baf03e2ea08..46f5500ebfdc82 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -191,10 +191,7 @@ describe('', () => { expect(wrapper).to.have.class('test-icon'); }); - // The icon gap is now a CSS var (var(--Tab-iconSpacing, ...)); jsdom can't - // resolve var(), so assert in a real browser only. Default render is verified - // pixel-identical by the density screenshot harness. - it.skipIf(isJsdom())('should have bottom margin when passed together with label', () => { + it('should have bottom margin when passed together with label', () => { render( } label="foo" /> diff --git a/packages/mui-material/src/TablePagination/TablePagination.js b/packages/mui-material/src/TablePagination/TablePagination.js index 61b8cbc5285b27..45c2b598ea838e 100644 --- a/packages/mui-material/src/TablePagination/TablePagination.js +++ b/packages/mui-material/src/TablePagination/TablePagination.js @@ -45,22 +45,18 @@ const TablePaginationToolbar = styled(Toolbar, { }), })( memoTheme(({ theme }) => ({ - // Base density seams: no `size` prop here, so each axis is a size-invariant - // base token. `--_` carries today's literal; the seam falls back to it. - '--_minHeight': '52px', - '--_actionsSpacing': '20px', - minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', + minHeight: 52, paddingRight: 2, [`${theme.breakpoints.up('xs')} and (orientation: landscape)`]: { - minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', + minHeight: 52, }, [theme.breakpoints.up('sm')]: { - minHeight: 'var(--TablePagination-minHeight, var(--_minHeight))', + minHeight: 52, paddingRight: 2, }, [`& .${tablePaginationClasses.actions}`]: { flexShrink: 0, - marginLeft: 'var(--TablePagination-actionsSpacing, var(--_actionsSpacing))', + marginLeft: 20, }, })), ); @@ -95,10 +91,7 @@ const TablePaginationSelect = styled(Select, { color: 'inherit', fontSize: 'inherit', flexShrink: 0, - // Base density seam for the select-to-rows gap. Co-located default keeps the - // unprefixed `--_selectSpacing` from inheriting a foreign value. - '--_selectSpacing': '32px', - marginRight: 'var(--TablePagination-selectSpacing, var(--_selectSpacing))', + marginRight: 32, marginLeft: 8, [`& .${tablePaginationClasses.select}`]: { paddingLeft: 8, diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js index cd2223e6323087..c9dc851fb0c0f6 100644 --- a/packages/mui-material/src/Tabs/Tabs.js +++ b/packages/mui-material/src/Tabs/Tabs.js @@ -77,10 +77,7 @@ const TabsRoot = styled('div', { })( memoTheme(({ theme }) => ({ overflow: 'hidden', - // Density adapter: base token, 48px literal fallback keeps the - // default pixel-identical. Tabs is the Tab's parent, so it can't read the - // child's `--Tab-minHeight` — it carries its own seam. - minHeight: 'var(--Tabs-minHeight, 48px)', + minHeight: 48, // Add iOS momentum scrolling for iOS < 13.0 WebkitOverflowScrolling: 'touch', display: 'flex', diff --git a/packages/mui-material/src/Toolbar/Toolbar.js b/packages/mui-material/src/Toolbar/Toolbar.js index 46939ac77a5362..dbfde2fb54eb23 100644 --- a/packages/mui-material/src/Toolbar/Toolbar.js +++ b/packages/mui-material/src/Toolbar/Toolbar.js @@ -31,21 +31,15 @@ const ToolbarRoot = styled('div', { position: 'relative', display: 'flex', alignItems: 'center', - // Gutter default (the responsive sm bump is set in the gutters variant). - // Only the `dense` minHeight is tokenized; the `regular` height stays driven - // by the public `theme.mixins.toolbar` so existing customization keeps working. - '--_minHeight': '48px', - '--_padInline': theme.spacing(2), variants: [ { props: ({ ownerState }) => !ownerState.disableGutters, style: { - // Gutters are shared across variants -> base token (no size layer), - // consumed directly with the internal default as fallback. - paddingLeft: 'var(--Toolbar-padInline, var(--_padInline))', - paddingRight: 'var(--Toolbar-padInline, var(--_padInline))', + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), [theme.breakpoints.up('sm')]: { - '--_padInline': theme.spacing(3), + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), }, }, }, @@ -54,9 +48,7 @@ const ToolbarRoot = styled('div', { variant: 'dense', }, style: { - '--_minHeight': '48px', - '--Toolbar-minHeight': 'var(--Toolbar-dense-minHeight, var(--_minHeight))', - minHeight: 'var(--Toolbar-minHeight, var(--_minHeight))', + minHeight: 48, }, }, { diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js index 3dc1e3f70dc176..5257bfe688e49e 100644 --- a/packages/mui-material/src/internal/SwitchBase.js +++ b/packages/mui-material/src/internal/SwitchBase.js @@ -25,11 +25,7 @@ const useUtilityClasses = (ownerState) => { const SwitchBaseRoot = styled(ButtonBase, { name: 'MuiSwitchBase', })({ - // Density adapter: SwitchBase is the agnostic layer shared by - // Checkbox/Radio (and the Switch thumb). It consumes one seam; the Material - // layer (Checkbox/Radio) routes its per-size public token into --SwitchBase-pad. - '--_pad': '9px', - padding: 'var(--SwitchBase-pad, var(--_pad))', + padding: 9, borderRadius: '50%', variants: [ { diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts index 3c30add368bffc..473c7c5cc36cb9 100644 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ b/packages/mui-material/src/styles/enhanceDensity.ts @@ -1,5 +1,4 @@ import { Theme } from './createTheme'; -import inputLabelClasses from '../InputLabel/inputLabelClasses'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; /** @@ -96,12 +95,6 @@ export default function enhanceDensity< return acc; }, {} as DensityScale); - // Public token name, prefix-guarded to match the component resolver: - // prefixed when the theme has a cssVarPrefix, bare otherwise. Only the converted - // components (Button, OutlinedInput, InputLabel) use it; the rest stay unprefixed. - const prefix = (themeInput as any).cssVarPrefix; - const pv = (name: string) => `--${prefix ? `${prefix}-` : ''}${name}`; - const theme = { ...themeInput } as T & { density: DensityScale }; theme.density = scale; theme.vars = { ...themeInput.vars, density: varRefs }; @@ -141,344 +134,6 @@ export default function enhanceDensity< ], }, }, - MuiChip: { - ...c?.MuiChip, - styleOverrides: { - ...c?.MuiChip?.styleOverrides, - root: [ - c?.MuiChip?.styleOverrides?.root, - { - '--Chip-small-height': varRefs.xl, - '--Chip-medium-height': varRefs.xxl, - '--Chip-small-padInline': varRefs.sm, - '--Chip-medium-padInline': varRefs.md, - }, - ], - }, - }, - MuiIconButton: { - ...c?.MuiIconButton, - styleOverrides: { - ...c?.MuiIconButton?.styleOverrides, - root: [ - c?.MuiIconButton?.styleOverrides?.root, - { - '--IconButton-small-pad': varRefs.xs, - '--IconButton-medium-pad': varRefs.sm, - '--IconButton-large-pad': varRefs.lg, - }, - ], - }, - }, - MuiMenuItem: { - ...c?.MuiMenuItem, - styleOverrides: { - ...c?.MuiMenuItem?.styleOverrides, - root: [ - c?.MuiMenuItem?.styleOverrides?.root, - { - '--MenuItem-minHeight': varRefs.xl, - '--MenuItem-dense-minHeight': varRefs.lg, - '--MenuItem-padBlock': varRefs.xs, - '--MenuItem-dense-padBlock': varRefs.xxs, - '--MenuItem-padInline': varRefs.lg, - '--MenuItem-dense-padInline': varRefs.md, - }, - ], - }, - }, - MuiListItem: { - ...c?.MuiListItem, - styleOverrides: { - ...c?.MuiListItem?.styleOverrides, - root: [ - c?.MuiListItem?.styleOverrides?.root, - { - '--ListItem-padBlock': varRefs.sm, - '--ListItem-dense-padBlock': varRefs.xxs, - '--ListItem-padInline': varRefs.lg, - '--ListItem-dense-padInline': varRefs.md, - }, - ], - }, - }, - MuiListItemButton: { - ...c?.MuiListItemButton, - styleOverrides: { - ...c?.MuiListItemButton?.styleOverrides, - root: [ - c?.MuiListItemButton?.styleOverrides?.root, - { - '--ListItemButton-padBlock': varRefs.sm, - '--ListItemButton-dense-padBlock': varRefs.xs, - '--ListItemButton-padInline': varRefs.lg, - '--ListItemButton-dense-padInline': varRefs.md, - }, - ], - }, - }, - MuiListItemIcon: { - ...c?.MuiListItemIcon, - styleOverrides: { - ...c?.MuiListItemIcon?.styleOverrides, - root: [ - c?.MuiListItemIcon?.styleOverrides?.root, - { - '--ListItemIcon-minWidth': `calc(36px + ${varRefs.md})`, - }, - ], - }, - }, - MuiListItemText: { - ...c?.MuiListItemText, - styleOverrides: { - ...c?.MuiListItemText?.styleOverrides, - root: [ - c?.MuiListItemText?.styleOverrides?.root, - { - // Sized-only: regular vs dense compactness each maps to its own step. - // marginBlock = vertical row spacing (smaller = denser); insetPad = - // indentation. - '--ListItemText-marginBlock': varRefs.xs, - '--ListItemText-dense-marginBlock': varRefs.xxs, - '--ListItemText-insetPad': `calc(${varRefs.xl} + ${varRefs.lg})`, - '--ListItemText-dense-insetPad': varRefs.xl, - }, - ], - }, - }, - MuiListSubheader: { - ...c?.MuiListSubheader, - styleOverrides: { - ...c?.MuiListSubheader?.styleOverrides, - root: [ - c?.MuiListSubheader?.styleOverrides?.root, - { - // Base tokens (no size layer): map the agnostic seams directly. - '--ListSubheader-height': varRefs.xl, - '--ListSubheader-padInline': varRefs.md, - '--ListSubheader-inset': `calc(${varRefs.xl} + ${varRefs.lg})`, - }, - ], - }, - }, - MuiToolbar: { - ...c?.MuiToolbar, - styleOverrides: { - ...c?.MuiToolbar?.styleOverrides, - root: [ - c?.MuiToolbar?.styleOverrides?.root, - { - // Only `dense` minHeight is tokenized (regular stays mixins.toolbar); - // gutter padInline is a base token. - '--Toolbar-dense-minHeight': varRefs.lg, - '--Toolbar-padInline': varRefs.md, - }, - ], - }, - }, - MuiTab: { - ...c?.MuiTab, - styleOverrides: { - ...c?.MuiTab?.styleOverrides, - root: [ - c?.MuiTab?.styleOverrides?.root, - { - // Base tokens: Tab has no size prop, so map the agnostic seams - // directly to density steps (no per-size tokens to route). - '--Tab-padBlock': varRefs.sm, - '--Tab-padInline': varRefs.lg, - '--Tab-minHeight': `calc(${varRefs.xl} + ${varRefs.lg})`, - '--Tab-iconSpacing': varRefs.xs, - }, - ], - }, - }, - MuiTabs: { - ...c?.MuiTabs, - styleOverrides: { - ...c?.MuiTabs?.styleOverrides, - root: [ - c?.MuiTabs?.styleOverrides?.root, - { - // Match Tab's minHeight step so the bar tracks the tabs it contains. - '--Tabs-minHeight': `calc(${varRefs.xl} + ${varRefs.lg})`, - }, - ], - }, - }, - MuiTablePagination: { - ...c?.MuiTablePagination, - styleOverrides: { - ...c?.MuiTablePagination?.styleOverrides, - root: [ - c?.MuiTablePagination?.styleOverrides?.root, - { - '--TablePagination-minHeight': `calc(${varRefs.xl} + ${varRefs.md})`, - '--TablePagination-actionsSpacing': varRefs.lg, - '--TablePagination-selectSpacing': varRefs.xl, - }, - ], - }, - }, - MuiCardContent: { - ...c?.MuiCardContent, - styleOverrides: { - ...c?.MuiCardContent?.styleOverrides, - root: [ - c?.MuiCardContent?.styleOverrides?.root, - { - // CardContent has no size prop -> base tokens (no per-size layer). - '--CardContent-pad': varRefs.lg, - '--CardContent-padBottom': varRefs.xl, - }, - ], - }, - }, - MuiSelect: { - ...c?.MuiSelect, - styleOverrides: { - ...c?.MuiSelect?.styleOverrides, - root: [ - c?.MuiSelect?.styleOverrides?.root, - { - // Base axis (no size layer) — single agnostic seam, mapped to a - // mid-step so density nudges the select content-box floor uniformly. - '--Select-minHeight': varRefs.lg, - }, - ], - }, - }, - MuiBreadcrumbs: { - ...c?.MuiBreadcrumbs, - styleOverrides: { - ...c?.MuiBreadcrumbs?.styleOverrides, - root: [ - c?.MuiBreadcrumbs?.styleOverrides?.root, - { - '--Breadcrumbs-separatorGap': varRefs.sm, - }, - ], - }, - }, - MuiInputAdornment: { - ...c?.MuiInputAdornment, - styleOverrides: { - ...c?.MuiInputAdornment?.styleOverrides, - root: [ - c?.MuiInputAdornment?.styleOverrides?.root, - { - '--InputAdornment-small-gap': varRefs.xxs, - '--InputAdornment-medium-gap': varRefs.sm, - '--InputAdornment-small-marginTop': varRefs.md, - '--InputAdornment-medium-marginTop': varRefs.lg, - }, - ], - }, - }, - MuiBadge: { - ...c?.MuiBadge, - styleOverrides: { - ...c?.MuiBadge?.styleOverrides, - root: [ - c?.MuiBadge?.styleOverrides?.root, - { - '--Badge-standard-pad': `0 ${varRefs.sm}`, - '--Badge-standard-size': varRefs.lg, - '--Badge-dot-pad': '0px', - '--Badge-dot-size': varRefs.xs, - }, - ], - }, - }, - MuiOutlinedInput: { - ...c?.MuiOutlinedInput, - styleOverrides: { - ...c?.MuiOutlinedInput?.styleOverrides, - root: [ - c?.MuiOutlinedInput?.styleOverrides?.root, - { - // Sized block/inline padding per size; block < inline to keep the - // input's 16.5/14 feel. - [pv('OutlinedInput-medium-padBlock')]: varRefs.md, - [pv('OutlinedInput-small-padBlock')]: varRefs.sm, - [pv('OutlinedInput-medium-padInline')]: varRefs.lg, - [pv('OutlinedInput-small-padInline')]: varRefs.md, - // The outlined label resting-Y tracks the input's block padding, but - // the label is a preceding sibling — it can't read the input's - // `--OutlinedInput-*-padBlock` (custom props don't inherit sibling -> - // sibling). So drive the agnostic `--comp-labelY` seam straight from - // the density step (which the label DOES inherit from `:root`), - // matching the component's -0.5/+0.5 rounding per size. - [`.${inputLabelClasses.root}:has(~ &)`]: { - '--comp-labelY': `calc(${varRefs.md} - 0.5px)`, - }, - variants: [ - { - props: { size: 'small' }, - style: { - [`.${inputLabelClasses.root}:has(~ &)`]: { - '--comp-labelY': `calc(${varRefs.sm} + 0.5px)`, - }, - }, - }, - ], - }, - ], - }, - }, - MuiCheckbox: { - ...c?.MuiCheckbox, - styleOverrides: { - ...c?.MuiCheckbox?.styleOverrides, - root: [ - c?.MuiCheckbox?.styleOverrides?.root, - { - // Touch-target padding (9px default both sizes), via SwitchBase. - '--Checkbox-medium-pad': varRefs.sm, - '--Checkbox-small-pad': varRefs.xs, - }, - ], - }, - }, - MuiRadio: { - ...c?.MuiRadio, - styleOverrides: { - ...c?.MuiRadio?.styleOverrides, - root: [ - c?.MuiRadio?.styleOverrides?.root, - { - '--Radio-medium-pad': varRefs.sm, - '--Radio-small-pad': varRefs.xs, - }, - ], - }, - }, - MuiSwitch: { - ...c?.MuiSwitch, - styleOverrides: { - ...c?.MuiSwitch?.styleOverrides, - root: [ - c?.MuiSwitch?.styleOverrides?.root, - { - // Switch dims are composed from scale steps to land on today's sizes - // at the default scale, then track density proportionally. pad/top/ - // travel/radius re-derive, so the geometry stays valid (touchSize == - // height -> centered; width > touchSize -> positive travel). - '--Switch-medium-width': `calc(${varRefs.xxl} * 2 - 6px)`, // 58 - '--Switch-medium-height': `calc(${varRefs.xxl} + ${varRefs.xs})`, // 38 - '--Switch-medium-touchSize': `calc(${varRefs.xxl} + ${varRefs.xs})`, // 38 (= height) - '--Switch-medium-thumbSize': `calc(${varRefs.lg} + ${varRefs.xxs})`, // 20 - '--Switch-medium-pad': varRefs.md, // 12 - '--Switch-small-width': `calc(${varRefs.xxl} + ${varRefs.sm})`, // 40 - '--Switch-small-height': varRefs.xl, // 24 - '--Switch-small-touchSize': varRefs.xl, // 24 (= height) - '--Switch-small-thumbSize': varRefs.lg, // 16 - '--Switch-small-pad': `calc(${varRefs.sm} - 1px)`, // 7 - }, - ], - }, - }, }; return theme; diff --git a/packages/mui-material/src/styles/tokenAccess.ts b/packages/mui-material/src/styles/tokenAccess.ts deleted file mode 100644 index fa24e5c8b3c834..00000000000000 --- a/packages/mui-material/src/styles/tokenAccess.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Density token name resolution. The prefix tracks the css-var - * feature: a theme created with `cssVariables` carries `cssVarPrefix` (default - * `mui`), so tokens resolve to `--mui-Button-pad`; a plain `createTheme()` has - * no prefix, so they resolve to `--Button-pad`. The component internals and the - * consumer both call the same resolver on the same theme, so the emitted name - * and the targeted name can never drift. - */ - -interface PrefixedTheme { - cssVarPrefix?: string | undefined; -} - -/** Bare (unwrapped) css-var name for a single ad-hoc field, prefix-guarded. */ -export const varName = (theme: PrefixedTheme | undefined, field: string): string => - `--${theme?.cssVarPrefix ? `${theme.cssVarPrefix}-` : ''}${field}`; - -/** - * Build a cached, theme-bound resolver from a component's key map. The resolved - * map depends only on `cssVarPrefix` (domain ≈ `{ '', 'mui' }`), so it's - * memoized per prefix: first call builds + freezes, every later call is an O(1) - * lookup returning the shared object — no per-call allocation, multi-field free. - */ -export function makeComponentVars>(keyMap: T) { - const cache = new Map>>(); - return (theme?: PrefixedTheme): Readonly> => { - const prefix = theme?.cssVarPrefix ?? ''; - let vars = cache.get(prefix); - if (vars === undefined) { - const resolved = {} as Record; - for (const key in keyMap) { - if (Object.prototype.hasOwnProperty.call(keyMap, key)) { - resolved[key] = `--${prefix ? `${prefix}-` : ''}${keyMap[key]}`; - } - } - vars = Object.freeze(resolved); - cache.set(prefix, vars); - } - return vars; - }; -} From dd301fca2b27e1fc1917c0fb88dedcf96364991b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 13:44:32 +0700 Subject: [PATCH 040/114] density Epic 2 foundation: split enhanceDensity into 3 preset fns Replace enhanceDensity(theme,{scale}) + DENSITY_PRESETS with a shared private core (densityScale.ts: types, DENSITY_KEYS, applyDensity) and 3 thin wrappers (enhance{Compact,Normal,Comfort}Density) each owning its scale + button typography. Barrel drops old exports. Experiment page switches preset->fn and reads scale off theme.density. normal = explicit px (self-contained). typography via theme.typography.button reflows line-height all sizes + font-size on medium (Button hardcodes small/large font-size). Zero-diff gate 3/3 at maxDiffPixels:0; padding reflows every (variant,size) cell to each preset scale. --- docs/pages/experiments/density-experiment.tsx | 54 +++---- packages/mui-material/src/Button/Button.js | 4 +- .../mui-material/src/Button/buttonVars.ts | 3 +- .../mui-material/src/styles/densityScale.ts | 129 ++++++++++++++++ .../src/styles/enhanceComfortDensity.ts | 12 ++ .../src/styles/enhanceCompactDensity.ts | 12 ++ .../mui-material/src/styles/enhanceDensity.ts | 140 ------------------ .../src/styles/enhanceNormalDensity.ts | 13 ++ packages/mui-material/src/styles/index.d.ts | 10 +- packages/mui-material/src/styles/index.js | 4 +- 10 files changed, 199 insertions(+), 182 deletions(-) create mode 100644 packages/mui-material/src/styles/densityScale.ts create mode 100644 packages/mui-material/src/styles/enhanceComfortDensity.ts create mode 100644 packages/mui-material/src/styles/enhanceCompactDensity.ts delete mode 100644 packages/mui-material/src/styles/enhanceDensity.ts create mode 100644 packages/mui-material/src/styles/enhanceNormalDensity.ts diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-experiment.tsx index a7c6aa7a980639..045de6a6260b43 100644 --- a/docs/pages/experiments/density-experiment.tsx +++ b/docs/pages/experiments/density-experiment.tsx @@ -14,7 +14,13 @@ import MenuItem from '@mui/material/MenuItem'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; -import { createTheme, ThemeProvider, enhanceDensity, DENSITY_PRESETS } from '@mui/material/styles'; +import { + createTheme, + ThemeProvider, + enhanceCompactDensity, + enhanceNormalDensity, + enhanceComfortDensity, +} from '@mui/material/styles'; import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout'; const SCALE_KEYS = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const; @@ -59,30 +65,12 @@ function validateMapping(input: string): { valid: boolean; error: string | null return { valid: true, error: null }; } -// Active preset's 7-step scale in px (for the legend + live preview). -// compact/comfort = explicit; `normal` = spacing-derived (unit 8px = enhanceDensity default). -const NORMAL_MULTIPLIER: Record<(typeof SCALE_KEYS)[number], number> = { - xxs: 0.5, - xs: 0.75, - sm: 1, - md: 1.5, - lg: 2, - xl: 3, - xxl: 4, -}; -const SPACING_UNIT = 8; - -function presetScalePx(preset: Preset): Record | null { - if (preset === 'unset') { - return null; - } - if (preset === 'normal') { - return Object.fromEntries( - SCALE_KEYS.map((k) => [k, `${NORMAL_MULTIPLIER[k] * SPACING_UNIT}px`]), - ); - } - return (DENSITY_PRESETS[preset] ?? {}) as Record; -} +// Each preset maps to its `enhance*Density` fn; `unset` applies none. +const PRESET_FN = { + compact: enhanceCompactDensity, + normal: enhanceNormalDensity, + comfort: enhanceComfortDensity, +} as const; // Resolved var string + px for a valid mapping value under the active scale — // e.g. `md` → { varStr: 'var(--mui-density-md)', px: '8px' } (compact). @@ -180,18 +168,20 @@ export default function DensityExperiment() { ); const mappingEnabled = preset !== 'unset'; - const scalePx = presetScalePx(preset); const visibleComponents: ComponentName[] = selection === 'All' ? COMPONENTS : [selection]; const canvasTheme = React.useMemo(() => { - if (preset === 'unset') { - return createTheme({ cssVariables: true }); - } - return enhanceDensity(createTheme({ cssVariables: true }), { - scale: DENSITY_PRESETS[preset], - }); + const base = createTheme({ cssVariables: true }); + return preset === 'unset' ? base : PRESET_FN[preset](base); }, [preset]); + // Active scale in px straight off the enhanced theme — single source of truth + // for the legend + preview, so it can't drift from what the preset applied. + const scalePx = + preset === 'unset' + ? null + : (canvasTheme as unknown as { density: Record }).density; + const setField = (comp: ComponentName, key: string, value: string) => setMapping((m) => ({ ...m, [comp]: { ...m[comp], [key]: value } })); diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index cf0c5a1bdc20f8..92db7975ececb1 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -95,8 +95,8 @@ const ButtonRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => { // Material UI layer: the internal sized tokens are the static, unprefixed - // `private_buttonVars` map, imported here and by `enhanceDensity` - // so emitted and targeted names can't drift. The agnostic seam (`--comp-pad`) + // `private_buttonVars` map, imported here and by the density core + // (`applyDensity`) so emitted and targeted names can't drift. The agnostic seam (`--comp-pad`) // and internal default (`--_pad`) are literal and unprefixed. const inheritContainedBackgroundColor = theme.palette.mode === 'light' ? theme.palette.grey[300] : theme.palette.grey[800]; diff --git a/packages/mui-material/src/Button/buttonVars.ts b/packages/mui-material/src/Button/buttonVars.ts index faab6c88093d20..06373bf15eeb00 100644 --- a/packages/mui-material/src/Button/buttonVars.ts +++ b/packages/mui-material/src/Button/buttonVars.ts @@ -2,7 +2,8 @@ /** * Button density token identities — the Material UI layer's internal * designer knobs (`private_*` per the density RFC). Static, unprefixed literals - * imported by both the styled component AND `enhanceDensity`, so the emitted and + * imported by both the styled component AND the density core (`applyDensity`), + * so the emitted and * targeted names can't drift. The agnostic seam (`--comp-pad`) and internal * default (`--_pad`) are the literal plumbing and live outside this map. */ diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts new file mode 100644 index 00000000000000..8a2633b037638b --- /dev/null +++ b/packages/mui-material/src/styles/densityScale.ts @@ -0,0 +1,129 @@ +import { Theme } from './createTheme'; +import { private_buttonVars as buttonVars } from '../Button/buttonVars'; + +/** + * Named density steps, surfaced as `--mui-density-*` CSS vars. Components wired + * by the `enhance*Density` presets pull their spacing tokens from these. + */ +export interface DensityScale { + xxs: string; + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + xxl: string; +} + +/** + * Button typography reflow applied alongside the scale. Handled here (not via + * component vars) so every family that renders `theme.typography.button` tracks + * the preset. + */ +export interface DensityTypography { + fontSize?: string | undefined; + lineHeight?: number | string | undefined; +} + +export interface DensityConfig { + scale: DensityScale; + typography?: DensityTypography | undefined; +} + +export type DensityKey = keyof DensityScale; + +export const DENSITY_KEYS: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; + +/** Theme shape the presets can enhance in place. */ +export type EnhanceableTheme = { + components?: Theme['components'] | undefined; + typography?: Record | undefined; + vars?: Record | undefined; +}; + +const cssVar = (key: DensityKey) => `--mui-density-${key}`; + +/** + * PRIVATE density core shared by the three `enhance*Density` presets. Not + * re-exported from the styles barrel — presets are the public surface. + * + * Does both jobs in one call (mirroring `enhanceHighContrast`): + * 1. **Emits** the scale as `--mui-density-*` CSS vars at `:root` (via + * `MuiCssBaseline` — requires ``) and exposes it on + * `theme.density` / `theme.vars.density`. + * 2. **Maps** each family's sized tokens (`private_*Vars`) to density steps + * through injected `styleOverrides.root`. The mapping is identical across + * presets — only the scale values differ — so it lives here once. + * + * Also merges `config.typography` into `theme.typography.button`. + * + * @param themeInput - The created theme to enhance. + * @param config - The preset's scale + typography. + * @returns The enhanced theme. + */ +export function applyDensity( + themeInput: T, + config: DensityConfig, +): T & { density: DensityScale } { + const { scale, typography } = config; + + const rootVars = DENSITY_KEYS.reduce>((acc, key) => { + acc[cssVar(key)] = scale[key]; + return acc; + }, {}); + + const varRefs = DENSITY_KEYS.reduce((acc, key) => { + acc[key] = `var(${cssVar(key)})`; + return acc; + }, {} as DensityScale); + + const theme = { ...themeInput } as T & { density: DensityScale }; + theme.density = scale; + theme.vars = { ...themeInput.vars, density: varRefs }; + + if (typography) { + theme.typography = { + ...themeInput.typography, + button: { ...themeInput.typography?.button, ...typography }, + }; + } + + const c = themeInput.components; + const existingBaseline = c?.MuiCssBaseline?.styleOverrides; + const baselineObject = + existingBaseline && typeof existingBaseline === 'object' ? existingBaseline : undefined; + + theme.components = { + ...c, + MuiCssBaseline: { + ...c?.MuiCssBaseline, + styleOverrides: { + ...baselineObject, + ':root': { + ...(baselineObject as any)?.[':root'], + ...rootVars, + }, + }, + }, + MuiButton: { + ...c?.MuiButton, + styleOverrides: { + ...c?.MuiButton?.styleOverrides, + root: [ + c?.MuiButton?.styleOverrides?.root, + { + // Sized-only: each size's `pad` shorthand (block inline) maps to its + // own density step, so tuning the scale keeps the per-size matrix. + // Emit through the same static map the styled fn reads — names can't + // drift. Bare, unprefixed. + [buttonVars.smallPad]: `${varRefs.xxs} ${varRefs.sm}`, + [buttonVars.mediumPad]: `${varRefs.xs} ${varRefs.lg}`, + [buttonVars.largePad]: `${varRefs.sm} ${varRefs.xl}`, + }, + ], + }, + }, + }; + + return theme; +} diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts new file mode 100644 index 00000000000000..dbf7a727e4fba4 --- /dev/null +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -0,0 +1,12 @@ +import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; + +// Comfort preset data — scale (7 steps) + button typography. No logic beyond the +// wrapper; if logic creeps in here, the preset split has drifted. +const comfort: DensityConfig = { + scale: { xxs: '6px', xs: '8px', sm: '12px', md: '16px', lg: '24px', xl: '32px', xxl: '40px' }, + typography: { fontSize: '0.9375rem', lineHeight: 2 }, +}; + +export default function enhanceComfortDensity(theme: T) { + return applyDensity(theme, comfort); +} diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts new file mode 100644 index 00000000000000..46af2336f8d00d --- /dev/null +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -0,0 +1,12 @@ +import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; + +// Compact preset data — scale (7 steps) + button typography. No logic beyond +// the wrapper; if logic creeps in here, the preset split has drifted. +const compact: DensityConfig = { + scale: { xxs: '2px', xs: '4px', sm: '6px', md: '8px', lg: '12px', xl: '18px', xxl: '24px' }, + typography: { fontSize: '0.8125rem', lineHeight: 1.5 }, +}; + +export default function enhanceCompactDensity(theme: T) { + return applyDensity(theme, compact); +} diff --git a/packages/mui-material/src/styles/enhanceDensity.ts b/packages/mui-material/src/styles/enhanceDensity.ts deleted file mode 100644 index 473c7c5cc36cb9..00000000000000 --- a/packages/mui-material/src/styles/enhanceDensity.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Theme } from './createTheme'; -import { private_buttonVars as buttonVars } from '../Button/buttonVars'; - -/** - * Named density steps, surfaced as `--mui-density-*` CSS vars. Components wired - * by `enhanceDensity` pull their spacing tokens from these. - */ -export interface DensityScale { - xxs: string; - xs: string; - sm: string; - md: string; - lg: string; - xl: string; - xxl: string; -} - -export interface DensityOptions { - /** - * Override any density step. Defaults derive from `theme.spacing`. - */ - scale?: Partial | undefined; -} - -type DensityKey = keyof DensityScale; - -/** - * PROTOTYPE-ONLY: explicit per-step px scales for the experiment page. Production - * scale is settled internally (RFC out-of-scope). `normal` is `undefined` → the - * spacing-derived `enhanceDensity` default. - */ -export const DENSITY_PRESETS: Record | undefined> = { - compact: { xxs: '2px', xs: '4px', sm: '6px', md: '8px', lg: '12px', xl: '18px', xxl: '24px' }, - normal: undefined, - comfort: { xxs: '6px', xs: '8px', sm: '12px', md: '16px', lg: '24px', xl: '32px', xxl: '40px' }, -}; - -const densityKeys: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; - -// Default scale: t-shirt steps derived from the theme spacing unit. -const defaultMultiplier: Record = { - xxs: 0.5, - xs: 0.75, - sm: 1, - md: 1.5, - lg: 2, - xl: 3, - xxl: 4, -}; - -const cssVar = (key: DensityKey) => `--mui-density-${key}`; - -/** - * Enhances a created theme with a holistic density layer. - * - * Does both jobs in one call (mirroring `enhanceHighContrast`): - * 1. **Emits** the density scale as `--mui-density-*` CSS vars at `:root` - * (via `MuiCssBaseline` — requires ``) and exposes them on - * `theme.density` / `theme.vars.density`. - * 2. **Maps** each component's sized tokens to density steps through injected - * `styleOverrides.root` (e.g. `--Button-medium-pad: var(--mui-density-xs) var(--mui-density-lg)`). - * - * `createTheme` is left untouched; without this function components render their - * literal-px defaults. - * - * @param themeInput - The created theme to enhance. - * @param options - Override the density scale. - * @returns The enhanced theme. - * - * @example - * const theme = enhanceDensity(createTheme({ cssVariables: true })); - * - * @example - * const theme = enhanceDensity(createTheme(), { scale: { lg: '12px' } }); - */ -export default function enhanceDensity< - T extends { - spacing: (value: number) => string | number; - components?: Theme['components'] | undefined; - vars?: Record | undefined; - }, ->(themeInput: T, options?: DensityOptions): T & { density: DensityScale } { - const scale = densityKeys.reduce((acc, key) => { - acc[key] = options?.scale?.[key] ?? String(themeInput.spacing(defaultMultiplier[key])); - return acc; - }, {} as DensityScale); - - const rootVars = densityKeys.reduce>((acc, key) => { - acc[cssVar(key)] = scale[key]; - return acc; - }, {}); - - const varRefs = densityKeys.reduce((acc, key) => { - acc[key] = `var(${cssVar(key)})`; - return acc; - }, {} as DensityScale); - - const theme = { ...themeInput } as T & { density: DensityScale }; - theme.density = scale; - theme.vars = { ...themeInput.vars, density: varRefs }; - - const c = themeInput.components; - const existingBaseline = c?.MuiCssBaseline?.styleOverrides; - const baselineObject = - existingBaseline && typeof existingBaseline === 'object' ? existingBaseline : undefined; - - theme.components = { - ...c, - MuiCssBaseline: { - ...c?.MuiCssBaseline, - styleOverrides: { - ...baselineObject, - ':root': { - ...(baselineObject as any)?.[':root'], - ...rootVars, - }, - }, - }, - MuiButton: { - ...c?.MuiButton, - styleOverrides: { - ...c?.MuiButton?.styleOverrides, - root: [ - c?.MuiButton?.styleOverrides?.root, - { - // Sized-only: each size's `pad` shorthand (block inline) maps to its - // own density step, so tuning the scale keeps the per-size matrix. - // De-prefixed (Option A): emit through the same static map the styled - // fn reads, so names can't drift. Bare, unprefixed. - [buttonVars.smallPad]: `${varRefs.xxs} ${varRefs.sm}`, - [buttonVars.mediumPad]: `${varRefs.xs} ${varRefs.lg}`, - [buttonVars.largePad]: `${varRefs.sm} ${varRefs.xl}`, - }, - ], - }, - }, - }; - - return theme; -} diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts new file mode 100644 index 00000000000000..f14e6aa07493da --- /dev/null +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -0,0 +1,13 @@ +import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; + +// Normal preset data — explicit px (self-contained, not spacing-derived) + button +// typography. No logic beyond the wrapper; if logic creeps in here, the preset +// split has drifted. +const normal: DensityConfig = { + scale: { xxs: '4px', xs: '6px', sm: '8px', md: '12px', lg: '16px', xl: '24px', xxl: '32px' }, + typography: { fontSize: '0.875rem', lineHeight: 1.75 }, +}; + +export default function enhanceNormalDensity(theme: T) { + return applyDensity(theme, normal); +} diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index 1ea1b94fc81219..2a877f11ec453e 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -9,12 +9,10 @@ export { CssThemeVariables, } from './createTheme'; export { default as enhanceHighContrast, HighContrastTokens } from './enhanceHighContrast'; -export { - default as enhanceDensity, - DENSITY_PRESETS, - DensityScale, - DensityOptions, -} from './enhanceDensity'; +export { default as enhanceCompactDensity } from './enhanceCompactDensity'; +export { default as enhanceNormalDensity } from './enhanceNormalDensity'; +export { default as enhanceComfortDensity } from './enhanceComfortDensity'; +export { DensityScale, DensityConfig, DensityTypography } from './densityScale'; export { default as adaptV4Theme, DeprecatedThemeOptions } from './adaptV4Theme'; export { Shadows } from './shadows'; export { ZIndex } from './zIndex'; diff --git a/packages/mui-material/src/styles/index.js b/packages/mui-material/src/styles/index.js index 1986b193144490..bb339991e439f2 100644 --- a/packages/mui-material/src/styles/index.js +++ b/packages/mui-material/src/styles/index.js @@ -26,7 +26,9 @@ export function experimental_sx() { } export { default as createTheme } from './createTheme'; export { default as enhanceHighContrast } from './enhanceHighContrast'; -export { default as enhanceDensity, DENSITY_PRESETS } from './enhanceDensity'; +export { default as enhanceCompactDensity } from './enhanceCompactDensity'; +export { default as enhanceNormalDensity } from './enhanceNormalDensity'; +export { default as enhanceComfortDensity } from './enhanceComfortDensity'; export { default as unstable_createMuiStrictModeTheme } from './createMuiStrictModeTheme'; export { default as createStyles } from './createStyles'; export { getUnit as unstable_getUnit, toUnitless as unstable_toUnitless } from './cssUtils'; From f73c182b874ae07f3b20573abb5804e3f4995890 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 14:09:09 +0700 Subject: [PATCH 041/114] density: rename experiment page url to density-playground --- .../{density-experiment.tsx => density-playground.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/pages/experiments/{density-experiment.tsx => density-playground.tsx} (100%) diff --git a/docs/pages/experiments/density-experiment.tsx b/docs/pages/experiments/density-playground.tsx similarity index 100% rename from docs/pages/experiments/density-experiment.tsx rename to docs/pages/experiments/density-playground.tsx From 6b4b71bef0e0218ab64b3091dae50a87d6c1cef9 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 14:13:57 +0700 Subject: [PATCH 042/114] density: nest preset typography under variant key ({ button: {...} }) typography config now mirrors theme.typography shape instead of a flat button-only { fontSize, lineHeight }; applyDensity merges each variant. Behavior unchanged. --- .../mui-material/src/styles/densityScale.ts | 20 +++++++++++-------- .../src/styles/enhanceComfortDensity.ts | 2 +- .../src/styles/enhanceCompactDensity.ts | 2 +- .../src/styles/enhanceNormalDensity.ts | 2 +- packages/mui-material/src/styles/index.d.ts | 7 ++++++- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index 8a2633b037638b..993e16e02a8870 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -16,15 +16,19 @@ export interface DensityScale { } /** - * Button typography reflow applied alongside the scale. Handled here (not via - * component vars) so every family that renders `theme.typography.button` tracks - * the preset. + * Per-variant typography reflow applied alongside the scale. Handled here (not + * via component vars) so every family that renders `theme.typography.` + * tracks the preset. Currently only `button` is reflowed. */ -export interface DensityTypography { +export interface DensityTypographyVariant { fontSize?: string | undefined; lineHeight?: number | string | undefined; } +export interface DensityTypography { + button?: DensityTypographyVariant | undefined; +} + export interface DensityConfig { scale: DensityScale; typography?: DensityTypography | undefined; @@ -82,10 +86,10 @@ export function applyDensity( theme.vars = { ...themeInput.vars, density: varRefs }; if (typography) { - theme.typography = { - ...themeInput.typography, - button: { ...themeInput.typography?.button, ...typography }, - }; + theme.typography = { ...themeInput.typography }; + (Object.keys(typography) as (keyof DensityTypography)[]).forEach((variant) => { + theme.typography![variant] = { ...themeInput.typography?.[variant], ...typography[variant] }; + }); } const c = themeInput.components; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index dbf7a727e4fba4..9c8512d043aee8 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -4,7 +4,7 @@ import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; // wrapper; if logic creeps in here, the preset split has drifted. const comfort: DensityConfig = { scale: { xxs: '6px', xs: '8px', sm: '12px', md: '16px', lg: '24px', xl: '32px', xxl: '40px' }, - typography: { fontSize: '0.9375rem', lineHeight: 2 }, + typography: { button: { fontSize: '0.9375rem', lineHeight: 2 } }, }; export default function enhanceComfortDensity(theme: T) { diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 46af2336f8d00d..028fb33b992dd4 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -4,7 +4,7 @@ import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; // the wrapper; if logic creeps in here, the preset split has drifted. const compact: DensityConfig = { scale: { xxs: '2px', xs: '4px', sm: '6px', md: '8px', lg: '12px', xl: '18px', xxl: '24px' }, - typography: { fontSize: '0.8125rem', lineHeight: 1.5 }, + typography: { button: { fontSize: '0.8125rem', lineHeight: 1.5 } }, }; export default function enhanceCompactDensity(theme: T) { diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index f14e6aa07493da..9cb971c6379f22 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -5,7 +5,7 @@ import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; // split has drifted. const normal: DensityConfig = { scale: { xxs: '4px', xs: '6px', sm: '8px', md: '12px', lg: '16px', xl: '24px', xxl: '32px' }, - typography: { fontSize: '0.875rem', lineHeight: 1.75 }, + typography: { button: { fontSize: '0.875rem', lineHeight: 1.75 } }, }; export default function enhanceNormalDensity(theme: T) { diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index 2a877f11ec453e..79a9f57ff588b5 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -12,7 +12,12 @@ export { default as enhanceHighContrast, HighContrastTokens } from './enhanceHig export { default as enhanceCompactDensity } from './enhanceCompactDensity'; export { default as enhanceNormalDensity } from './enhanceNormalDensity'; export { default as enhanceComfortDensity } from './enhanceComfortDensity'; -export { DensityScale, DensityConfig, DensityTypography } from './densityScale'; +export { + DensityScale, + DensityConfig, + DensityTypography, + DensityTypographyVariant, +} from './densityScale'; export { default as adaptV4Theme, DeprecatedThemeOptions } from './adaptV4Theme'; export { Shadows } from './shadows'; export { ZIndex } from './zIndex'; From af74d4db0150c7768339a6a95e8b5cfdc027b684 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 14:19:27 +0700 Subject: [PATCH 043/114] density: drop typography config, colocate per-preset in enhance* files applyDensity(theme, scale) now handles scale + var mapping only. compact/ comfort set theme.typography.button directly; normal leaves it untouched (= today). Removes DensityConfig/DensityTypography/DensityTypographyVariant. Zero-diff gate 3/3; reflow unchanged. --- .../mui-material/src/styles/densityScale.ts | 35 +++---------------- .../src/styles/enhanceComfortDensity.ts | 22 ++++++++---- .../src/styles/enhanceCompactDensity.ts | 22 ++++++++---- .../src/styles/enhanceNormalDensity.ts | 20 ++++++----- packages/mui-material/src/styles/index.d.ts | 7 +--- 5 files changed, 47 insertions(+), 59 deletions(-) diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index 993e16e02a8870..850ff5b0bb4807 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -15,25 +15,6 @@ export interface DensityScale { xxl: string; } -/** - * Per-variant typography reflow applied alongside the scale. Handled here (not - * via component vars) so every family that renders `theme.typography.` - * tracks the preset. Currently only `button` is reflowed. - */ -export interface DensityTypographyVariant { - fontSize?: string | undefined; - lineHeight?: number | string | undefined; -} - -export interface DensityTypography { - button?: DensityTypographyVariant | undefined; -} - -export interface DensityConfig { - scale: DensityScale; - typography?: DensityTypography | undefined; -} - export type DensityKey = keyof DensityScale; export const DENSITY_KEYS: DensityKey[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']; @@ -59,18 +40,17 @@ const cssVar = (key: DensityKey) => `--mui-density-${key}`; * through injected `styleOverrides.root`. The mapping is identical across * presets — only the scale values differ — so it lives here once. * - * Also merges `config.typography` into `theme.typography.button`. + * Typography reflow is NOT handled here — each preset applies its own (or none, + * for `normal`) after calling this. * * @param themeInput - The created theme to enhance. - * @param config - The preset's scale + typography. + * @param scale - The preset's 7-step scale. * @returns The enhanced theme. */ export function applyDensity( themeInput: T, - config: DensityConfig, + scale: DensityScale, ): T & { density: DensityScale } { - const { scale, typography } = config; - const rootVars = DENSITY_KEYS.reduce>((acc, key) => { acc[cssVar(key)] = scale[key]; return acc; @@ -85,13 +65,6 @@ export function applyDensity( theme.density = scale; theme.vars = { ...themeInput.vars, density: varRefs }; - if (typography) { - theme.typography = { ...themeInput.typography }; - (Object.keys(typography) as (keyof DensityTypography)[]).forEach((variant) => { - theme.typography![variant] = { ...themeInput.typography?.[variant], ...typography[variant] }; - }); - } - const c = themeInput.components; const existingBaseline = c?.MuiCssBaseline?.styleOverrides; const baselineObject = diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 9c8512d043aee8..d3988cc11a4f3a 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -1,12 +1,20 @@ -import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; +import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; -// Comfort preset data — scale (7 steps) + button typography. No logic beyond the -// wrapper; if logic creeps in here, the preset split has drifted. -const comfort: DensityConfig = { - scale: { xxs: '6px', xs: '8px', sm: '12px', md: '16px', lg: '24px', xl: '32px', xxl: '40px' }, - typography: { button: { fontSize: '0.9375rem', lineHeight: 2 } }, +const scale: DensityScale = { + xxs: '6px', + xs: '8px', + sm: '12px', + md: '16px', + lg: '24px', + xl: '32px', + xxl: '40px', }; export default function enhanceComfortDensity(theme: T) { - return applyDensity(theme, comfort); + const enhanced = applyDensity(theme, scale); + enhanced.typography = { + ...enhanced.typography, + button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, + }; + return enhanced; } diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 028fb33b992dd4..3c45cd4cd4089d 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -1,12 +1,20 @@ -import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; +import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; -// Compact preset data — scale (7 steps) + button typography. No logic beyond -// the wrapper; if logic creeps in here, the preset split has drifted. -const compact: DensityConfig = { - scale: { xxs: '2px', xs: '4px', sm: '6px', md: '8px', lg: '12px', xl: '18px', xxl: '24px' }, - typography: { button: { fontSize: '0.8125rem', lineHeight: 1.5 } }, +const scale: DensityScale = { + xxs: '2px', + xs: '4px', + sm: '6px', + md: '8px', + lg: '12px', + xl: '18px', + xxl: '24px', }; export default function enhanceCompactDensity(theme: T) { - return applyDensity(theme, compact); + const enhanced = applyDensity(theme, scale); + enhanced.typography = { + ...enhanced.typography, + button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, + }; + return enhanced; } diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 9cb971c6379f22..193bfce872a1c5 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -1,13 +1,17 @@ -import { applyDensity, DensityConfig, EnhanceableTheme } from './densityScale'; +import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; -// Normal preset data — explicit px (self-contained, not spacing-derived) + button -// typography. No logic beyond the wrapper; if logic creeps in here, the preset -// split has drifted. -const normal: DensityConfig = { - scale: { xxs: '4px', xs: '6px', sm: '8px', md: '12px', lg: '16px', xl: '24px', xxl: '32px' }, - typography: { button: { fontSize: '0.875rem', lineHeight: 1.75 } }, +// Explicit px (self-contained, not spacing-derived). Normal keeps today's Button +// typography — no reflow, so nothing beyond the scale. +const scale: DensityScale = { + xxs: '4px', + xs: '6px', + sm: '8px', + md: '12px', + lg: '16px', + xl: '24px', + xxl: '32px', }; export default function enhanceNormalDensity(theme: T) { - return applyDensity(theme, normal); + return applyDensity(theme, scale); } diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index 79a9f57ff588b5..f24e48e5974caf 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -12,12 +12,7 @@ export { default as enhanceHighContrast, HighContrastTokens } from './enhanceHig export { default as enhanceCompactDensity } from './enhanceCompactDensity'; export { default as enhanceNormalDensity } from './enhanceNormalDensity'; export { default as enhanceComfortDensity } from './enhanceComfortDensity'; -export { - DensityScale, - DensityConfig, - DensityTypography, - DensityTypographyVariant, -} from './densityScale'; +export { DensityScale } from './densityScale'; export { default as adaptV4Theme, DeprecatedThemeOptions } from './adaptV4Theme'; export { Shadows } from './shadows'; export { ZIndex } from './zIndex'; From 7bebb83c1fe8c5a4928fc1e09e8a8a5591a01e98 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 14:37:53 +0700 Subject: [PATCH 044/114] density: applyDensity emits scale only; presets own component-var mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyDensity(theme, scale) now handles MuiCssBaseline + theme.density/vars only — component-agnostic. Each enhance*Density maps its component vars to density steps inside its own fn (via new addRootOverride helper + exported densityVars refs), so a preset can point a token at a different step than its siblings. Button padding mapping identical across presets today. Zero-diff gate 3/3; reflow unchanged. --- packages/mui-material/src/Button/Button.js | 4 +- .../mui-material/src/Button/buttonVars.ts | 4 +- .../mui-material/src/styles/densityScale.ts | 78 ++++++++++--------- .../src/styles/enhanceComfortDensity.ts | 8 +- .../src/styles/enhanceCompactDensity.ts | 8 +- .../src/styles/enhanceNormalDensity.ts | 13 +++- 6 files changed, 69 insertions(+), 46 deletions(-) diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js index 92db7975ececb1..9a5ddcca9cba1d 100644 --- a/packages/mui-material/src/Button/Button.js +++ b/packages/mui-material/src/Button/Button.js @@ -95,8 +95,8 @@ const ButtonRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => { // Material UI layer: the internal sized tokens are the static, unprefixed - // `private_buttonVars` map, imported here and by the density core - // (`applyDensity`) so emitted and targeted names can't drift. The agnostic seam (`--comp-pad`) + // `private_buttonVars` map, imported here and by the `enhance*Density` + // presets so emitted and targeted names can't drift. The agnostic seam (`--comp-pad`) // and internal default (`--_pad`) are literal and unprefixed. const inheritContainedBackgroundColor = theme.palette.mode === 'light' ? theme.palette.grey[300] : theme.palette.grey[800]; diff --git a/packages/mui-material/src/Button/buttonVars.ts b/packages/mui-material/src/Button/buttonVars.ts index 06373bf15eeb00..395ee1712bddcd 100644 --- a/packages/mui-material/src/Button/buttonVars.ts +++ b/packages/mui-material/src/Button/buttonVars.ts @@ -2,8 +2,8 @@ /** * Button density token identities — the Material UI layer's internal * designer knobs (`private_*` per the density RFC). Static, unprefixed literals - * imported by both the styled component AND the density core (`applyDensity`), - * so the emitted and + * imported by both the styled component AND the `enhance*Density` presets that + * map these to density steps, so the emitted and * targeted names can't drift. The agnostic seam (`--comp-pad`) and internal * default (`--_pad`) are the literal plumbing and live outside this map. */ diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index 850ff5b0bb4807..a71b5f8d44205d 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -1,9 +1,8 @@ import { Theme } from './createTheme'; -import { private_buttonVars as buttonVars } from '../Button/buttonVars'; /** - * Named density steps, surfaced as `--mui-density-*` CSS vars. Components wired - * by the `enhance*Density` presets pull their spacing tokens from these. + * Named density steps, surfaced as `--mui-density-*` CSS vars. Presets assign a + * component's sized tokens to these steps (via `densityVars` + `addRootOverride`). */ export interface DensityScale { xxs: string; @@ -28,20 +27,25 @@ export type EnhanceableTheme = { const cssVar = (key: DensityKey) => `--mui-density-${key}`; +/** + * `var(--mui-density-*)` reference for each step. Presets read these to point a + * component var at a chosen step, e.g. `[buttonVars.mediumPad]: `${densityVars.xs} ${densityVars.lg}``. + */ +export const densityVars: DensityScale = DENSITY_KEYS.reduce((acc, key) => { + acc[key] = `var(${cssVar(key)})`; + return acc; +}, {} as DensityScale); + /** * PRIVATE density core shared by the three `enhance*Density` presets. Not * re-exported from the styles barrel — presets are the public surface. * - * Does both jobs in one call (mirroring `enhanceHighContrast`): - * 1. **Emits** the scale as `--mui-density-*` CSS vars at `:root` (via - * `MuiCssBaseline` — requires ``) and exposes it on - * `theme.density` / `theme.vars.density`. - * 2. **Maps** each family's sized tokens (`private_*Vars`) to density steps - * through injected `styleOverrides.root`. The mapping is identical across - * presets — only the scale values differ — so it lives here once. - * - * Typography reflow is NOT handled here — each preset applies its own (or none, - * for `normal`) after calling this. + * **Scale emission only.** Emits the scale as `--mui-density-*` CSS vars at + * `:root` (via `MuiCssBaseline` — requires ``) and exposes it on + * `theme.density` / `theme.vars.density`. It is **component-agnostic**: it does + * NOT touch any `Mui*` component. Each preset maps component vars → density + * steps itself (`addRootOverride`), so a preset can point the same token at a + * different step than its siblings. * * @param themeInput - The created theme to enhance. * @param scale - The preset's 7-step scale. @@ -56,14 +60,9 @@ export function applyDensity( return acc; }, {}); - const varRefs = DENSITY_KEYS.reduce((acc, key) => { - acc[key] = `var(${cssVar(key)})`; - return acc; - }, {} as DensityScale); - const theme = { ...themeInput } as T & { density: DensityScale }; theme.density = scale; - theme.vars = { ...themeInput.vars, density: varRefs }; + theme.vars = { ...themeInput.vars, density: densityVars }; const c = themeInput.components; const existingBaseline = c?.MuiCssBaseline?.styleOverrides; @@ -82,25 +81,30 @@ export function applyDensity( }, }, }, - MuiButton: { - ...c?.MuiButton, - styleOverrides: { - ...c?.MuiButton?.styleOverrides, - root: [ - c?.MuiButton?.styleOverrides?.root, - { - // Sized-only: each size's `pad` shorthand (block inline) maps to its - // own density step, so tuning the scale keeps the per-size matrix. - // Emit through the same static map the styled fn reads — names can't - // drift. Bare, unprefixed. - [buttonVars.smallPad]: `${varRefs.xxs} ${varRefs.sm}`, - [buttonVars.mediumPad]: `${varRefs.xs} ${varRefs.lg}`, - [buttonVars.largePad]: `${varRefs.sm} ${varRefs.xl}`, - }, - ], - }, - }, }; return theme; } + +/** + * Append a `styleOverrides.root` object to a component slot, preserving any + * existing root overrides (array-wrapped). Presets use this to attach their + * component-var → density-step assignments after `applyDensity`. + */ +export function addRootOverride( + components: C, + name: string, + root: Record, +): C { + const existing = (components as any)?.[name]; + return { + ...components, + [name]: { + ...existing, + styleOverrides: { + ...existing?.styleOverrides, + root: [existing?.styleOverrides?.root, root], + }, + }, + } as C; +} diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index d3988cc11a4f3a..bd50ed14e08f1d 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -1,4 +1,5 @@ -import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; +import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; +import { private_buttonVars as buttonVars } from '../Button/buttonVars'; const scale: DensityScale = { xxs: '6px', @@ -12,6 +13,11 @@ const scale: DensityScale = { export default function enhanceComfortDensity(theme: T) { const enhanced = applyDensity(theme, scale); + enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, + [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, + [buttonVars.largePad]: `${d.sm} ${d.xl}`, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 3c45cd4cd4089d..c4c22d379b683c 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -1,4 +1,5 @@ -import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; +import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; +import { private_buttonVars as buttonVars } from '../Button/buttonVars'; const scale: DensityScale = { xxs: '2px', @@ -12,6 +13,11 @@ const scale: DensityScale = { export default function enhanceCompactDensity(theme: T) { const enhanced = applyDensity(theme, scale); + enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, + [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, + [buttonVars.largePad]: `${d.sm} ${d.xl}`, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 193bfce872a1c5..408e0988f87b68 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -1,7 +1,8 @@ -import { applyDensity, DensityScale, EnhanceableTheme } from './densityScale'; +import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; +import { private_buttonVars as buttonVars } from '../Button/buttonVars'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button -// typography — no reflow, so nothing beyond the scale. +// typography — no reflow — so only the padding→step assignment below. const scale: DensityScale = { xxs: '4px', xs: '6px', @@ -13,5 +14,11 @@ const scale: DensityScale = { }; export default function enhanceNormalDensity(theme: T) { - return applyDensity(theme, scale); + const enhanced = applyDensity(theme, scale); + enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, + [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, + [buttonVars.largePad]: `${d.sm} ${d.xl}`, + }); + return enhanced; } From f7f8f2f425a4b80790c32928879f3aa0597c462f Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 14:44:46 +0700 Subject: [PATCH 045/114] density: addRootOverride mutates in place, drop call-site reassignment Helper now mutates the enhanced theme's (fresh, applyDensity-owned) components in place and returns void; applyDensity return type asserts components is defined so call sites need no reassignment or non-null assertion. Behavior unchanged (zero-diff 3/3; compact 4px 12px, comfort 8px 24px). --- .../mui-material/src/styles/densityScale.ts | 37 ++++++++++--------- .../src/styles/enhanceComfortDensity.ts | 2 +- .../src/styles/enhanceCompactDensity.ts | 2 +- .../src/styles/enhanceNormalDensity.ts | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index a71b5f8d44205d..676d06483f16c6 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -54,13 +54,16 @@ export const densityVars: DensityScale = DENSITY_KEYS.reduce((acc, key) => { export function applyDensity( themeInput: T, scale: DensityScale, -): T & { density: DensityScale } { +): T & { density: DensityScale; components: NonNullable } { const rootVars = DENSITY_KEYS.reduce>((acc, key) => { acc[cssVar(key)] = scale[key]; return acc; }, {}); - const theme = { ...themeInput } as T & { density: DensityScale }; + const theme = { ...themeInput } as T & { + density: DensityScale; + components: NonNullable; + }; theme.density = scale; theme.vars = { ...themeInput.vars, density: densityVars }; @@ -87,24 +90,24 @@ export function applyDensity( } /** - * Append a `styleOverrides.root` object to a component slot, preserving any - * existing root overrides (array-wrapped). Presets use this to attach their + * Attach a `styleOverrides.root` object to a component slot, preserving any + * existing root overrides (array-wrapped). Presets use this to add their * component-var → density-step assignments after `applyDensity`. + * + * **Mutates `components` in place** — pass the enhanced theme's `components` + * (fresh, owned by `applyDensity`), never a theme's shared `components`. */ -export function addRootOverride( - components: C, +export function addRootOverride( + components: NonNullable, name: string, root: Record, -): C { - const existing = (components as any)?.[name]; - return { - ...components, - [name]: { - ...existing, - styleOverrides: { - ...existing?.styleOverrides, - root: [existing?.styleOverrides?.root, root], - }, +): void { + const slot = (components as any)[name]; + (components as any)[name] = { + ...slot, + styleOverrides: { + ...slot?.styleOverrides, + root: [slot?.styleOverrides?.root, root], }, - } as C; + }; } diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index bd50ed14e08f1d..f3366fd9883b50 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -13,7 +13,7 @@ const scale: DensityScale = { export default function enhanceComfortDensity(theme: T) { const enhanced = applyDensity(theme, scale); - enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + addRootOverride(enhanced.components, 'MuiButton', { [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index c4c22d379b683c..4a26018a85b7b5 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -13,7 +13,7 @@ const scale: DensityScale = { export default function enhanceCompactDensity(theme: T) { const enhanced = applyDensity(theme, scale); - enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + addRootOverride(enhanced.components, 'MuiButton', { [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 408e0988f87b68..447f308e64014d 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -15,7 +15,7 @@ const scale: DensityScale = { export default function enhanceNormalDensity(theme: T) { const enhanced = applyDensity(theme, scale); - enhanced.components = addRootOverride(enhanced.components, 'MuiButton', { + addRootOverride(enhanced.components, 'MuiButton', { [buttonVars.smallPad]: `${d.xxs} ${d.sm}`, [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, From 144c6899220d87cfa02bb9fa4b61ea419c4c4e16 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 15:21:38 +0700 Subject: [PATCH 046/114] density playground: visual debug toolbar (padding + text box overlays) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent ToggleButtons toggle data-debug-* on the canvas; pure-CSS translucent overlays — ::before fill for padding (green), label-span bg for text box (blue), lifted above the padding overlay so text stays crisp. Layout-safe (absolute + pointer-events:none), never touches component styles. --- docs/pages/experiments/density-playground.tsx | 99 ++++++++++++++++--- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 045de6a6260b43..b147c8dd74eb4e 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -14,6 +14,11 @@ import MenuItem from '@mui/material/MenuItem'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tooltip from '@mui/material/Tooltip'; +import PaddingIcon from '@mui/icons-material/Padding'; +import TitleIcon from '@mui/icons-material/Title'; import { createTheme, ThemeProvider, @@ -32,6 +37,26 @@ type Preset = (typeof PRESETS)[number]; type Size = (typeof SIZES)[number]; type MappingKey = `${Size}Pad`; +// Visual-debug overlays, toggled by `data-debug-*` on the canvas. Pure CSS, +// layout-safe (absolute ::before + pointer-events:none), never touches the +// components' real styles. The label span sits above the padding overlay +// (z-index) so text stays crisp; its blue fill only shows in text mode. +const DEBUG_SX = { + '& .density-debug-text': { position: 'relative', zIndex: 1, borderRadius: '2px' }, + '&[data-debug-padding] .MuiButtonBase-root': { position: 'relative' }, + '&[data-debug-padding] .MuiButtonBase-root::before': { + content: '""', + position: 'absolute', + inset: 0, + borderRadius: 'inherit', + backgroundColor: 'rgba(46, 204, 64, 0.28)', // padding = green (DevTools convention) + pointerEvents: 'none', + }, + '&[data-debug-text] .density-debug-text': { + backgroundColor: 'rgba(0, 116, 217, 0.32)', // text box = blue + }, +} as const; + const PRESET_LABEL: Record = { unset: 'unset — no fn · today · 0-diff', compact: 'compact', @@ -128,7 +153,7 @@ function ButtonMatrix({ sx={sx} data-cell={`${variant}-${size}`} > - {variant} + {variant} ))}
@@ -163,6 +188,7 @@ const initialMapping = () => export default function DensityExperiment() { const [preset, setPreset] = React.useState('unset'); const [selection, setSelection] = React.useState('All'); + const [debug, setDebug] = React.useState([]); const [mapping, setMapping] = React.useState>>( initialMapping, ); @@ -324,22 +350,63 @@ export default function DensityExperiment() { - {/* CANVAS — wrapped in the density-enhanced theme. */} - - - - - {visibleComponents.map((comp) => ( - - - {COMPONENT_DEFS[comp].canvasLabel} - - {COMPONENT_DEFS[comp].renderMatrix({ mapping: mapping[comp], mappingEnabled })} - - ))} - + {/* RIGHT COLUMN — debug toolbar (plain theme) + themed canvas. */} + + + + Visual debug + + setDebug(next)} + aria-label="visual debug overlays" + > + + + + + + + + + + + - + + {/* CANVAS — wrapped in the density-enhanced theme. */} + + + + + {visibleComponents.map((comp) => ( + + + {COMPONENT_DEFS[comp].canvasLabel} + + {COMPONENT_DEFS[comp].renderMatrix({ mapping: mapping[comp], mappingEnabled })} + + ))} + + + + ); From c10aa5d9d3219501010df3061432b3934bc11458 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 15:26:01 +0700 Subject: [PATCH 047/114] density playground: padding overlay highlights only the ring, not content ::before inherits button padding + mask-composite:exclude knocks out the content box, so green fills only the padding region (adapts to any size). --- docs/pages/experiments/density-playground.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index b147c8dd74eb4e..83bea17060c0de 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -48,9 +48,18 @@ const DEBUG_SX = { content: '""', position: 'absolute', inset: 0, + // `inset:0` sizes the overlay to the button's padding-box; `padding:inherit` + // then shrinks its content-box to the button's content box, and the + // `exclude` mask knocks that center out → green fills only the padding ring. + padding: 'inherit', + boxSizing: 'border-box', borderRadius: 'inherit', - backgroundColor: 'rgba(46, 204, 64, 0.28)', // padding = green (DevTools convention) + backgroundColor: 'rgba(46, 204, 64, 0.5)', // padding = green (DevTools convention) pointerEvents: 'none', + WebkitMask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', + WebkitMaskComposite: 'xor', + mask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', + maskComposite: 'exclude', }, '&[data-debug-text] .density-debug-text': { backgroundColor: 'rgba(0, 116, 217, 0.32)', // text box = blue From cb446872a309138a1c14e8b747995281f16e4d48 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 16:06:55 +0700 Subject: [PATCH 048/114] density Epic 2: MenuItem coverage (dense axis) Tokenize MenuItem on the dense boolean axis (three-layer --comp-* seams over --_* defaults; base routes non-dense public token, dense variant swaps in dense default + dense token). Static private_menuItemVars map, re-exported from index. All 3 enhance*Density map MuiMenuItem via addRootOverride: minHeight->xl/dense->lg, blockPad->xs/dense->xxs, inlinePad->lg/dense->md. Add MenuItem to density fixture + playground. unset pixel-identical to master (zero-diff gate, both dense states); compact/comfort reflow both rows. MenuItem.test 58 pass. --- docs/pages/experiments/density-fixture.tsx | 18 +++++++++ docs/pages/experiments/density-playground.tsx | 36 ++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 32 +++++++++++++++- .../mui-material/src/MenuItem/MenuItem.js | 37 +++++++++++++++---- packages/mui-material/src/MenuItem/index.d.ts | 1 + packages/mui-material/src/MenuItem/index.js | 1 + .../mui-material/src/MenuItem/menuItemVars.ts | 20 ++++++++++ .../src/styles/enhanceComfortDensity.ts | 9 +++++ .../src/styles/enhanceCompactDensity.ts | 9 +++++ .../src/styles/enhanceNormalDensity.ts | 9 +++++ 10 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 packages/mui-material/src/MenuItem/menuItemVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 70a2f20c058159..6e04af2bc79675 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -28,6 +28,24 @@ const scopes: Record> = { ['--Button-large-pad' as any]: '16px 30px', }, }, + MenuItem: { + dense: { + ['--MenuItem-minHeight' as any]: '36px', + ['--MenuItem-block-pad' as any]: '3px', + ['--MenuItem-inline-pad' as any]: '10px', + ['--MenuItem-dense-minHeight' as any]: '26px', + ['--MenuItem-dense-block-pad' as any]: '2px', + ['--MenuItem-dense-inline-pad' as any]: '8px', + }, + loose: { + ['--MenuItem-minHeight' as any]: '60px', + ['--MenuItem-block-pad' as any]: '12px', + ['--MenuItem-inline-pad' as any]: '28px', + ['--MenuItem-dense-minHeight' as any]: '44px', + ['--MenuItem-dense-block-pad' as any]: '8px', + ['--MenuItem-dense-inline-pad' as any]: '20px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 83bea17060c0de..2de4e366697b7f 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -11,6 +11,10 @@ import FormLabel from '@mui/material/FormLabel'; import FormControlLabel from '@mui/material/FormControlLabel'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import InboxIcon from '@mui/icons-material/Inbox'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; @@ -173,6 +177,29 @@ function ButtonMatrix({ ); } +function MenuItemMatrix() { + return ( + + Default item + Selected item + + + + + With icon + + With divider + Dense item + + + + + Dense + icon + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -181,6 +208,15 @@ const COMPONENT_DEFS = { prefill: { smallPad: 'xxs sm', mediumPad: 'xs lg', largePad: 'sm xl' }, renderMatrix: (args) => , }, + MenuItem: { + canvasLabel: 'MenuItem (default + dense) — preset-driven', + // Preset-driven only: MenuItem reflows via enhance*Density's MuiMenuItem + // mapping; no per-element mapping inputs (its dense axis differs from + // Button's sized pads). Flip the preset to see it reflow. + fields: [], + prefill: {}, + renderMatrix: () => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 309bad12e6616b..e8ab37dfa1dbad 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -1,8 +1,13 @@ import * as React from 'react'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import InboxIcon from '@mui/icons-material/Inbox'; -// Shared density demo matrix for the CSS-var density adapter (Button only). +// Shared density demo matrix for the CSS-var density adapter. // Consumed by the screenshot fixture (density-fixture). `level=default` (no token // overrides) must stay pixel-identical to the pre-change baseline. const demos: Record = { @@ -23,6 +28,31 @@ const demos: Record = { ))}
), + MenuItem: ( + // MenuItem requires a MenuList/Menu ancestor (MenuListContext). + + Default item + Selected item + + + + + With icon + + With divider + No gutters + Dense item + + + + + Dense + icon + + + Dense no gutters + + + ), }; export default demos; diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 68df44938b4404..27f66dee9af243 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -20,6 +20,7 @@ import { listItemTextClasses } from '../ListItemText'; import { useMenuListContext } from '../MenuList/MenuListContext'; import { useSelectFocusSource } from '../Select/utils'; import menuItemClasses, { getMenuItemUtilityClass } from './menuItemClasses'; +import { private_menuItemVars as menuItemVars } from './menuItemVars'; export const overridesResolver = (props, styles) => { const { ownerState } = props; @@ -61,14 +62,24 @@ const MenuItemRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => ({ ...theme.typography.body1, + // Material UI layer: internal sized tokens are the static, unprefixed + // `private_menuItemVars` map, imported here and by the `enhance*Density` + // presets so emitted and targeted names can't drift. Density axis is the + // `dense` boolean: the base routes the non-dense public token over the + // internal default (`--_`) into the agnostic seam (`--comp-`); + // the `dense` variant swaps in the `dense` default + dense public token. + '--_minHeight': '48px', + '--_blockPad': '6px', + [`--comp-minHeight`]: `var(${menuItemVars.minHeight}, var(--_minHeight))`, + [`--comp-blockPad`]: `var(${menuItemVars.blockPad}, var(--_blockPad))`, display: 'flex', justifyContent: 'flex-start', alignItems: 'center', position: 'relative', textDecoration: 'none', - minHeight: 48, - paddingTop: 6, - paddingBottom: 6, + minHeight: 'var(--comp-minHeight)', + paddingTop: 'var(--comp-blockPad)', + paddingBottom: 'var(--comp-blockPad)', boxSizing: 'border-box', whiteSpace: 'nowrap', '&:hover': { @@ -131,8 +142,16 @@ const MenuItemRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + '--_inlinePad': '16px', + [`--comp-inlinePad`]: `var(${menuItemVars.inlinePad}, var(--_inlinePad))`, + paddingLeft: 'var(--comp-inlinePad)', + paddingRight: 'var(--comp-inlinePad)', + }, + }, + { + props: ({ ownerState }) => !ownerState.disableGutters && ownerState.dense, + style: { + [`--comp-inlinePad`]: `var(${menuItemVars.denseInlinePad}, var(--_inlinePad))`, }, }, { @@ -153,9 +172,11 @@ const MenuItemRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.dense, style: { - minHeight: 32, // https://m2.material.io/components/menus#specs > Dense - paddingTop: 4, - paddingBottom: 4, + // https://m2.material.io/components/menus#specs > Dense + '--_minHeight': '32px', + '--_blockPad': '4px', + [`--comp-minHeight`]: `var(${menuItemVars.denseMinHeight}, var(--_minHeight))`, + [`--comp-blockPad`]: `var(${menuItemVars.denseBlockPad}, var(--_blockPad))`, ...theme.typography.body2, [`& .${listItemIconClasses.root} svg`]: { fontSize: '1.25rem', diff --git a/packages/mui-material/src/MenuItem/index.d.ts b/packages/mui-material/src/MenuItem/index.d.ts index 6ef0ce0d270227..8c4e43c1f59744 100644 --- a/packages/mui-material/src/MenuItem/index.d.ts +++ b/packages/mui-material/src/MenuItem/index.d.ts @@ -3,3 +3,4 @@ export * from './MenuItem'; export * from './menuItemClasses'; export { default as menuItemClasses } from './menuItemClasses'; +export { private_menuItemVars } from './menuItemVars'; diff --git a/packages/mui-material/src/MenuItem/index.js b/packages/mui-material/src/MenuItem/index.js index ec789a4b43b2a9..024f71ad758d7e 100644 --- a/packages/mui-material/src/MenuItem/index.js +++ b/packages/mui-material/src/MenuItem/index.js @@ -2,3 +2,4 @@ export { default } from './MenuItem'; export * from './menuItemClasses'; export { default as menuItemClasses } from './menuItemClasses'; +export { private_menuItemVars } from './menuItemVars'; diff --git a/packages/mui-material/src/MenuItem/menuItemVars.ts b/packages/mui-material/src/MenuItem/menuItemVars.ts new file mode 100644 index 00000000000000..e786afcd08883b --- /dev/null +++ b/packages/mui-material/src/MenuItem/menuItemVars.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * MenuItem density token identities — the Material UI layer's internal + * designer knobs (`private_*` per the density RFC). Static, unprefixed literals + * imported by both the styled component AND the `enhance*Density` presets that + * map these to density steps, so the emitted and targeted names can't drift. + * The agnostic seam (`--comp-`) and internal default (`--_`) are the + * literal plumbing and live outside this map. + * + * MenuItem's compactness axis is the `dense` boolean (not small/medium/large): + * each sized token has a `dense` counterpart the dense variant routes to. + */ +export const private_menuItemVars = { + minHeight: '--MenuItem-minHeight', + denseMinHeight: '--MenuItem-dense-minHeight', + blockPad: '--MenuItem-block-pad', + denseBlockPad: '--MenuItem-dense-block-pad', + inlinePad: '--MenuItem-inline-pad', + denseInlinePad: '--MenuItem-dense-inline-pad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index f3366fd9883b50..b1982e56078d36 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -1,5 +1,6 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; +import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; const scale: DensityScale = { xxs: '6px', @@ -18,6 +19,14 @@ export default function enhanceComfortDensity(theme: [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); + addRootOverride(enhanced.components, 'MuiMenuItem', { + [menuItemVars.minHeight]: d.xl, + [menuItemVars.denseMinHeight]: d.lg, + [menuItemVars.blockPad]: d.xs, + [menuItemVars.denseBlockPad]: d.xxs, + [menuItemVars.inlinePad]: d.lg, + [menuItemVars.denseInlinePad]: d.md, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 4a26018a85b7b5..b198d714e422ed 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -1,5 +1,6 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; +import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; const scale: DensityScale = { xxs: '2px', @@ -18,6 +19,14 @@ export default function enhanceCompactDensity(theme: [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); + addRootOverride(enhanced.components, 'MuiMenuItem', { + [menuItemVars.minHeight]: d.xl, + [menuItemVars.denseMinHeight]: d.lg, + [menuItemVars.blockPad]: d.xs, + [menuItemVars.denseBlockPad]: d.xxs, + [menuItemVars.inlinePad]: d.lg, + [menuItemVars.denseInlinePad]: d.md, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 447f308e64014d..4ca17829adf72d 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -1,5 +1,6 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; +import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button // typography — no reflow — so only the padding→step assignment below. @@ -20,5 +21,13 @@ export default function enhanceNormalDensity(theme: [buttonVars.mediumPad]: `${d.xs} ${d.lg}`, [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); + addRootOverride(enhanced.components, 'MuiMenuItem', { + [menuItemVars.minHeight]: d.xl, + [menuItemVars.denseMinHeight]: d.lg, + [menuItemVars.blockPad]: d.xs, + [menuItemVars.denseBlockPad]: d.xxs, + [menuItemVars.inlinePad]: d.lg, + [menuItemVars.denseInlinePad]: d.md, + }); return enhanced; } From 533d7797655a09ee0213a503dc3e5e80d351fe78 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 16:42:46 +0700 Subject: [PATCH 049/114] density Epic 2 MenuItem: camelCase token names (blockPad not block-pad) Consistent with --MenuItem-minHeight and the map keys; drops the kebab/camel mix. --- docs/pages/experiments/density-fixture.tsx | 16 ++++++++-------- .../mui-material/src/MenuItem/menuItemVars.ts | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 6e04af2bc79675..6119d4266fef1e 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -31,19 +31,19 @@ const scopes: Record> = { MenuItem: { dense: { ['--MenuItem-minHeight' as any]: '36px', - ['--MenuItem-block-pad' as any]: '3px', - ['--MenuItem-inline-pad' as any]: '10px', + ['--MenuItem-blockPad' as any]: '3px', + ['--MenuItem-inlinePad' as any]: '10px', ['--MenuItem-dense-minHeight' as any]: '26px', - ['--MenuItem-dense-block-pad' as any]: '2px', - ['--MenuItem-dense-inline-pad' as any]: '8px', + ['--MenuItem-dense-blockPad' as any]: '2px', + ['--MenuItem-dense-inlinePad' as any]: '8px', }, loose: { ['--MenuItem-minHeight' as any]: '60px', - ['--MenuItem-block-pad' as any]: '12px', - ['--MenuItem-inline-pad' as any]: '28px', + ['--MenuItem-blockPad' as any]: '12px', + ['--MenuItem-inlinePad' as any]: '28px', ['--MenuItem-dense-minHeight' as any]: '44px', - ['--MenuItem-dense-block-pad' as any]: '8px', - ['--MenuItem-dense-inline-pad' as any]: '20px', + ['--MenuItem-dense-blockPad' as any]: '8px', + ['--MenuItem-dense-inlinePad' as any]: '20px', }, }, }; diff --git a/packages/mui-material/src/MenuItem/menuItemVars.ts b/packages/mui-material/src/MenuItem/menuItemVars.ts index e786afcd08883b..dd5b8e46f8b8f7 100644 --- a/packages/mui-material/src/MenuItem/menuItemVars.ts +++ b/packages/mui-material/src/MenuItem/menuItemVars.ts @@ -13,8 +13,8 @@ export const private_menuItemVars = { minHeight: '--MenuItem-minHeight', denseMinHeight: '--MenuItem-dense-minHeight', - blockPad: '--MenuItem-block-pad', - denseBlockPad: '--MenuItem-dense-block-pad', - inlinePad: '--MenuItem-inline-pad', - denseInlinePad: '--MenuItem-dense-inline-pad', + blockPad: '--MenuItem-blockPad', + denseBlockPad: '--MenuItem-dense-blockPad', + inlinePad: '--MenuItem-inlinePad', + denseInlinePad: '--MenuItem-dense-inlinePad', } as const; From b42761271f1c57e3656abc5d97a619cb138e51fe Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 16:59:47 +0700 Subject: [PATCH 050/114] density playground: interactive vars mapping for MenuItem MenuItem was preset-driven only (fields: []). Add its 6 component-var inputs (minHeight/blockPad/inlinePad + dense) like Button; each item carries the mapping as element-level sx so it overrides the preset. --- docs/pages/experiments/density-playground.tsx | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 2de4e366697b7f..9a53fa6eb5a10b 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -10,7 +10,7 @@ import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import FormControlLabel from '@mui/material/FormControlLabel'; import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; +import MenuItem, { private_menuItemVars } from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; @@ -177,20 +177,55 @@ function ButtonMatrix({ ); } -function MenuItemMatrix() { +// MenuItem's density tokens (single-value each), keyed by the `dense` axis +// rather than Button's small/medium/large. Field key === mapping-state key. +const MENUITEM_FIELDS: DensityField[] = [ + { key: 'minHeight', cssVar: private_menuItemVars.minHeight }, + { key: 'blockPad', cssVar: private_menuItemVars.blockPad }, + { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad }, + { key: 'denseMinHeight', cssVar: private_menuItemVars.denseMinHeight }, + { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad }, + { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad }, +]; + +function MenuItemMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + // Element-level tokens win over the preset's styleOverride, so set every valid + // token on each item (regular items read the plain tokens, dense read the + // `dense-*` ones — the unused set is inert). At `unset`/invalid emit none → + // falls back to the literal defaults / preset mapping. + const itemSx = mappingEnabled + ? Object.fromEntries( + MENUITEM_FIELDS.filter((f) => validateMapping(mapping[f.key] ?? '').valid).map((f) => [ + f.cssVar, + stepsToVar(mapping[f.key]), + ]), + ) + : undefined; return ( - Default item - Selected item - + Default item + + Selected item + + With icon - With divider - Dense item - + + With divider + + + Dense item + + @@ -209,13 +244,18 @@ const COMPONENT_DEFS = { renderMatrix: (args) => , }, MenuItem: { - canvasLabel: 'MenuItem (default + dense) — preset-driven', - // Preset-driven only: MenuItem reflows via enhance*Density's MuiMenuItem - // mapping; no per-element mapping inputs (its dense axis differs from - // Button's sized pads). Flip the preset to see it reflow. - fields: [], - prefill: {}, - renderMatrix: () => , + canvasLabel: 'MenuItem (default + dense)', + fields: MENUITEM_FIELDS, + // Canonical prefill matches enhanceDensity's own MuiMenuItem assignment. + prefill: { + minHeight: 'xl', + blockPad: 'xs', + inlinePad: 'lg', + denseMinHeight: 'lg', + denseBlockPad: 'xxs', + denseInlinePad: 'md', + }, + renderMatrix: (args) => , }, } satisfies Record; From 38db47ad65540f25f8ebbe4597788f01c1e0a601 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 17:13:54 +0700 Subject: [PATCH 051/114] =?UTF-8?q?density=20MenuItem:=20route=20=E2=89=A5?= =?UTF-8?q?sm=20auto-collapse=20through=20--=5FminHeight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The !dense variant hard-set min-height:auto at ≥sm, overriding the seam so --MenuItem-minHeight was never consumed on desktop. Set --_minHeight: auto instead → seam still reads the token when a preset sets it (unset → auto, as today). Zero-diff holds; non-dense min-height now reflows. --- packages/mui-material/src/MenuItem/MenuItem.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 27f66dee9af243..0fcde457d05fb8 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -165,7 +165,10 @@ const MenuItemRoot = styled(ButtonBase, { props: ({ ownerState }) => !ownerState.dense, style: { [theme.breakpoints.up('sm')]: { - minHeight: 'auto', + // The ≥sm auto-collapse rides on the internal default, not a hard + // `min-height`, so the seam still consumes `--MenuItem-minHeight` + // when a preset sets it (unset → auto, as today). + '--_minHeight': 'auto', }, }, }, From 47f6b7ad05a45d55a2a018107697e251f212e370 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 17:29:16 +0700 Subject: [PATCH 052/114] =?UTF-8?q?density=20MenuItem:=20express=20?= =?UTF-8?q?=E2=89=A5sm=20auto-collapse=20as=20seam=20fallback,=20not=20--?= =?UTF-8?q?=5FminHeight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-point --comp-minHeight: var(--MenuItem-minHeight, auto) in the !dense ≥sm variant instead of reassigning --_minHeight. Same behavior (token still wins; unset -> auto), but explicit at the override site and keeps --_minHeight the stable literal. Zero-diff holds; min-height reflows. --- packages/mui-material/src/MenuItem/MenuItem.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 0fcde457d05fb8..4ee92e9c34dc25 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -165,10 +165,9 @@ const MenuItemRoot = styled(ButtonBase, { props: ({ ownerState }) => !ownerState.dense, style: { [theme.breakpoints.up('sm')]: { - // The ≥sm auto-collapse rides on the internal default, not a hard - // `min-height`, so the seam still consumes `--MenuItem-minHeight` - // when a preset sets it (unset → auto, as today). - '--_minHeight': 'auto', + // ≥sm non-dense collapses to `auto` (as today), expressed as the + // seam's fallback so a preset's `--MenuItem-minHeight` still wins. + [`--comp-minHeight`]: `var(${menuItemVars.minHeight}, auto)`, }, }, }, From 5b01061149ced3eb6cb8d5ac7df621458b0b3e34 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 17:59:42 +0700 Subject: [PATCH 053/114] density: sizing props use raw px, density steps are spacing-only Policy: --mui-density-* steps map padding/margin/gap/inset only. width/ height use raw px per preset (the scale tops out at xxl, too small for heights). MenuItem minHeight/denseMinHeight now raw px (compact 36/28, normal 44/32, comfort 56/40); blockPad/inlinePad stay density steps. Drop minHeight from the playground step-mapping fields. Zero-diff holds. --- docs/pages/experiments/density-playground.tsx | 10 ++++------ .../mui-material/src/styles/enhanceComfortDensity.ts | 5 +++-- .../mui-material/src/styles/enhanceCompactDensity.ts | 5 +++-- .../mui-material/src/styles/enhanceNormalDensity.ts | 5 +++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 9a53fa6eb5a10b..7592e4867aedd4 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -177,13 +177,13 @@ function ButtonMatrix({ ); } -// MenuItem's density tokens (single-value each), keyed by the `dense` axis +// MenuItem's density-step tokens (spacing only), keyed by the `dense` axis // rather than Button's small/medium/large. Field key === mapping-state key. +// min-height is NOT here — heights use raw px (set per preset), not density +// steps — so it isn't interactively remappable. const MENUITEM_FIELDS: DensityField[] = [ - { key: 'minHeight', cssVar: private_menuItemVars.minHeight }, { key: 'blockPad', cssVar: private_menuItemVars.blockPad }, { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad }, - { key: 'denseMinHeight', cssVar: private_menuItemVars.denseMinHeight }, { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad }, { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad }, ]; @@ -246,12 +246,10 @@ const COMPONENT_DEFS = { MenuItem: { canvasLabel: 'MenuItem (default + dense)', fields: MENUITEM_FIELDS, - // Canonical prefill matches enhanceDensity's own MuiMenuItem assignment. + // Canonical prefill matches enhanceDensity's own MuiMenuItem spacing mapping. prefill: { - minHeight: 'xl', blockPad: 'xs', inlinePad: 'lg', - denseMinHeight: 'lg', denseBlockPad: 'xxs', denseInlinePad: 'md', }, diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index b1982e56078d36..86398300f7cddf 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -20,8 +20,9 @@ export default function enhanceComfortDensity(theme: [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); addRootOverride(enhanced.components, 'MuiMenuItem', { - [menuItemVars.minHeight]: d.xl, - [menuItemVars.denseMinHeight]: d.lg, + // Height = raw px (density steps are spacing-only). Padding = density steps. + [menuItemVars.minHeight]: '56px', + [menuItemVars.denseMinHeight]: '40px', [menuItemVars.blockPad]: d.xs, [menuItemVars.denseBlockPad]: d.xxs, [menuItemVars.inlinePad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index b198d714e422ed..7e0b072987b282 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -20,8 +20,9 @@ export default function enhanceCompactDensity(theme: [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); addRootOverride(enhanced.components, 'MuiMenuItem', { - [menuItemVars.minHeight]: d.xl, - [menuItemVars.denseMinHeight]: d.lg, + // Height = raw px (density steps are spacing-only). Padding = density steps. + [menuItemVars.minHeight]: '36px', + [menuItemVars.denseMinHeight]: '28px', [menuItemVars.blockPad]: d.xs, [menuItemVars.denseBlockPad]: d.xxs, [menuItemVars.inlinePad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 4ca17829adf72d..dcf6de8ef951b3 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -22,8 +22,9 @@ export default function enhanceNormalDensity(theme: [buttonVars.largePad]: `${d.sm} ${d.xl}`, }); addRootOverride(enhanced.components, 'MuiMenuItem', { - [menuItemVars.minHeight]: d.xl, - [menuItemVars.denseMinHeight]: d.lg, + // Height = raw px (density steps are spacing-only). Padding = density steps. + [menuItemVars.minHeight]: '44px', + [menuItemVars.denseMinHeight]: '32px', [menuItemVars.blockPad]: d.xs, [menuItemVars.denseBlockPad]: d.xxs, [menuItemVars.inlinePad]: d.lg, From e611875947fd8072db304f797a924f49819d6764 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 18:11:42 +0700 Subject: [PATCH 054/114] density: tokenize List blockPad + Menu popover demo + rename playground family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tokenize MuiList vertical padding (--List-blockPad, three-layer) — the menu's outer breathing; map ->sm in all presets (8px@normal=today). - Playground: rename family MenuItem->Menu; add --List-blockPad field so the mapping shows all Menu* spacing; add a popover Menu demo (Button opens Menu) beside the static list, both driven by the same mapping. Zero-diff holds (MenuItem gate contains a MenuList=List); list padding reflows 6/8/12; popover picks up density. --- docs/pages/experiments/density-playground.tsx | 94 +++++++++++++------ packages/mui-material/src/List/List.js | 11 ++- packages/mui-material/src/List/index.d.ts | 1 + packages/mui-material/src/List/index.js | 1 + packages/mui-material/src/List/listVars.ts | 12 +++ .../src/styles/enhanceComfortDensity.ts | 5 + .../src/styles/enhanceCompactDensity.ts | 5 + .../src/styles/enhanceNormalDensity.ts | 5 + 8 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 packages/mui-material/src/List/listVars.ts diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 7592e4867aedd4..6bee4b6080d66d 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -12,6 +12,8 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import Select from '@mui/material/Select'; import MenuItem, { private_menuItemVars } from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; +import Menu from '@mui/material/Menu'; +import { private_listVars } from '@mui/material/List'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import InboxIcon from '@mui/icons-material/Inbox'; @@ -177,38 +179,21 @@ function ButtonMatrix({ ); } -// MenuItem's density-step tokens (spacing only), keyed by the `dense` axis -// rather than Button's small/medium/large. Field key === mapping-state key. -// min-height is NOT here — heights use raw px (set per preset), not density -// steps — so it isn't interactively remappable. -const MENUITEM_FIELDS: DensityField[] = [ +// The Menu family's density-step tokens (spacing only): the List container's +// block padding + MenuItem's block/inline padding, keyed by the `dense` axis. +// Field key === mapping-state key. min-height is NOT here — heights use raw px +// (set per preset), not density steps — so it isn't interactively remappable. +const MENU_FIELDS: DensityField[] = [ + { key: 'listBlockPad', cssVar: private_listVars.blockPad }, { key: 'blockPad', cssVar: private_menuItemVars.blockPad }, { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad }, { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad }, { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad }, ]; -function MenuItemMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - // Element-level tokens win over the preset's styleOverride, so set every valid - // token on each item (regular items read the plain tokens, dense read the - // `dense-*` ones — the unused set is inert). At `unset`/invalid emit none → - // falls back to the literal defaults / preset mapping. - const itemSx = mappingEnabled - ? Object.fromEntries( - MENUITEM_FIELDS.filter((f) => validateMapping(mapping[f.key] ?? '').valid).map((f) => [ - f.cssVar, - stepsToVar(mapping[f.key]), - ]), - ) - : undefined; +function MenuDemoItems({ itemSx }: { itemSx: Record | undefined }) { return ( - + Default item Selected item @@ -231,7 +216,53 @@ function MenuItemMatrix({ Dense + icon - + + ); +} + +function MenuMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const [anchorEl, setAnchorEl] = React.useState(null); + // Element-level tokens win over the preset's styleOverride. `--List-blockPad` + // goes on the list root; `--MenuItem-*` on each item (regular reads plain, + // dense reads `dense-*` — unused set is inert). At `unset`/invalid emit none. + const valid = (key: string) => validateMapping(mapping[key] ?? '').valid; + const listSx = + mappingEnabled && valid('listBlockPad') + ? { [private_listVars.blockPad]: stepsToVar(mapping.listBlockPad) } + : undefined; + const itemSx = mappingEnabled + ? Object.fromEntries( + MENU_FIELDS.filter((f) => f.key !== 'listBlockPad' && valid(f.key)).map((f) => [ + f.cssVar, + stepsToVar(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + + +
+ + setAnchorEl(null)} + slotProps={{ list: { sx: listSx } }} + > + + +
+
); } @@ -243,17 +274,18 @@ const COMPONENT_DEFS = { prefill: { smallPad: 'xxs sm', mediumPad: 'xs lg', largePad: 'sm xl' }, renderMatrix: (args) => , }, - MenuItem: { - canvasLabel: 'MenuItem (default + dense)', - fields: MENUITEM_FIELDS, - // Canonical prefill matches enhanceDensity's own MuiMenuItem spacing mapping. + Menu: { + canvasLabel: 'Menu — static list + popover (default + dense)', + fields: MENU_FIELDS, + // Canonical prefill matches enhanceDensity's own MuiList/MuiMenuItem mapping. prefill: { + listBlockPad: 'sm', blockPad: 'xs', inlinePad: 'lg', denseBlockPad: 'xxs', denseInlinePad: 'md', }, - renderMatrix: (args) => , + renderMatrix: (args) => , }, } satisfies Record; diff --git a/packages/mui-material/src/List/List.js b/packages/mui-material/src/List/List.js index 3e885b9208be28..a24f14a5715ed2 100644 --- a/packages/mui-material/src/List/List.js +++ b/packages/mui-material/src/List/List.js @@ -7,6 +7,7 @@ import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import ListContext from './ListContext'; import { getListUtilityClass } from './listClasses'; +import { private_listVars as listVars } from './listVars'; const useUtilityClasses = (ownerState) => { const { classes, disablePadding, dense, subheader } = ownerState; @@ -40,8 +41,14 @@ const ListRoot = styled('ul', { { props: ({ ownerState }) => !ownerState.disablePadding, style: { - paddingTop: 8, - paddingBottom: 8, + // Density seam: `--List-blockPad` (spacing token) over the literal + // default via the agnostic `--comp-listBlockPad`. Same map read by the + // `enhance*Density` presets. The `subheader` variant below still forces + // `paddingTop: 0` (unchanged). + '--_listBlockPad': '8px', + [`--comp-listBlockPad`]: `var(${listVars.blockPad}, var(--_listBlockPad))`, + paddingTop: 'var(--comp-listBlockPad)', + paddingBottom: 'var(--comp-listBlockPad)', }, }, { diff --git a/packages/mui-material/src/List/index.d.ts b/packages/mui-material/src/List/index.d.ts index 6d6f8d6cfa0d68..d72bb5f25e1b3a 100644 --- a/packages/mui-material/src/List/index.d.ts +++ b/packages/mui-material/src/List/index.d.ts @@ -3,3 +3,4 @@ export * from './List'; export { default as listClasses } from './listClasses'; export * from './listClasses'; +export { private_listVars } from './listVars'; diff --git a/packages/mui-material/src/List/index.js b/packages/mui-material/src/List/index.js index 6435a92475c14f..4c271eb471bc27 100644 --- a/packages/mui-material/src/List/index.js +++ b/packages/mui-material/src/List/index.js @@ -2,3 +2,4 @@ export { default } from './List'; export { default as listClasses } from './listClasses'; export * from './listClasses'; +export { private_listVars } from './listVars'; diff --git a/packages/mui-material/src/List/listVars.ts b/packages/mui-material/src/List/listVars.ts new file mode 100644 index 00000000000000..6aa9e83932f487 --- /dev/null +++ b/packages/mui-material/src/List/listVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * List density token identities (`private_*` per the density RFC). Static, + * unprefixed literals imported by both the styled component AND the + * `enhance*Density` presets, so emitted and targeted names can't drift. + * + * `blockPad` is the list's vertical breathing (`padding-block`, today `8px` when + * not `disablePadding`). It's what gives a `Menu`/`MenuList` its top/bottom gap. + */ +export const private_listVars = { + blockPad: '--List-blockPad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 86398300f7cddf..a7f6b36d9b1bb3 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -1,6 +1,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; +import { private_listVars as listVars } from '../List/listVars'; const scale: DensityScale = { xxs: '6px', @@ -28,6 +29,10 @@ export default function enhanceComfortDensity(theme: [menuItemVars.inlinePad]: d.lg, [menuItemVars.denseInlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiList', { + // Menu/list vertical breathing (spacing token). + [listVars.blockPad]: d.sm, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 7e0b072987b282..cb1d7a69873b97 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -1,6 +1,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; +import { private_listVars as listVars } from '../List/listVars'; const scale: DensityScale = { xxs: '2px', @@ -28,6 +29,10 @@ export default function enhanceCompactDensity(theme: [menuItemVars.inlinePad]: d.lg, [menuItemVars.denseInlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiList', { + // Menu/list vertical breathing (spacing token). + [listVars.blockPad]: d.sm, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index dcf6de8ef951b3..6a37c56ae87a78 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -1,6 +1,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, EnhanceableTheme } from './densityScale'; import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; +import { private_listVars as listVars } from '../List/listVars'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button // typography — no reflow — so only the padding→step assignment below. @@ -30,5 +31,9 @@ export default function enhanceNormalDensity(theme: [menuItemVars.inlinePad]: d.lg, [menuItemVars.denseInlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiList', { + // Menu/list vertical breathing (spacing token). + [listVars.blockPad]: d.sm, + }); return enhanced; } From bc159c03d711ee0b21fd8d075f2014f5b077211d Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 18:34:58 +0700 Subject: [PATCH 055/114] density: rule-4 consume shape + CSS-value mapping input + MenuItem text-box - MenuItem/List consume via var(--comp-, var(--_)) (RFC rule 4; keeps the closed loop closed even if a seam is unset). - Playground mapping input accepts ANY valid CSS value; density key = sugar for var(--mui-density-*). Restore minHeight/denseMinHeight rows (raw px, empty=inert so preset drives). Stop rejecting raw px as 'not a density key'. - Wrap MenuItem labels in .density-debug-text so the text-box overlay works. Zero-diff MenuItem+Button 3/3. --- docs/pages/experiments/density-playground.tsx | 108 ++++++++++-------- packages/mui-material/src/List/List.js | 10 +- .../mui-material/src/MenuItem/MenuItem.js | 10 +- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 6bee4b6080d66d..1c704f55443ea6 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -81,30 +81,37 @@ const PRESET_LABEL: Record = { const buttonVar = (size: Size) => private_buttonVars[`${size}Pad` as MappingKey]; -// Keys-only → density-var string. The validator guarantees each token ∈ SCALE_KEYS. -// 1 step → applies to all sides; 2 steps → `block inline`. -const stepsToVar = (input: string) => - input - .trim() - .split(/\s+/) - .map((t) => `var(--mui-density-${t})`) +const isDensityKey = (t: string) => (SCALE_KEYS as readonly string[]).includes(t); +const tokenize = (input: string) => input.trim().split(/\s+/).filter(Boolean); + +// A mapping input is ANY valid CSS value. A density key (`xxs`…`xxl`) is sugar +// for `var(--mui-density-)`; anything else passes through verbatim as raw +// CSS (`12px`, `2rem`, `auto`). 1 token → all sides; 2 → `block inline`. +const resolveValue = (input: string) => + tokenize(input) + .map((t) => (isDensityKey(t) ? `var(--mui-density-${t})` : t)) .join(' '); -function validateMapping(input: string): { valid: boolean; error: string | null } { - const tokens = input.trim().split(/\s+/).filter(Boolean); +// Empty = inert (no override, no error). >2 tokens = error. Otherwise ok — raw +// values are first-class, never rejected as "not a density key". +function parseMapping(input: string): { state: 'empty' | 'ok' | 'error'; error?: string } { + const tokens = tokenize(input); if (tokens.length === 0) { - return { valid: false, error: 'enter 1–2 density steps' }; + return { state: 'empty' }; } if (tokens.length > 2) { - return { valid: false, error: 'max 2 steps (block inline)' }; - } - const bad = tokens.find((t) => !(SCALE_KEYS as readonly string[]).includes(t)); - if (bad) { - return { valid: false, error: `"${bad}" is not a density key` }; + return { state: 'error', error: 'max 2 values (block inline)' }; } - return { valid: true, error: null }; + return { state: 'ok' }; } +// Human-readable resolved value: keys show their px (from the active scale), +// raw values echo as typed. +const previewText = (input: string, scalePx: Record | null) => + tokenize(input) + .map((t) => (isDensityKey(t) ? (scalePx?.[t] ?? t) : t)) + .join(' '); + // Each preset maps to its `enhance*Density` fn; `unset` applies none. const PRESET_FN = { compact: enhanceCompactDensity, @@ -112,13 +119,6 @@ const PRESET_FN = { comfort: enhanceComfortDensity, } as const; -// Resolved var string + px for a valid mapping value under the active scale — -// e.g. `md` → { varStr: 'var(--mui-density-md)', px: '8px' } (compact). -function resolvePreview(value: string, scalePx: Record | null) { - const tokens = value.trim().split(/\s+/).filter(Boolean); - return { varStr: stepsToVar(value), px: scalePx ? tokens.map((t) => scalePx[t]).join(' ') : '' }; -} - // --------------------------------------------------------------------------- // Density-component registry. Only Button is de-prefixed/wired in this // prototype; add entries here as more families gain a static `private_*Vars` @@ -146,11 +146,14 @@ function ButtonMatrix({ {SIZES.map((size) => { const key = `${size}Pad`; - const { valid } = validateMapping(mapping[key] ?? ''); + const value = mapping[key] ?? ''; // TO5/TO6: element-level token wins over the preset's styleOverride. - // At `unset` (or invalid input) emit NO token → falls back to the literal + // At `unset`/empty/invalid emit NO token → falls back to the literal // `--_pad` default (unset) or the preset's own mapping. - const sx = mappingEnabled && valid ? { [buttonVar(size)]: stepsToVar(mapping[key]) } : undefined; + const sx = + mappingEnabled && parseMapping(value).state === 'ok' + ? { [buttonVar(size)]: resolveValue(value) } + : undefined; return ( @@ -179,42 +182,50 @@ function ButtonMatrix({ ); } -// The Menu family's density-step tokens (spacing only): the List container's -// block padding + MenuItem's block/inline padding, keyed by the `dense` axis. -// Field key === mapping-state key. min-height is NOT here — heights use raw px -// (set per preset), not density steps — so it isn't interactively remappable. +// The Menu family's density tokens: List container block padding + MenuItem +// block/inline padding + min-height, keyed by the `dense` axis. Field key === +// mapping-state key. Sizing tokens (`minHeight`) accept raw px like any other — +// a density key is just sugar; heights ship as raw px per preset. const MENU_FIELDS: DensityField[] = [ { key: 'listBlockPad', cssVar: private_listVars.blockPad }, { key: 'blockPad', cssVar: private_menuItemVars.blockPad }, { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad }, + { key: 'minHeight', cssVar: private_menuItemVars.minHeight }, { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad }, { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad }, + { key: 'denseMinHeight', cssVar: private_menuItemVars.denseMinHeight }, ]; function MenuDemoItems({ itemSx }: { itemSx: Record | undefined }) { return ( - Default item + + Default item + - Selected item + Selected item - With icon + + With icon + - With divider + With divider - Dense item + Dense item - Dense + icon + + Dense + icon + ); @@ -230,17 +241,18 @@ function MenuMatrix({ const [anchorEl, setAnchorEl] = React.useState(null); // Element-level tokens win over the preset's styleOverride. `--List-blockPad` // goes on the list root; `--MenuItem-*` on each item (regular reads plain, - // dense reads `dense-*` — unused set is inert). At `unset`/invalid emit none. - const valid = (key: string) => validateMapping(mapping[key] ?? '').valid; + // dense reads `dense-*` — unused set is inert). Empty/invalid → emit none, so + // the preset's own value shows through (e.g. blank min-height keeps the preset px). + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; const listSx = - mappingEnabled && valid('listBlockPad') - ? { [private_listVars.blockPad]: stepsToVar(mapping.listBlockPad) } + mappingEnabled && active('listBlockPad') + ? { [private_listVars.blockPad]: resolveValue(mapping.listBlockPad) } : undefined; const itemSx = mappingEnabled ? Object.fromEntries( - MENU_FIELDS.filter((f) => f.key !== 'listBlockPad' && valid(f.key)).map((f) => [ + MENU_FIELDS.filter((f) => f.key !== 'listBlockPad' && active(f.key)).map((f) => [ f.cssVar, - stepsToVar(mapping[f.key]), + resolveValue(mapping[f.key]), ]), ) : undefined; @@ -423,14 +435,13 @@ export default function DensityExperiment() { {COMPONENT_DEFS[comp].fields.map((field) => { const value = mapping[comp][field.key] ?? ''; - const { valid, error } = validateMapping(value); - const showError = mappingEnabled && !valid; - const preview = resolvePreview(value, scalePx); + const parsed = parseMapping(value); + const showError = mappingEnabled && parsed.state === 'error'; let helper = ' '; if (showError) { - helper = error ?? ' '; - } else if (mappingEnabled && valid) { - helper = preview.px; + helper = parsed.error ?? ' '; + } else if (mappingEnabled && parsed.state === 'ok') { + helper = previewText(value, scalePx); // key → px · raw → as typed } return ( !ownerState.disablePadding, style: { // Density seam: `--List-blockPad` (spacing token) over the literal - // default via the agnostic `--comp-listBlockPad`. Same map read by the + // default via the agnostic `--comp-blockPad`. Same map read by the // `enhance*Density` presets. The `subheader` variant below still forces // `paddingTop: 0` (unchanged). - '--_listBlockPad': '8px', - [`--comp-listBlockPad`]: `var(${listVars.blockPad}, var(--_listBlockPad))`, - paddingTop: 'var(--comp-listBlockPad)', - paddingBottom: 'var(--comp-listBlockPad)', + '--_blockPad': '8px', + [`--comp-blockPad`]: `var(${listVars.blockPad}, var(--_blockPad))`, + paddingTop: 'var(--comp-blockPad, var(--_blockPad))', + paddingBottom: 'var(--comp-blockPad, var(--_blockPad))', }, }, { diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js index 4ee92e9c34dc25..998b8598289da3 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.js +++ b/packages/mui-material/src/MenuItem/MenuItem.js @@ -77,9 +77,9 @@ const MenuItemRoot = styled(ButtonBase, { alignItems: 'center', position: 'relative', textDecoration: 'none', - minHeight: 'var(--comp-minHeight)', - paddingTop: 'var(--comp-blockPad)', - paddingBottom: 'var(--comp-blockPad)', + minHeight: 'var(--comp-minHeight, var(--_minHeight))', + paddingTop: 'var(--comp-blockPad, var(--_blockPad))', + paddingBottom: 'var(--comp-blockPad, var(--_blockPad))', boxSizing: 'border-box', whiteSpace: 'nowrap', '&:hover': { @@ -144,8 +144,8 @@ const MenuItemRoot = styled(ButtonBase, { style: { '--_inlinePad': '16px', [`--comp-inlinePad`]: `var(${menuItemVars.inlinePad}, var(--_inlinePad))`, - paddingLeft: 'var(--comp-inlinePad)', - paddingRight: 'var(--comp-inlinePad)', + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', }, }, { From 8f860983d10f30fe66a702546d62dc4428057b2f Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 18:43:09 +0700 Subject: [PATCH 056/114] density playground: sync sizing field defaults with the active preset Read raw-px sizing tokens (minHeight/denseMinHeight) live off the enhanced theme's MuiMenuItem override instead of leaving them empty; re-sync the mapping when the preset changes. Single source of truth = enhanceDensity, no drift. Spacing fields keep their density-key prefill. --- docs/pages/experiments/density-playground.tsx | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 1c704f55443ea6..1a4a1072218f2e 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -306,19 +306,46 @@ type Selection = 'All' | ComponentName; const COMPONENTS = Object.keys(COMPONENT_DEFS) as ComponentName[]; -const initialMapping = () => - Object.fromEntries(COMPONENTS.map((c) => [c, { ...COMPONENT_DEFS[c].prefill }])) as unknown as Record< - ComponentName, - Record - >; +// Read the value a preset assigned to a token, straight off the enhanced theme's +// component overrides (the same `addRootOverride` output). Single source of truth +// for raw-px sizing defaults — can't drift from what `enhance*Density` ships. +function themeTokenValue(theme: unknown, cssVar: string): string | undefined { + const components = (theme as { components?: Record })?.components ?? {}; + for (const name of Object.keys(components)) { + const root = components[name]?.styleOverrides?.root; + const layers = Array.isArray(root) ? root : [root]; + for (let i = layers.length - 1; i >= 0; i -= 1) { + const layer = layers[i]; + if (layer && typeof layer === 'object' && cssVar in layer) { + return layer[cssVar] as string; + } + } + } + return undefined; +} + +// Canonical mapping for the active preset: spacing tokens prefill with their +// density key (preset-independent), sizing tokens with the preset's raw px read +// live off the theme. Empty when the preset didn't set a token (e.g. `unset`). +const buildMapping = (theme: unknown) => + Object.fromEntries( + COMPONENTS.map((c) => [ + c, + Object.fromEntries( + COMPONENT_DEFS[c].fields.map((field) => [ + field.key, + (COMPONENT_DEFS[c].prefill as Record)[field.key] ?? + themeTokenValue(theme, field.cssVar) ?? + '', + ]), + ), + ]), + ) as Record>; export default function DensityExperiment() { const [preset, setPreset] = React.useState('unset'); const [selection, setSelection] = React.useState('All'); const [debug, setDebug] = React.useState([]); - const [mapping, setMapping] = React.useState>>( - initialMapping, - ); const mappingEnabled = preset !== 'unset'; const visibleComponents: ComponentName[] = selection === 'All' ? COMPONENTS : [selection]; @@ -328,6 +355,17 @@ export default function DensityExperiment() { return preset === 'unset' ? base : PRESET_FN[preset](base); }, [preset]); + const [mapping, setMapping] = React.useState>>(() => + buildMapping(canvasTheme), + ); + + // Re-sync the mapping to the active preset's canonical values (incl. its raw-px + // sizing) whenever the preset changes, so fields default to what enhanceDensity + // ships rather than going stale/empty. + React.useEffect(() => { + setMapping(buildMapping(canvasTheme)); + }, [canvasTheme]); + // Active scale in px straight off the enhanced theme — single source of truth // for the legend + preview, so it can't drift from what the preset applied. const scalePx = @@ -338,7 +376,7 @@ export default function DensityExperiment() { const setField = (comp: ComponentName, key: string, value: string) => setMapping((m) => ({ ...m, [comp]: { ...m[comp], [key]: value } })); - const resetMapping = () => setMapping(initialMapping()); + const resetMapping = () => setMapping(buildMapping(canvasTheme)); return ( From c740e0afb67877decf2b77471b191dad590eb7f3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 18:55:11 +0700 Subject: [PATCH 057/114] density playground: top control bar + scrollable panels + compact header Fixed-viewport shell. Move enhanceDensity preset (horizontal radios) and Visual debug toggles into a full-width top bar (preset left, debug right). Sidebar = fixed Component selector + independently scrollable Vars mapping. Canvas scrolls independently. Title+subtitle shrunk to one compact row. --- docs/pages/experiments/density-playground.tsx | 193 ++++++++++-------- 1 file changed, 106 insertions(+), 87 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 1a4a1072218f2e..61fcdba9b74f5a 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -379,54 +379,106 @@ export default function DensityExperiment() { const resetMapping = () => setMapping(buildMapping(canvasTheme)); return ( - - - - - Density — experiment + + + + {/* Title row — compact, single line. */} + + + Density — playground - + Flip the preset · pick a component · remap its tokens to density steps - - {/* CONTROLS — outside the themed scope, so they don't pick up density. */} + {/* Control bar — full width: preset (left) · visual debug (right). */} + + + + enhanceDensity + + setPreset(event.target.value as Preset)} + > + {PRESETS.map((p) => ( + } + label={PRESET_LABEL[p]} + slotProps={{ typography: { variant: 'body2' } }} + /> + ))} + + + + + + Visual debug + + setDebug(next)} + aria-label="visual debug overlays" + > + + + + + + + + + + + + + + + {/* Content — sidebar (fixed Component + scrollable mapping) · scrollable canvas. */} + - - enhanceDensity preset - setPreset(event.target.value as Preset)} - > - {PRESETS.map((p) => ( - } - label={PRESET_LABEL[p]} - /> - ))} - - - - + Component @@ -445,7 +497,10 @@ export default function DensityExperiment() { - + Vars mapping @@ -515,63 +570,27 @@ export default function DensityExperiment() { - {/* RIGHT COLUMN — debug toolbar (plain theme) + themed canvas. */} - + {/* CANVAS — density-enhanced theme; scrolls independently. */} + + - - Visual debug - - setDebug(next)} - aria-label="visual debug overlays" - > - - - - - - - - - - - + + {visibleComponents.map((comp) => ( + + + {COMPONENT_DEFS[comp].canvasLabel} + + {COMPONENT_DEFS[comp].renderMatrix({ mapping: mapping[comp], mappingEnabled })} + + ))} + - - {/* CANVAS — wrapped in the density-enhanced theme. */} - - - - - {visibleComponents.map((comp) => ( - - - {COMPONENT_DEFS[comp].canvasLabel} - - {COMPONENT_DEFS[comp].renderMatrix({ mapping: mapping[comp], mappingEnabled })} - - ))} - - - - +
); From cbf129a63b3b47fa81bd20e5bd761bdca2c3e4c2 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 19:00:30 +0700 Subject: [PATCH 058/114] density playground: preset labels to plain words (none/compact/normal/comfort) Display-only; 'unset' key unchanged. --- docs/pages/experiments/density-playground.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 61fcdba9b74f5a..825d60a4c8bdbe 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -73,9 +73,9 @@ const DEBUG_SX = { } as const; const PRESET_LABEL: Record = { - unset: 'unset — no fn · today · 0-diff', + unset: 'none', compact: 'compact', - normal: 'normal — default density scale', + normal: 'normal', comfort: 'comfort', }; From c5d5cf920d52fd7debaf40c8856a2009f5636cb3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 21:58:58 +0700 Subject: [PATCH 059/114] density Tooltip: tokenize bubble (blockPad/inlinePad/offset/arrowSize) + wire 3 presets - 3-layer seams on `tooltip` slot (no root slot); arrow width/protrusion/insets derive from one --comp-arrowSize, 0.71 literal kept - tooltipVars.ts camelCase, re-export index.js/.d.ts - presets: spacing->steps (blockPad xxs, inlinePad sm, offset lg), arrowSize->raw px (sizing, spacing-only policy) - addRootOverride gains optional slot param (default root); themeTokenValue scans all slots - playground Tooltip entry + padding-ring/text-box debug on .MuiTooltip-tooltip; fixture demo - zero-diff maxDiffPixels:0 vs baseline; reflow verified via getComputedStyle; Tooltip.test 182 pass (RTL offset assertion tokenized) --- docs/pages/experiments/density-fixture.tsx | 14 ++ docs/pages/experiments/density-playground.tsx | 123 ++++++++++++++---- docs/src/modules/components/densityDemos.tsx | 26 ++++ packages/mui-material/src/Tooltip/Tooltip.js | 54 +++++--- .../mui-material/src/Tooltip/Tooltip.test.js | 5 +- packages/mui-material/src/Tooltip/index.d.ts | 2 + packages/mui-material/src/Tooltip/index.js | 2 + .../mui-material/src/Tooltip/tooltipVars.ts | 16 +++ .../mui-material/src/styles/densityScale.ts | 19 ++- .../src/styles/enhanceComfortDensity.ts | 14 ++ .../src/styles/enhanceCompactDensity.ts | 14 ++ .../src/styles/enhanceNormalDensity.ts | 14 ++ 12 files changed, 256 insertions(+), 47 deletions(-) create mode 100644 packages/mui-material/src/Tooltip/tooltipVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 6119d4266fef1e..2c007a6fbc5770 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -46,6 +46,20 @@ const scopes: Record> = { ['--MenuItem-dense-inlinePad' as any]: '20px', }, }, + Tooltip: { + dense: { + ['--Tooltip-blockPad' as any]: '2px', + ['--Tooltip-inlinePad' as any]: '6px', + ['--Tooltip-offset' as any]: '10px', + ['--Tooltip-arrowSize' as any]: '9px', + }, + loose: { + ['--Tooltip-blockPad' as any]: '8px', + ['--Tooltip-inlinePad' as any]: '16px', + ['--Tooltip-offset' as any]: '20px', + ['--Tooltip-arrowSize' as any]: '16px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 825d60a4c8bdbe..efb12a1f64b961 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -22,7 +22,7 @@ import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Tooltip from '@mui/material/Tooltip'; +import Tooltip, { private_tooltipVars } from '@mui/material/Tooltip'; import PaddingIcon from '@mui/icons-material/Padding'; import TitleIcon from '@mui/icons-material/Title'; import { @@ -47,26 +47,32 @@ type MappingKey = `${Size}Pad`; // layout-safe (absolute ::before + pointer-events:none), never touches the // components' real styles. The label span sits above the padding overlay // (z-index) so text stays crisp; its blue fill only shows in text mode. +// The padding-ring overlay: `inset:0` sizes it to the element's padding-box; +// `padding:inherit` shrinks its content-box to the element's content box, and +// the `exclude` mask knocks that center out → green fills only the padding ring. +const PADDING_RING = { + content: '""', + position: 'absolute', + inset: 0, + padding: 'inherit', + boxSizing: 'border-box', + borderRadius: 'inherit', + backgroundColor: 'rgba(46, 204, 64, 0.5)', // padding = green (DevTools convention) + pointerEvents: 'none', + WebkitMask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', + WebkitMaskComposite: 'xor', + mask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', + maskComposite: 'exclude', +} as const; + const DEBUG_SX = { '& .density-debug-text': { position: 'relative', zIndex: 1, borderRadius: '2px' }, - '&[data-debug-padding] .MuiButtonBase-root': { position: 'relative' }, - '&[data-debug-padding] .MuiButtonBase-root::before': { - content: '""', - position: 'absolute', - inset: 0, - // `inset:0` sizes the overlay to the button's padding-box; `padding:inherit` - // then shrinks its content-box to the button's content box, and the - // `exclude` mask knocks that center out → green fills only the padding ring. - padding: 'inherit', - boxSizing: 'border-box', - borderRadius: 'inherit', - backgroundColor: 'rgba(46, 204, 64, 0.5)', // padding = green (DevTools convention) - pointerEvents: 'none', - WebkitMask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', - WebkitMaskComposite: 'xor', - mask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)', - maskComposite: 'exclude', + // Padding ring on ButtonBase (Button/MenuItem) + the Tooltip bubble. + '&[data-debug-padding] .MuiButtonBase-root, &[data-debug-padding] .MuiTooltip-tooltip': { + position: 'relative', }, + '&[data-debug-padding] .MuiButtonBase-root::before': PADDING_RING, + '&[data-debug-padding] .MuiTooltip-tooltip::before': PADDING_RING, '&[data-debug-text] .density-debug-text': { backgroundColor: 'rgba(0, 116, 217, 0.32)', // text box = blue }, @@ -278,6 +284,60 @@ function MenuMatrix({ ); } +// Tooltip density tokens (regular/pointer only — `touch` is out of scope). +// Padding + anchor offset are spacing (prefill density keys); arrow size ships +// as raw px per preset (read live off the theme), like MenuItem min-height. +const TOOLTIP_FIELDS: DensityField[] = [ + { key: 'blockPad', cssVar: private_tooltipVars.blockPad }, + { key: 'inlinePad', cssVar: private_tooltipVars.inlinePad }, + { key: 'offset', cssVar: private_tooltipVars.offset }, + { key: 'arrowSize', cssVar: private_tooltipVars.arrowSize }, +]; + +function TooltipMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + // Element-level tokens win over the preset. All four land on the bubble + // (`tooltip` slot); the arrow inherits `--comp-arrowSize` from it. + const tooltipSx = mappingEnabled + ? Object.fromEntries( + TOOLTIP_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + // Force open + inline (no portal) so the bubble sits inside the debug scope + // and picks up the padding-ring / text-box overlays. + const slotProps = { + popper: { disablePortal: true }, + tooltip: { sx: tooltipSx }, + } as const; + return ( + + Default tooltip} + open + placement="bottom" + slotProps={slotProps} + > + + + Arrow tooltip} + arrow + open + placement="bottom" + slotProps={slotProps} + > + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -299,6 +359,17 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + Tooltip: { + canvasLabel: 'Tooltip — pointer (default + arrow); touch out of scope', + fields: TOOLTIP_FIELDS, + // Spacing tokens prefill density keys; arrow size (raw px) reads off the theme. + prefill: { + blockPad: 'xxs', + inlinePad: 'sm', + offset: 'lg', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; @@ -312,12 +383,16 @@ const COMPONENTS = Object.keys(COMPONENT_DEFS) as ComponentName[]; function themeTokenValue(theme: unknown, cssVar: string): string | undefined { const components = (theme as { components?: Record })?.components ?? {}; for (const name of Object.keys(components)) { - const root = components[name]?.styleOverrides?.root; - const layers = Array.isArray(root) ? root : [root]; - for (let i = layers.length - 1; i >= 0; i -= 1) { - const layer = layers[i]; - if (layer && typeof layer === 'object' && cssVar in layer) { - return layer[cssVar] as string; + // Scan every slot's overrides, not just `root` — Tooltip's tokens land on + // the `tooltip` slot (it has no root slot). + const styleOverrides = components[name]?.styleOverrides ?? {}; + for (const slot of Object.keys(styleOverrides)) { + const layers = Array.isArray(styleOverrides[slot]) ? styleOverrides[slot] : [styleOverrides[slot]]; + for (let i = layers.length - 1; i >= 0; i -= 1) { + const layer = layers[i]; + if (layer && typeof layer === 'object' && cssVar in layer) { + return layer[cssVar] as string; + } } } } diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index e8ab37dfa1dbad..d326200a4d5861 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -1,12 +1,22 @@ import * as React from 'react'; +import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; +import Tooltip from '@mui/material/Tooltip'; import InboxIcon from '@mui/icons-material/Inbox'; +// Force the tooltip open + inline (no portal) so it renders inside +// `#density-scope` and lands in the scoped screenshot; snap the transition so +// the capture is deterministic. +const staticTooltipSlotProps = { + popper: { disablePortal: true }, + transition: { appear: false, timeout: 0 }, +} as const; + // Shared density demo matrix for the CSS-var density adapter. // Consumed by the screenshot fixture (density-fixture). `level=default` (no token // overrides) must stay pixel-identical to the pre-change baseline. @@ -53,6 +63,22 @@ const demos: Record = { ), + Tooltip: ( + // Always-open, inline tooltips (no hover/portal) so they render inside the + // scoped screenshot. The box is left unpositioned on purpose — Popper then + // anchors against the viewport (correct coords); the fixed height just gives + // the scope a tall enough capture box for the bottom-placed bubbles. + + + + + + + + + + + ), }; export default demos; diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js index f0ffb0c969f66a..cb44818e9863e4 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.js +++ b/packages/mui-material/src/Tooltip/Tooltip.js @@ -19,6 +19,7 @@ import useId from '../utils/useId'; import useControlled from '../utils/useControlled'; import useSlot from '../utils/useSlot'; import tooltipClasses, { getTooltipUtilityClass } from './tooltipClasses'; +import { private_tooltipVars as tooltipVars } from './tooltipVars'; function round(value) { return Math.round(value * 1e5) / 1e5; @@ -68,34 +69,37 @@ const TooltipPopper = styled(Popper, { { props: ({ ownerState }) => ownerState.arrow, style: { + // Arrow protrusion/insets derive from the single `--comp-arrowSize` + // (default `1em`) so the arrow + its negative seam can't drift; keep + // the literal `0.71` (= 1/√2, hardcoded upstream) for zero-diff. [`&[data-popper-placement*="bottom"] .${tooltipClasses.arrow}`]: { top: 0, - marginTop: '-0.71em', + marginTop: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * -0.71)', '&::before': { transformOrigin: '0 100%', }, }, [`&[data-popper-placement*="top"] .${tooltipClasses.arrow}`]: { bottom: 0, - marginBottom: '-0.71em', + marginBottom: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * -0.71)', '&::before': { transformOrigin: '100% 0', }, }, [`&[data-popper-placement*="right"] .${tooltipClasses.arrow}`]: { - height: '1em', - width: '0.71em', + height: 'var(--comp-arrowSize, var(--_arrowSize))', + width: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * 0.71)', insetInlineStart: 0, - marginInlineStart: '-0.71em', + marginInlineStart: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * -0.71)', '&::before': { transformOrigin: '100% 100%', }, }, [`&[data-popper-placement*="left"] .${tooltipClasses.arrow}`]: { - height: '1em', - width: '0.71em', + height: 'var(--comp-arrowSize, var(--_arrowSize))', + width: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * 0.71)', insetInlineEnd: 0, - marginInlineEnd: '-0.71em', + marginInlineEnd: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * -0.71)', '&::before': { transformOrigin: '0 0', }, @@ -121,13 +125,31 @@ const TooltipTooltip = styled('div', { }, })( memoTheme(({ theme }) => ({ + // Density seams for the regular (pointer) tooltip: spacing tokens + // `--Tooltip-blockPad`/`-inlinePad` (padding) + `--Tooltip-offset` (per- + // placement anchor margin) over their literal defaults via the agnostic + // `--comp-*`. `--_arrowSize` (the arrow's base dim) also lives here so it + // cascades to the arrow child. The same map is read by the `enhance*Density` + // presets. The `touch` variant below keeps its `8/16` pad + `24` offset + // literals (out of density scope — accessibility, not comfort). + '--_blockPad': '4px', + '--_inlinePad': '8px', + '--_offset': '14px', + '--_arrowSize': '1em', + [`--comp-blockPad`]: `var(${tooltipVars.blockPad}, var(--_blockPad))`, + [`--comp-inlinePad`]: `var(${tooltipVars.inlinePad}, var(--_inlinePad))`, + [`--comp-offset`]: `var(${tooltipVars.offset}, var(--_offset))`, + [`--comp-arrowSize`]: `var(${tooltipVars.arrowSize}, var(--_arrowSize))`, backgroundColor: theme.vars ? theme.vars.palette.Tooltip.bg : theme.alpha(theme.palette.grey[700], 0.92), borderRadius: (theme.vars || theme).shape.borderRadius, color: (theme.vars || theme).palette.common.white, fontFamily: theme.typography.fontFamily, - padding: '4px 8px', + paddingTop: 'var(--comp-blockPad, var(--_blockPad))', + paddingBottom: 'var(--comp-blockPad, var(--_blockPad))', + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', fontSize: theme.typography.pxToRem(11), maxWidth: 300, margin: 2, @@ -135,19 +157,19 @@ const TooltipTooltip = styled('div', { fontWeight: theme.typography.fontWeightMedium, [`.${tooltipClasses.popper}[data-popper-placement*="left"] &`]: { transformOrigin: 'right center', - marginInlineEnd: '14px', + marginInlineEnd: 'var(--comp-offset, var(--_offset))', }, [`.${tooltipClasses.popper}[data-popper-placement*="right"] &`]: { transformOrigin: 'left center', - marginInlineStart: '14px', + marginInlineStart: 'var(--comp-offset, var(--_offset))', }, [`.${tooltipClasses.popper}[data-popper-placement*="top"] &`]: { transformOrigin: 'center bottom', - marginBottom: '14px', + marginBottom: 'var(--comp-offset, var(--_offset))', }, [`.${tooltipClasses.popper}[data-popper-placement*="bottom"] &`]: { transformOrigin: 'center top', - marginTop: '14px', + marginTop: 'var(--comp-offset, var(--_offset))', }, variants: [ { @@ -194,8 +216,10 @@ const TooltipArrow = styled('span', { memoTheme(({ theme }) => ({ overflow: 'hidden', position: 'absolute', - width: '1em', - height: '0.71em' /* = width / sqrt(2) = (length of the hypotenuse) */, + // Long dim = `--comp-arrowSize` (inherited from the bubble); the hypotenuse + // protrusion = same var × 0.71 (kept literal for zero-diff, = 1/√2). + width: 'var(--comp-arrowSize, var(--_arrowSize))', + height: 'calc(var(--comp-arrowSize, var(--_arrowSize)) * 0.71)' /* = width / sqrt(2) */, boxSizing: 'border-box', color: theme.vars ? theme.vars.palette.Tooltip.bg : theme.alpha(theme.palette.grey[700], 0.9), '&::before': { diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js index 3106a6578c43fb..2581ad64eb43df 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.test.js +++ b/packages/mui-material/src/Tooltip/Tooltip.test.js @@ -79,7 +79,10 @@ function expectRtlRightPlacementStyles() { expect(popper).to.have.attribute('data-popper-placement', 'right'); expect(tooltip).toHaveComputedStyle({ direction: 'rtl' }); - expect(hasInjectedStyle('margin-inline-start: 14px')).to.equal(true); + // Anchor offset is tokenized: the seam resolves to 14px via `--_offset`. + expect(hasInjectedStyle('margin-inline-start: var(--comp-offset, var(--_offset))')).to.equal( + true, + ); expect(hasInjectedStyle('inset-inline-start: 0')).to.equal(true); expectArrowOnInlineEnd(tooltip, arrow); } diff --git a/packages/mui-material/src/Tooltip/index.d.ts b/packages/mui-material/src/Tooltip/index.d.ts index c44b819bdc9bff..213661aefa64b5 100644 --- a/packages/mui-material/src/Tooltip/index.d.ts +++ b/packages/mui-material/src/Tooltip/index.d.ts @@ -3,3 +3,5 @@ export * from './Tooltip'; export { default as tooltipClasses } from './tooltipClasses'; export * from './tooltipClasses'; + +export { private_tooltipVars } from './tooltipVars'; diff --git a/packages/mui-material/src/Tooltip/index.js b/packages/mui-material/src/Tooltip/index.js index 41217639d8cc24..1e5cea82a678df 100644 --- a/packages/mui-material/src/Tooltip/index.js +++ b/packages/mui-material/src/Tooltip/index.js @@ -2,3 +2,5 @@ export { default } from './Tooltip'; export { default as tooltipClasses } from './tooltipClasses'; export * from './tooltipClasses'; + +export { private_tooltipVars } from './tooltipVars'; diff --git a/packages/mui-material/src/Tooltip/tooltipVars.ts b/packages/mui-material/src/Tooltip/tooltipVars.ts new file mode 100644 index 00000000000000..e6d72b534e0693 --- /dev/null +++ b/packages/mui-material/src/Tooltip/tooltipVars.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Tooltip density token identities — the Material UI layer's internal designer + * knobs (`private_*` per the density RFC). Static, unprefixed literals imported + * by both the styled component AND the `enhance*Density` presets that map these + * to density steps, so emitted and targeted names can't drift. The agnostic + * seams (`--comp-*`) and internal defaults (`--_*`) are the literal plumbing and + * live outside this map. Regular (pointer) tooltip only — the `touch` variant is + * out of density scope and keeps its own literals. + */ +export const private_tooltipVars = { + blockPad: '--Tooltip-blockPad', + inlinePad: '--Tooltip-inlinePad', + offset: '--Tooltip-offset', + arrowSize: '--Tooltip-arrowSize', +} as const; diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index 676d06483f16c6..f231c4a19d0e4f 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -90,24 +90,29 @@ export function applyDensity( } /** - * Attach a `styleOverrides.root` object to a component slot, preserving any - * existing root overrides (array-wrapped). Presets use this to add their + * Attach a `styleOverrides` object to a component slot, preserving any existing + * overrides for that slot (array-wrapped). Presets use this to add their * component-var → density-step assignments after `applyDensity`. * + * Defaults to the `root` slot (Button, MenuItem, …). Pass `slot` for components + * whose density seams live on a non-root slot — e.g. Tooltip has no `root` slot, + * so its tokens land on `tooltip` (the bubble, ancestor of the arrow). + * * **Mutates `components` in place** — pass the enhanced theme's `components` * (fresh, owned by `applyDensity`), never a theme's shared `components`. */ export function addRootOverride( components: NonNullable, name: string, - root: Record, + overrides: Record, + slot: string = 'root', ): void { - const slot = (components as any)[name]; + const component = (components as any)[name]; (components as any)[name] = { - ...slot, + ...component, styleOverrides: { - ...slot?.styleOverrides, - root: [slot?.styleOverrides?.root, root], + ...component?.styleOverrides, + [slot]: [component?.styleOverrides?.[slot], overrides], }, }; } diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index a7f6b36d9b1bb3..35840453b60dca 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -2,6 +2,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, Enhancea import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; +import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; const scale: DensityScale = { xxs: '6px', @@ -33,6 +34,19 @@ export default function enhanceComfortDensity(theme: // Menu/list vertical breathing (spacing token). [listVars.blockPad]: d.sm, }); + addRootOverride( + enhanced.components, + 'MuiTooltip', + { + // Regular (pointer) tooltip only — `touch` stays at its literals. + // Padding + anchor offset = density steps (spacing). Arrow = raw px (sizing). + [tooltipVars.blockPad]: d.xxs, + [tooltipVars.inlinePad]: d.sm, + [tooltipVars.offset]: d.lg, + [tooltipVars.arrowSize]: '14px', + }, + 'tooltip', + ); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index cb1d7a69873b97..0c38ffa3f5b1cd 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -2,6 +2,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, Enhancea import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; +import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; const scale: DensityScale = { xxs: '2px', @@ -33,6 +34,19 @@ export default function enhanceCompactDensity(theme: // Menu/list vertical breathing (spacing token). [listVars.blockPad]: d.sm, }); + addRootOverride( + enhanced.components, + 'MuiTooltip', + { + // Regular (pointer) tooltip only — `touch` stays at its literals. + // Padding + anchor offset = density steps (spacing). Arrow = raw px (sizing). + [tooltipVars.blockPad]: d.xxs, + [tooltipVars.inlinePad]: d.sm, + [tooltipVars.offset]: d.lg, + [tooltipVars.arrowSize]: '10px', + }, + 'tooltip', + ); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 6a37c56ae87a78..22ff46a6daef0f 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -2,6 +2,7 @@ import { addRootOverride, applyDensity, densityVars as d, DensityScale, Enhancea import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; +import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button // typography — no reflow — so only the padding→step assignment below. @@ -35,5 +36,18 @@ export default function enhanceNormalDensity(theme: // Menu/list vertical breathing (spacing token). [listVars.blockPad]: d.sm, }); + addRootOverride( + enhanced.components, + 'MuiTooltip', + { + // Regular (pointer) tooltip only — `touch` stays at its literals. + // Padding + anchor offset = density steps (spacing). Arrow = raw px (sizing). + [tooltipVars.blockPad]: d.xxs, + [tooltipVars.inlinePad]: d.sm, + [tooltipVars.offset]: d.lg, + [tooltipVars.arrowSize]: '11px', + }, + 'tooltip', + ); return enhanced; } From ab1aac817e8faefbb113bdc8795222aef6bd85c8 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 22:36:35 +0700 Subject: [PATCH 060/114] density OutlinedInput: tokenize input padding + InputLabel bridge + InputAdornment gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OutlinedInput input block/inline padding + root adornment/multiline gutters, per size (base medium / small variant) - InputLabel outlined resting-Y consumes --InputLabel-y; OutlinedInput sets it via :has(~&) from the blockPad token (±0.5), presets from the density step (siblings don't inherit) - InputAdornment gap + filled marginTop, per size; small size variant added - vars maps outlinedInputVars/inputLabelVars/inputAdornmentVars (re-export js+d.ts) - addRootOverride overrides typed Record (nested selectors + variants); presets wire MuiOutlinedInput (+size variant label rule) + MuiInputAdornment - playground OutlinedInput entry (mapping on TextField ancestor so bridge sees it); fixture demo + scopes - zero-diff maxDiffPixels:0; reflow verified via getComputedStyle (label-Y tracks blockPad ±0.5 every preset); 140 tests pass - single gap token both adornment sides (not leading/trailing split); --comp-* seam; blockPad/inlinePad naming --- docs/pages/experiments/density-fixture.tsx | 18 +++++ docs/pages/experiments/density-playground.tsx | 72 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 26 +++++++ .../src/InputAdornment/InputAdornment.js | 21 +++++- .../src/InputAdornment/index.d.ts | 2 + .../mui-material/src/InputAdornment/index.js | 2 + .../src/InputAdornment/inputAdornmentVars.ts | 14 ++++ .../mui-material/src/InputLabel/InputLabel.js | 7 +- .../mui-material/src/InputLabel/index.d.ts | 2 + packages/mui-material/src/InputLabel/index.js | 2 + .../src/InputLabel/inputLabelVars.ts | 11 +++ .../src/OutlinedInput/OutlinedInput.js | 62 ++++++++++++++-- .../mui-material/src/OutlinedInput/index.d.ts | 2 + .../mui-material/src/OutlinedInput/index.js | 2 + .../src/OutlinedInput/outlinedInputVars.ts | 15 ++++ .../mui-material/src/styles/densityScale.ts | 2 +- .../src/styles/enhanceComfortDensity.ts | 27 +++++++ .../src/styles/enhanceCompactDensity.ts | 27 +++++++ .../src/styles/enhanceNormalDensity.ts | 27 +++++++ 19 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 packages/mui-material/src/InputAdornment/inputAdornmentVars.ts create mode 100644 packages/mui-material/src/InputLabel/inputLabelVars.ts create mode 100644 packages/mui-material/src/OutlinedInput/outlinedInputVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 2c007a6fbc5770..4cfdaaaf536ffb 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -60,6 +60,24 @@ const scopes: Record> = { ['--Tooltip-arrowSize' as any]: '16px', }, }, + OutlinedInput: { + dense: { + ['--OutlinedInput-medium-blockPad' as any]: '10px', + ['--OutlinedInput-small-blockPad' as any]: '5px', + ['--OutlinedInput-medium-inlinePad' as any]: '10px', + ['--OutlinedInput-small-inlinePad' as any]: '8px', + ['--InputAdornment-medium-gap' as any]: '4px', + ['--InputAdornment-small-gap' as any]: '2px', + }, + loose: { + ['--OutlinedInput-medium-blockPad' as any]: '24px', + ['--OutlinedInput-small-blockPad' as any]: '16px', + ['--OutlinedInput-medium-inlinePad' as any]: '20px', + ['--OutlinedInput-small-inlinePad' as any]: '16px', + ['--InputAdornment-medium-gap' as any]: '14px', + ['--InputAdornment-small-gap' as any]: '10px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index efb12a1f64b961..4cc4d617e93271 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -18,6 +18,8 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import InboxIcon from '@mui/icons-material/Inbox'; import TextField from '@mui/material/TextField'; +import InputAdornment, { private_inputAdornmentVars } from '@mui/material/InputAdornment'; +import { private_outlinedInputVars } from '@mui/material/OutlinedInput'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -338,6 +340,62 @@ function TooltipMatrix({ ); } +// OutlinedInput family: input block/inline padding (per size) + adornment gap +// (per size). All spacing → prefill density keys. The label resting-Y is a +// derived bridge (not a direct field). +const OUTLINED_INPUT_FIELDS: DensityField[] = [ + { key: 'mediumBlockPad', cssVar: private_outlinedInputVars.mediumBlockPad }, + { key: 'smallBlockPad', cssVar: private_outlinedInputVars.smallBlockPad }, + { key: 'mediumInlinePad', cssVar: private_outlinedInputVars.mediumInlinePad }, + { key: 'smallInlinePad', cssVar: private_outlinedInputVars.smallInlinePad }, + { key: 'mediumGap', cssVar: private_inputAdornmentVars.mediumGap }, + { key: 'smallGap', cssVar: private_inputAdornmentVars.smallGap }, +]; + +function OutlinedInputMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + // Tokens go on the TextField (ancestor of label + input + adornment) so the + // `:has(~ &)` label bridge sees them — element-level on the input root can't + // reach the sibling label. + const sx = mappingEnabled + ? Object.fromEntries( + OUTLINED_INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + Medium} variant="outlined" sx={sx} /> + Small} + variant="outlined" + size="small" + sx={sx} + /> + Start adornment} + variant="outlined" + sx={sx} + slotProps={{ input: { startAdornment: $ } }} + /> + End adornment} + variant="outlined" + sx={sx} + slotProps={{ input: { endAdornment: kg } }} + /> + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -370,6 +428,20 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + OutlinedInput: { + canvasLabel: 'OutlinedInput — size axis + adornments (label bridge)', + fields: OUTLINED_INPUT_FIELDS, + // All spacing → density keys (match the preset assignment). + prefill: { + mediumBlockPad: 'md', + smallBlockPad: 'sm', + mediumInlinePad: 'lg', + smallInlinePad: 'md', + mediumGap: 'sm', + smallGap: 'xxs', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index d326200a4d5861..86dffc4b2505b1 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -7,6 +7,8 @@ import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Tooltip from '@mui/material/Tooltip'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -79,6 +81,30 @@ const demos: Record = { ), + OutlinedInput: ( + // Labelled outlined fields, one per token group: size axis (medium/small) + + // start/end adornment. Each floating label exercises the `:has()` bridge. + + {/* Empty + unfocused → resting label, centered on the block padding (the + `--InputLabel-y` bridge crux). */} + + + $ }, + }} + /> + kg }, + }} + /> + + ), }; export default demos; diff --git a/packages/mui-material/src/InputAdornment/InputAdornment.js b/packages/mui-material/src/InputAdornment/InputAdornment.js index abb06efb23735f..9dc492af66c64d 100644 --- a/packages/mui-material/src/InputAdornment/InputAdornment.js +++ b/packages/mui-material/src/InputAdornment/InputAdornment.js @@ -11,6 +11,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import inputAdornmentClasses, { getInputAdornmentUtilityClass } from './inputAdornmentClasses'; +import { private_inputAdornmentVars as vars } from './inputAdornmentVars'; const overridesResolver = (props, styles) => { const { ownerState } = props; @@ -50,7 +51,21 @@ const InputAdornmentRoot = styled('div', { alignItems: 'center', whiteSpace: 'nowrap', color: (theme.vars || theme).palette.action.active, + // Density seams over the literal defaults. Medium routing sits in the base + // (not a `size: 'medium'` variant) because `size` from FormControl can be + // undefined; the small variant below reroutes to the small tokens. + '--_gap': '8px', + '--_marginTop': '16px', + '--comp-gap': `var(${vars.mediumGap}, var(--_gap))`, + '--comp-marginTop': `var(${vars.mediumMarginTop}, var(--_marginTop))`, variants: [ + { + props: ({ ownerState }) => ownerState.size === 'small', + style: { + '--comp-gap': `var(${vars.smallGap}, var(--_gap))`, + '--comp-marginTop': `var(${vars.smallMarginTop}, var(--_marginTop))`, + }, + }, { props: { variant: 'filled', @@ -58,7 +73,7 @@ const InputAdornmentRoot = styled('div', { style: { [`&.${inputAdornmentClasses.positionStart}&:not(.${inputAdornmentClasses.hiddenLabel})`]: { - marginTop: 16, + marginTop: 'var(--comp-marginTop, var(--_marginTop))', }, }, }, @@ -67,7 +82,7 @@ const InputAdornmentRoot = styled('div', { position: 'start', }, style: { - marginRight: 8, + marginRight: 'var(--comp-gap, var(--_gap))', }, }, { @@ -75,7 +90,7 @@ const InputAdornmentRoot = styled('div', { position: 'end', }, style: { - marginLeft: 8, + marginLeft: 'var(--comp-gap, var(--_gap))', }, }, { diff --git a/packages/mui-material/src/InputAdornment/index.d.ts b/packages/mui-material/src/InputAdornment/index.d.ts index 48e5ebc6741b70..6021d093815140 100644 --- a/packages/mui-material/src/InputAdornment/index.d.ts +++ b/packages/mui-material/src/InputAdornment/index.d.ts @@ -3,3 +3,5 @@ export { default } from './InputAdornment'; export { default as inputAdornmentClasses } from './inputAdornmentClasses'; export * from './inputAdornmentClasses'; + +export { private_inputAdornmentVars } from './inputAdornmentVars'; diff --git a/packages/mui-material/src/InputAdornment/index.js b/packages/mui-material/src/InputAdornment/index.js index b1e7375e2c5564..6603f8fd3b7cfb 100644 --- a/packages/mui-material/src/InputAdornment/index.js +++ b/packages/mui-material/src/InputAdornment/index.js @@ -2,3 +2,5 @@ export { default } from './InputAdornment'; export { default as inputAdornmentClasses } from './inputAdornmentClasses'; export * from './inputAdornmentClasses'; + +export { private_inputAdornmentVars } from './inputAdornmentVars'; diff --git a/packages/mui-material/src/InputAdornment/inputAdornmentVars.ts b/packages/mui-material/src/InputAdornment/inputAdornmentVars.ts new file mode 100644 index 00000000000000..c20361ab4b0175 --- /dev/null +++ b/packages/mui-material/src/InputAdornment/inputAdornmentVars.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * InputAdornment density token identities — internal designer knobs (`private_*` + * per the density RFC). Static literals shared by the styled component AND the + * `enhance*Density` presets. `gap` = the input-facing margin (start adornment's + * marginRight / end adornment's marginLeft, one token both sides); `marginTop` = + * the filled positionStart top offset. Per-size (medium default / small). + */ +export const private_inputAdornmentVars = { + mediumGap: '--InputAdornment-medium-gap', + smallGap: '--InputAdornment-small-gap', + mediumMarginTop: '--InputAdornment-medium-marginTop', + smallMarginTop: '--InputAdornment-small-marginTop', +} as const; diff --git a/packages/mui-material/src/InputLabel/InputLabel.js b/packages/mui-material/src/InputLabel/InputLabel.js index f62e71601fce90..f065aeabd1f410 100644 --- a/packages/mui-material/src/InputLabel/InputLabel.js +++ b/packages/mui-material/src/InputLabel/InputLabel.js @@ -11,6 +11,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getInputLabelUtilityClasses } from './inputLabelClasses'; +import { private_inputLabelVars as vars } from './inputLabelVars'; import { getTransitionStyles } from '../transitions/utils'; const useUtilityClasses = (ownerState) => { @@ -145,7 +146,9 @@ const InputLabelRoot = styled(FormLabel, { // see comment above on filled.zIndex zIndex: 1, pointerEvents: 'none', - transform: 'translate(14px, 16px) scale(1)', + // Resting-Y is a seam set by OutlinedInput (via `:has(~ &)`) so the + // label tracks the input's block padding under density; default 16px. + transform: `translate(14px, var(${vars.y}, 16px)) scale(1)`, maxWidth: 'calc(100% - 24px)', }, }, @@ -155,7 +158,7 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - transform: 'translate(14px, 9px) scale(1)', + transform: `translate(14px, var(${vars.y}, 9px)) scale(1)`, }, }, { diff --git a/packages/mui-material/src/InputLabel/index.d.ts b/packages/mui-material/src/InputLabel/index.d.ts index 1bee4cf24899ca..daa187aca36388 100644 --- a/packages/mui-material/src/InputLabel/index.d.ts +++ b/packages/mui-material/src/InputLabel/index.d.ts @@ -3,3 +3,5 @@ export * from './InputLabel'; export { default as inputLabelClasses } from './inputLabelClasses'; export * from './inputLabelClasses'; + +export { private_inputLabelVars } from './inputLabelVars'; diff --git a/packages/mui-material/src/InputLabel/index.js b/packages/mui-material/src/InputLabel/index.js index 70140e12a84000..a0a3b397a755eb 100644 --- a/packages/mui-material/src/InputLabel/index.js +++ b/packages/mui-material/src/InputLabel/index.js @@ -2,3 +2,5 @@ export { default } from './InputLabel'; export { default as inputLabelClasses } from './inputLabelClasses'; export * from './inputLabelClasses'; + +export { private_inputLabelVars } from './inputLabelVars'; diff --git a/packages/mui-material/src/InputLabel/inputLabelVars.ts b/packages/mui-material/src/InputLabel/inputLabelVars.ts new file mode 100644 index 00000000000000..b8544352877f8e --- /dev/null +++ b/packages/mui-material/src/InputLabel/inputLabelVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * InputLabel density token identity — the outlined label's resting translateY. + * A seam **set by the input** (OutlinedInput/FilledInput), not owned here: the + * label is a preceding DOM sibling of the input, so the input pushes this value + * onto the label via `:has(~ &)`. The label only consumes it (`private_*` per + * the density RFC). + */ +export const private_inputLabelVars = { + y: '--InputLabel-y', +} as const; diff --git a/packages/mui-material/src/OutlinedInput/OutlinedInput.js b/packages/mui-material/src/OutlinedInput/OutlinedInput.js index bb851995a8224d..2f045a1c3f717f 100644 --- a/packages/mui-material/src/OutlinedInput/OutlinedInput.js +++ b/packages/mui-material/src/OutlinedInput/OutlinedInput.js @@ -11,6 +11,8 @@ import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import { useDefaultProps } from '../DefaultPropsProvider'; import outlinedInputClasses, { getOutlinedInputUtilityClass } from './outlinedInputClasses'; +import inputLabelClasses from '../InputLabel/inputLabelClasses'; +import { private_outlinedInputVars as vars } from './outlinedInputVars'; import InputBase, { rootOverridesResolver as inputBaseRootOverridesResolver, inputOverridesResolver as inputBaseInputOverridesResolver, @@ -46,6 +48,13 @@ const OutlinedInputRoot = styled(InputBaseRoot, { const borderColor = theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; return { + // Density seam: the outlined label centers on the input's block padding, + // but it's a preceding sibling — reach it via `:has(~ &)` and derive its + // resting-Y from the public sized token. Medium resolves to 16px + // (16.5 - 0.5 rounding); the small variant below does 9px (8.5 + 0.5). + [`.${inputLabelClasses.root}:has(~ &)`]: { + '--InputLabel-y': `calc(var(${vars.mediumBlockPad}, 16.5px) - 0.5px)`, + }, position: 'relative', borderRadius: (theme.vars || theme).shape.borderRadius, [`&:hover .${outlinedInputClasses.notchedOutline}`]: { @@ -84,28 +93,60 @@ const OutlinedInputRoot = styled(InputBaseRoot, { }, }, }, + { + props: { size: 'small' }, + style: { + // Small label resolves to 9px (8.5 + 0.5). + [`.${inputLabelClasses.root}:has(~ &)`]: { + '--InputLabel-y': `calc(var(${vars.smallBlockPad}, 8.5px) + 0.5px)`, + }, + }, + }, { props: ({ ownerState }) => ownerState.startAdornment, style: { - paddingLeft: 14, + '--_inlinePad': '14px', + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + }, + }, + { + props: ({ ownerState, size }) => ownerState.startAdornment && size === 'small', + style: { + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { props: ({ ownerState }) => ownerState.endAdornment, style: { - paddingRight: 14, + '--_inlinePad': '14px', + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', + }, + }, + { + props: ({ ownerState, size }) => ownerState.endAdornment && size === 'small', + style: { + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { props: ({ ownerState }) => ownerState.multiline, style: { - padding: '16.5px 14px', + '--_blockPad': '16.5px', + '--_inlinePad': '14px', + '--comp-blockPad': `var(${vars.mediumBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + padding: + 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', }, }, { props: ({ ownerState, size }) => ownerState.multiline && size === 'small', style: { - padding: '8.5px 14px', + '--_blockPad': '8.5px', + '--comp-blockPad': `var(${vars.smallBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, ], @@ -134,7 +175,14 @@ const OutlinedInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - padding: '16.5px 14px', + // Both axes tokenized in place: `var(--comp-, var(--_))` over the + // per-size public token; the internal defaults are the Material px (block + // 16.5/8.5, inline 14 both sizes), specialized by the size variant below. + '--_blockPad': '16.5px', + '--_inlinePad': '14px', + '--comp-blockPad': `var(${vars.mediumBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', '&:-webkit-autofill': { ...(!theme.vars && { WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', @@ -155,7 +203,9 @@ const OutlinedInputInput = styled(InputBaseInput, { size: 'small', }, style: { - padding: '8.5px 14px', + '--_blockPad': '8.5px', + '--comp-blockPad': `var(${vars.smallBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { diff --git a/packages/mui-material/src/OutlinedInput/index.d.ts b/packages/mui-material/src/OutlinedInput/index.d.ts index 70e67f7c67f200..03c2a36dcfd7dc 100644 --- a/packages/mui-material/src/OutlinedInput/index.d.ts +++ b/packages/mui-material/src/OutlinedInput/index.d.ts @@ -3,3 +3,5 @@ export * from './OutlinedInput'; export { default as outlinedInputClasses } from './outlinedInputClasses'; export * from './outlinedInputClasses'; + +export { private_outlinedInputVars } from './outlinedInputVars'; diff --git a/packages/mui-material/src/OutlinedInput/index.js b/packages/mui-material/src/OutlinedInput/index.js index 4877ca68a9f8f5..31c81e125c0289 100644 --- a/packages/mui-material/src/OutlinedInput/index.js +++ b/packages/mui-material/src/OutlinedInput/index.js @@ -2,3 +2,5 @@ export { default } from './OutlinedInput'; export { default as outlinedInputClasses } from './outlinedInputClasses'; export * from './outlinedInputClasses'; + +export { private_outlinedInputVars } from './outlinedInputVars'; diff --git a/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts b/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts new file mode 100644 index 00000000000000..4ec94499c6b50b --- /dev/null +++ b/packages/mui-material/src/OutlinedInput/outlinedInputVars.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * OutlinedInput density token identities — internal designer knobs (`private_*` + * per the density RFC). Static literals imported by both the styled component + * AND the `enhance*Density` presets so emitted and targeted names can't drift. + * Per-size (medium default / small); block vs inline split because the outlined + * label centers on the block padding. The agnostic seams (`--comp-*`) and + * internal defaults (`--_*`) are the literal plumbing, outside this map. + */ +export const private_outlinedInputVars = { + mediumBlockPad: '--OutlinedInput-medium-blockPad', + smallBlockPad: '--OutlinedInput-small-blockPad', + mediumInlinePad: '--OutlinedInput-medium-inlinePad', + smallInlinePad: '--OutlinedInput-small-inlinePad', +} as const; diff --git a/packages/mui-material/src/styles/densityScale.ts b/packages/mui-material/src/styles/densityScale.ts index f231c4a19d0e4f..240c2f961457dc 100644 --- a/packages/mui-material/src/styles/densityScale.ts +++ b/packages/mui-material/src/styles/densityScale.ts @@ -104,7 +104,7 @@ export function applyDensity( export function addRootOverride( components: NonNullable, name: string, - overrides: Record, + overrides: Record, slot: string = 'root', ): void { const component = (components as any)[name]; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 35840453b60dca..57f904b202c14e 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -3,6 +3,10 @@ import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; +import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; +import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; +import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { xxs: '6px', @@ -47,6 +51,29 @@ export default function enhanceComfortDensity(theme: }, 'tooltip', ); + addRootOverride(enhanced.components, 'MuiOutlinedInput', { + // Padding = density steps (block < inline, keeping the 16.5/14 feel). + [oiVars.mediumBlockPad]: d.md, + [oiVars.smallBlockPad]: d.sm, + [oiVars.mediumInlinePad]: d.lg, + [oiVars.smallInlinePad]: d.md, + // The label is a preceding sibling — it can't read the input root's token, + // so derive `--InputLabel-y` from the density step (inherited from :root), + // matching the component's -0.5/+0.5 per-size rounding. + [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.md} - 0.5px)` }, + variants: [ + { + props: { size: 'small' }, + style: { [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.sm} + 0.5px)` } }, + }, + ], + }); + addRootOverride(enhanced.components, 'MuiInputAdornment', { + [iaVars.smallGap]: d.xxs, + [iaVars.mediumGap]: d.sm, + [iaVars.smallMarginTop]: d.md, + [iaVars.mediumMarginTop]: d.lg, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 0c38ffa3f5b1cd..57e8914955f579 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -3,6 +3,10 @@ import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; +import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; +import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; +import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { xxs: '2px', @@ -47,6 +51,29 @@ export default function enhanceCompactDensity(theme: }, 'tooltip', ); + addRootOverride(enhanced.components, 'MuiOutlinedInput', { + // Padding = density steps (block < inline, keeping the 16.5/14 feel). + [oiVars.mediumBlockPad]: d.md, + [oiVars.smallBlockPad]: d.sm, + [oiVars.mediumInlinePad]: d.lg, + [oiVars.smallInlinePad]: d.md, + // The label is a preceding sibling — it can't read the input root's token, + // so derive `--InputLabel-y` from the density step (inherited from :root), + // matching the component's -0.5/+0.5 per-size rounding. + [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.md} - 0.5px)` }, + variants: [ + { + props: { size: 'small' }, + style: { [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.sm} + 0.5px)` } }, + }, + ], + }); + addRootOverride(enhanced.components, 'MuiInputAdornment', { + [iaVars.smallGap]: d.xxs, + [iaVars.mediumGap]: d.sm, + [iaVars.smallMarginTop]: d.md, + [iaVars.mediumMarginTop]: d.lg, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 22ff46a6daef0f..2f3ed5de9d505d 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -3,6 +3,10 @@ import { private_buttonVars as buttonVars } from '../Button/buttonVars'; import { private_menuItemVars as menuItemVars } from '../MenuItem/menuItemVars'; import { private_listVars as listVars } from '../List/listVars'; import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; +import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; +import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; +import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button // typography — no reflow — so only the padding→step assignment below. @@ -49,5 +53,28 @@ export default function enhanceNormalDensity(theme: }, 'tooltip', ); + addRootOverride(enhanced.components, 'MuiOutlinedInput', { + // Padding = density steps (block < inline, keeping the 16.5/14 feel). + [oiVars.mediumBlockPad]: d.md, + [oiVars.smallBlockPad]: d.sm, + [oiVars.mediumInlinePad]: d.lg, + [oiVars.smallInlinePad]: d.md, + // The label is a preceding sibling — it can't read the input root's token, + // so derive `--InputLabel-y` from the density step (inherited from :root), + // matching the component's -0.5/+0.5 per-size rounding. + [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.md} - 0.5px)` }, + variants: [ + { + props: { size: 'small' }, + style: { [`.${inputLabelClasses.root}:has(~ &)`]: { [ilVars.y]: `calc(${d.sm} + 0.5px)` } }, + }, + ], + }); + addRootOverride(enhanced.components, 'MuiInputAdornment', { + [iaVars.smallGap]: d.xxs, + [iaVars.mediumGap]: d.sm, + [iaVars.smallMarginTop]: d.md, + [iaVars.mediumMarginTop]: d.lg, + }); return enhanced; } From 51f63072d24800619d8d6bb3d3ff420c689d9be3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 22:49:24 +0700 Subject: [PATCH 061/114] density FilledInput: tokenize box padding (per size) + two-value label bridge - FilledInput input + root top/bottom/inline padding, per size (base medium / small variant); hiddenLabel keeps literals (out of scope) - InputLabel filled resting+shrunk transforms consume --FilledInputLabel-restY/-shrinkY (fallback = today literals); identity in inputLabelVars (filledRestY/filledShrinkY) - no clean topPad->Y formula; presets set label Ys as tuned raw px per size via :has(~&)+variants - filledInputVars (medium/small x topPad/bottomPad/inlinePad); re-export js+d.ts - playground FilledInput entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle (box + label track); 92 tests pass --- docs/pages/experiments/density-fixture.tsx | 16 +++++ docs/pages/experiments/density-playground.tsx | 61 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 9 +++ .../src/FilledInput/FilledInput.js | 61 +++++++++++++++---- .../src/FilledInput/filledInputVars.ts | 17 ++++++ .../mui-material/src/FilledInput/index.d.ts | 2 + .../mui-material/src/FilledInput/index.js | 2 + .../mui-material/src/InputLabel/InputLabel.js | 10 +-- .../src/InputLabel/inputLabelVars.ts | 13 ++-- .../src/styles/enhanceComfortDensity.ts | 26 ++++++++ .../src/styles/enhanceCompactDensity.ts | 26 ++++++++ .../src/styles/enhanceNormalDensity.ts | 26 ++++++++ 12 files changed, 249 insertions(+), 20 deletions(-) create mode 100644 packages/mui-material/src/FilledInput/filledInputVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 4cfdaaaf536ffb..8daa45733d1510 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -78,6 +78,22 @@ const scopes: Record> = { ['--InputAdornment-small-gap' as any]: '10px', }, }, + FilledInput: { + dense: { + ['--FilledInput-medium-topPad' as any]: '18px', + ['--FilledInput-medium-bottomPad' as any]: '4px', + ['--FilledInput-medium-inlinePad' as any]: '8px', + ['--FilledInputLabel-restY' as any]: '11px', + ['--FilledInputLabel-shrinkY' as any]: '5px', + }, + loose: { + ['--FilledInput-medium-topPad' as any]: '34px', + ['--FilledInput-medium-bottomPad' as any]: '12px', + ['--FilledInput-medium-inlinePad' as any]: '16px', + ['--FilledInputLabel-restY' as any]: '22px', + ['--FilledInputLabel-shrinkY' as any]: '10px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 4cc4d617e93271..5c3b9424337635 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -20,6 +20,7 @@ import InboxIcon from '@mui/icons-material/Inbox'; import TextField from '@mui/material/TextField'; import InputAdornment, { private_inputAdornmentVars } from '@mui/material/InputAdornment'; import { private_outlinedInputVars } from '@mui/material/OutlinedInput'; +import { private_filledInputVars } from '@mui/material/FilledInput'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -396,6 +397,53 @@ function OutlinedInputMatrix({ ); } +// FilledInput family: box top/bottom/inline padding (per size). All spacing → +// prefill density keys. The label rest/shrink Y follow the active preset (tuned +// raw px, not editable here). +const FILLED_INPUT_FIELDS: DensityField[] = [ + { key: 'mediumTopPad', cssVar: private_filledInputVars.mediumTopPad }, + { key: 'smallTopPad', cssVar: private_filledInputVars.smallTopPad }, + { key: 'mediumBottomPad', cssVar: private_filledInputVars.mediumBottomPad }, + { key: 'smallBottomPad', cssVar: private_filledInputVars.smallBottomPad }, + { key: 'mediumInlinePad', cssVar: private_filledInputVars.mediumInlinePad }, + { key: 'smallInlinePad', cssVar: private_filledInputVars.smallInlinePad }, +]; + +function FilledInputMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + FILLED_INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + Medium} variant="filled" sx={sx} /> + Small} + variant="filled" + size="small" + sx={sx} + /> + Filled value} + variant="filled" + defaultValue="Value" + sx={sx} + /> + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -442,6 +490,19 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + FilledInput: { + canvasLabel: 'FilledInput — size axis (box padding); label follows preset', + fields: FILLED_INPUT_FIELDS, + prefill: { + mediumTopPad: 'xl', + smallTopPad: 'lg', + mediumBottomPad: 'sm', + smallBottomPad: 'xxs', + mediumInlinePad: 'md', + smallInlinePad: 'md', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 86dffc4b2505b1..adaab0fb823d70 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -105,6 +105,15 @@ const demos: Record = { /> ), + FilledInput: ( + // Empty (resting label, inside the box) + valued (shrunk label, in the top + // strip) exercise both Y seams of the two-value label bridge. + + + + + + ), }; export default demos; diff --git a/packages/mui-material/src/FilledInput/FilledInput.js b/packages/mui-material/src/FilledInput/FilledInput.js index 349252d64c3d5e..b13d01ac70105b 100644 --- a/packages/mui-material/src/FilledInput/FilledInput.js +++ b/packages/mui-material/src/FilledInput/FilledInput.js @@ -11,6 +11,7 @@ import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import { useDefaultProps } from '../DefaultPropsProvider'; import filledInputClasses, { getFilledInputUtilityClass } from './filledInputClasses'; +import { private_filledInputVars as vars } from './filledInputVars'; import { rootOverridesResolver as inputBaseRootOverridesResolver, inputOverridesResolver as inputBaseInputOverridesResolver, @@ -157,26 +158,52 @@ const FilledInputRoot = styled(InputBaseRoot, { { props: ({ ownerState }) => ownerState.startAdornment, style: { - paddingLeft: 12, + '--_inlinePad': '12px', + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + }, + }, + { + props: ({ ownerState, size }) => ownerState.startAdornment && size === 'small', + style: { + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { props: ({ ownerState }) => ownerState.endAdornment, style: { - paddingRight: 12, + '--_inlinePad': '12px', + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', + }, + }, + { + props: ({ ownerState, size }) => ownerState.endAdornment && size === 'small', + style: { + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { props: ({ ownerState }) => ownerState.multiline, style: { - padding: '25px 12px 8px', + '--_topPad': '25px', + '--_bottomPad': '8px', + '--_inlinePad': '12px', + '--comp-topPad': `var(${vars.mediumTopPad}, var(--_topPad))`, + '--comp-bottomPad': `var(${vars.mediumBottomPad}, var(--_bottomPad))`, + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + padding: + 'var(--comp-topPad, var(--_topPad)) var(--comp-inlinePad, var(--_inlinePad)) var(--comp-bottomPad, var(--_bottomPad))', }, }, { props: ({ ownerState, size }) => ownerState.multiline && size === 'small', style: { - paddingTop: 21, - paddingBottom: 4, + '--_topPad': '21px', + '--_bottomPad': '4px', + '--comp-topPad': `var(${vars.smallTopPad}, var(--_topPad))`, + '--comp-bottomPad': `var(${vars.smallBottomPad}, var(--_bottomPad))`, + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { @@ -205,10 +232,19 @@ const FilledInputInput = styled(InputBaseInput, { overridesResolver: inputBaseInputOverridesResolver, })( memoTheme(({ theme }) => ({ - paddingTop: 25, - paddingRight: 12, - paddingBottom: 8, - paddingLeft: 12, + // Box height = top/bottom padding, tokenized per size (base medium / small + // variant). The with-label defaults are the Material px (25/8 medium, 21/4 + // small); `hiddenLabel` keeps its own literals below (out of scope). + '--_topPad': '25px', + '--_bottomPad': '8px', + '--_inlinePad': '12px', + '--comp-topPad': `var(${vars.mediumTopPad}, var(--_topPad))`, + '--comp-bottomPad': `var(${vars.mediumBottomPad}, var(--_bottomPad))`, + '--comp-inlinePad': `var(${vars.mediumInlinePad}, var(--_inlinePad))`, + paddingTop: 'var(--comp-topPad, var(--_topPad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingBottom: 'var(--comp-bottomPad, var(--_bottomPad))', + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', '&:-webkit-autofill': { ...(!theme.vars && { WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', @@ -230,8 +266,11 @@ const FilledInputInput = styled(InputBaseInput, { size: 'small', }, style: { - paddingTop: 21, - paddingBottom: 4, + '--_topPad': '21px', + '--_bottomPad': '4px', + '--comp-topPad': `var(${vars.smallTopPad}, var(--_topPad))`, + '--comp-bottomPad': `var(${vars.smallBottomPad}, var(--_bottomPad))`, + '--comp-inlinePad': `var(${vars.smallInlinePad}, var(--_inlinePad))`, }, }, { diff --git a/packages/mui-material/src/FilledInput/filledInputVars.ts b/packages/mui-material/src/FilledInput/filledInputVars.ts new file mode 100644 index 00000000000000..8ba645436487c4 --- /dev/null +++ b/packages/mui-material/src/FilledInput/filledInputVars.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * FilledInput density token identities — internal designer knobs (`private_*` + * per the density RFC). The box height is the input's top/bottom padding (per + * size). The floating label's resting/shrunk Y are separate seams in + * `inputLabelVars` (`filledRestY`/`filledShrinkY`) — set by the presets (no + * clean formula from `topPad`, the offsets are hand-tuned per size), consumed + * by InputLabel. `hiddenLabel` is out of scope (keeps its own literals). + */ +export const private_filledInputVars = { + mediumTopPad: '--FilledInput-medium-topPad', + smallTopPad: '--FilledInput-small-topPad', + mediumBottomPad: '--FilledInput-medium-bottomPad', + smallBottomPad: '--FilledInput-small-bottomPad', + mediumInlinePad: '--FilledInput-medium-inlinePad', + smallInlinePad: '--FilledInput-small-inlinePad', +} as const; diff --git a/packages/mui-material/src/FilledInput/index.d.ts b/packages/mui-material/src/FilledInput/index.d.ts index f3576964636f51..633c693367f2eb 100644 --- a/packages/mui-material/src/FilledInput/index.d.ts +++ b/packages/mui-material/src/FilledInput/index.d.ts @@ -3,3 +3,5 @@ export * from './FilledInput'; export { default as filledInputClasses } from './filledInputClasses'; export * from './filledInputClasses'; + +export { private_filledInputVars } from './filledInputVars'; diff --git a/packages/mui-material/src/FilledInput/index.js b/packages/mui-material/src/FilledInput/index.js index 91b5a314f0050b..2c5cfbde0db6e8 100644 --- a/packages/mui-material/src/FilledInput/index.js +++ b/packages/mui-material/src/FilledInput/index.js @@ -2,3 +2,5 @@ export { default } from './FilledInput'; export { default as filledInputClasses } from './filledInputClasses'; export * from './filledInputClasses'; + +export { private_filledInputVars } from './filledInputVars'; diff --git a/packages/mui-material/src/InputLabel/InputLabel.js b/packages/mui-material/src/InputLabel/InputLabel.js index f065aeabd1f410..fb8095f8aac9bd 100644 --- a/packages/mui-material/src/InputLabel/InputLabel.js +++ b/packages/mui-material/src/InputLabel/InputLabel.js @@ -109,7 +109,9 @@ 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)', + // Resting/shrunk Y are seams the input (FilledInput) sets under density + // so the label tracks the box's top padding; defaults are today's px. + transform: `translate(12px, var(${vars.filledRestY}, 16px)) scale(1)`, maxWidth: 'calc(100% - 24px)', }, }, @@ -119,7 +121,7 @@ const InputLabelRoot = styled(FormLabel, { size: 'small', }, style: { - transform: 'translate(12px, 13px) scale(1)', + transform: `translate(12px, var(${vars.filledRestY}, 13px)) scale(1)`, }, }, { @@ -127,7 +129,7 @@ const InputLabelRoot = styled(FormLabel, { style: { userSelect: 'none', pointerEvents: 'auto', - transform: 'translate(12px, 7px) scale(0.75)', + transform: `translate(12px, var(${vars.filledShrinkY}, 7px)) scale(0.75)`, maxWidth: 'calc(133% - 24px)', }, }, @@ -135,7 +137,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, var(${vars.filledShrinkY}, 4px)) scale(0.75)`, }, }, { diff --git a/packages/mui-material/src/InputLabel/inputLabelVars.ts b/packages/mui-material/src/InputLabel/inputLabelVars.ts index b8544352877f8e..77172016053cd7 100644 --- a/packages/mui-material/src/InputLabel/inputLabelVars.ts +++ b/packages/mui-material/src/InputLabel/inputLabelVars.ts @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ /** - * InputLabel density token identity — the outlined label's resting translateY. - * A seam **set by the input** (OutlinedInput/FilledInput), not owned here: the - * label is a preceding DOM sibling of the input, so the input pushes this value - * onto the label via `:has(~ &)`. The label only consumes it (`private_*` per - * the density RFC). + * InputLabel density token identities — the floating label's translateY seams, + * **set by the input** (OutlinedInput/FilledInput), not owned here: the label is + * a preceding DOM sibling of the input, so the input (or a preset) pushes these + * onto the label via `:has(~ &)`. The label only consumes them (`private_*` per + * the density RFC). `y` = outlined resting Y; `filledRestY`/`filledShrinkY` = + * the filled label's resting/shrunk Y (both inside the box). */ export const private_inputLabelVars = { y: '--InputLabel-y', + filledRestY: '--FilledInputLabel-restY', + filledShrinkY: '--FilledInputLabel-shrinkY', } as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 57f904b202c14e..49ad7375a1d310 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -6,6 +6,7 @@ import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -74,6 +75,31 @@ export default function enhanceComfortDensity(theme: [iaVars.smallMarginTop]: d.md, [iaVars.mediumMarginTop]: d.lg, }); + addRootOverride(enhanced.components, 'MuiFilledInput', { + // Box padding = density steps; the label rest/shrink Y are tuned raw px + // (no clean formula from topPad), set on the sibling label via `:has(~ &)`. + [fiVars.mediumTopPad]: d.xl, + [fiVars.smallTopPad]: d.lg, + [fiVars.mediumBottomPad]: d.sm, + [fiVars.smallBottomPad]: d.xxs, + [fiVars.mediumInlinePad]: d.md, + [fiVars.smallInlinePad]: d.md, + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '20px', + [ilVars.filledShrinkY]: '9px', + }, + variants: [ + { + props: { size: 'small' }, + style: { + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '15px', + [ilVars.filledShrinkY]: '5px', + }, + }, + }, + ], + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 57e8914955f579..70d542f20ebb6a 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -6,6 +6,7 @@ import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -74,6 +75,31 @@ export default function enhanceCompactDensity(theme: [iaVars.smallMarginTop]: d.md, [iaVars.mediumMarginTop]: d.lg, }); + addRootOverride(enhanced.components, 'MuiFilledInput', { + // Box padding = density steps; the label rest/shrink Y are tuned raw px + // (no clean formula from topPad), set on the sibling label via `:has(~ &)`. + [fiVars.mediumTopPad]: d.xl, + [fiVars.smallTopPad]: d.lg, + [fiVars.mediumBottomPad]: d.sm, + [fiVars.smallBottomPad]: d.xxs, + [fiVars.mediumInlinePad]: d.md, + [fiVars.smallInlinePad]: d.md, + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '11px', + [ilVars.filledShrinkY]: '5px', + }, + variants: [ + { + props: { size: 'small' }, + style: { + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '8px', + [ilVars.filledShrinkY]: '3px', + }, + }, + }, + ], + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 2f3ed5de9d505d..5e311a03849884 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -6,6 +6,7 @@ import { private_tooltipVars as tooltipVars } from '../Tooltip/tooltipVars'; import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedInputVars'; import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; +import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -76,5 +77,30 @@ export default function enhanceNormalDensity(theme: [iaVars.smallMarginTop]: d.md, [iaVars.mediumMarginTop]: d.lg, }); + addRootOverride(enhanced.components, 'MuiFilledInput', { + // Box padding = density steps; the label rest/shrink Y are tuned raw px + // (no clean formula from topPad), set on the sibling label via `:has(~ &)`. + [fiVars.mediumTopPad]: d.xl, + [fiVars.smallTopPad]: d.lg, + [fiVars.mediumBottomPad]: d.sm, + [fiVars.smallBottomPad]: d.xxs, + [fiVars.mediumInlinePad]: d.md, + [fiVars.smallInlinePad]: d.md, + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '15px', + [ilVars.filledShrinkY]: '7px', + }, + variants: [ + { + props: { size: 'small' }, + style: { + [`.${inputLabelClasses.root}:has(~ &)`]: { + [ilVars.filledRestY]: '10px', + [ilVars.filledShrinkY]: '4px', + }, + }, + }, + ], + }); return enhanced; } From 82d6a34a41b5a5c24dc9b6b507cf082c9094aeaf Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 22:56:29 +0700 Subject: [PATCH 062/114] density Input (standard): tokenize input top/bottom padding on the standard slot - tokenize on Input's InputInput (was empty {}), not the shared InputBase baseline, so the literal stays the zero-diff fallback for Filled/Outlined - topPad per size (base medium / small variant), bottomPad shared; inline 0; multiline resets to 0 - inputVars (mediumTopPad/smallTopPad/bottomPad); re-export js+d.ts; presets map mediumTopPad->xs, smallTopPad->xxs, bottomPad->xs - root marginTop:16 label reserve untouched (typography, out of scope) - playground Input entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle; 38 tests pass - completes TextField's 3 input slices (CS-19) --- docs/pages/experiments/density-fixture.tsx | 10 +++++ docs/pages/experiments/density-playground.tsx | 45 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 8 ++++ packages/mui-material/src/Input/Input.js | 30 ++++++++++++- packages/mui-material/src/Input/index.d.ts | 2 + packages/mui-material/src/Input/index.js | 2 + packages/mui-material/src/Input/inputVars.ts | 16 +++++++ .../src/styles/enhanceComfortDensity.ts | 7 +++ .../src/styles/enhanceCompactDensity.ts | 7 +++ .../src/styles/enhanceNormalDensity.ts | 7 +++ 10 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 packages/mui-material/src/Input/inputVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 8daa45733d1510..8ca2ac5f7c7576 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -94,6 +94,16 @@ const scopes: Record> = { ['--FilledInputLabel-shrinkY' as any]: '10px', }, }, + Input: { + dense: { + ['--Input-medium-topPad' as any]: '2px', + ['--Input-bottomPad' as any]: '2px', + }, + loose: { + ['--Input-medium-topPad' as any]: '12px', + ['--Input-bottomPad' as any]: '12px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 5c3b9424337635..5225268ff49de8 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -21,6 +21,7 @@ import TextField from '@mui/material/TextField'; import InputAdornment, { private_inputAdornmentVars } from '@mui/material/InputAdornment'; import { private_outlinedInputVars } from '@mui/material/OutlinedInput'; import { private_filledInputVars } from '@mui/material/FilledInput'; +import { private_inputVars } from '@mui/material/Input'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -444,6 +445,40 @@ function FilledInputMatrix({ ); } +// Input (standard) family: input top/bottom padding (top per size, bottom +// shared). Inline is 0; the label floats above (no bridge). +const INPUT_FIELDS: DensityField[] = [ + { key: 'mediumTopPad', cssVar: private_inputVars.mediumTopPad }, + { key: 'smallTopPad', cssVar: private_inputVars.smallTopPad }, + { key: 'bottomPad', cssVar: private_inputVars.bottomPad }, +]; + +function InputMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + Medium} variant="standard" sx={sx} /> + Small} + variant="standard" + size="small" + sx={sx} + /> + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -503,6 +538,16 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + Input: { + canvasLabel: 'Input (standard) — size axis (input top/bottom padding)', + fields: INPUT_FIELDS, + prefill: { + mediumTopPad: 'xs', + smallTopPad: 'xxs', + bottomPad: 'xs', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index adaab0fb823d70..dc61ec4189a3fc 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -114,6 +114,14 @@ const demos: Record = { ), + Input: ( + // Standard fields, label floats above (no bridge). Empty → input top/bottom + // padding is the whole density lever. + + + + + ), }; export default demos; diff --git a/packages/mui-material/src/Input/Input.js b/packages/mui-material/src/Input/Input.js index 49eaef2a04b135..b87ffc1623a28c 100644 --- a/packages/mui-material/src/Input/Input.js +++ b/packages/mui-material/src/Input/Input.js @@ -12,6 +12,7 @@ import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFil import { useDefaultProps } from '../DefaultPropsProvider'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; import inputClasses, { getInputUtilityClass } from './inputClasses'; +import { private_inputVars as vars } from './inputVars'; import { getTransitionStyles } from '../transitions/utils'; import { rootOverridesResolver as inputBaseRootOverridesResolver, @@ -138,7 +139,34 @@ const InputInput = styled(InputBaseInput, { name: 'MuiInput', slot: 'Input', overridesResolver: inputBaseInputOverridesResolver, -})({}); +})({ + // Standard-only density seam over the InputBase baseline padding (`4px 0 5px`; + // small top `1px`). Tokenized here — not on the shared baseline — so the + // literal stays the zero-diff fallback for Filled/Outlined. Inline stays 0; + // multiline resets to 0 (as today). + '--_topPad': '4px', + '--_bottomPad': '5px', + '--comp-topPad': `var(${vars.mediumTopPad}, var(--_topPad))`, + '--comp-bottomPad': `var(${vars.bottomPad}, var(--_bottomPad))`, + paddingTop: 'var(--comp-topPad, var(--_topPad))', + paddingBottom: 'var(--comp-bottomPad, var(--_bottomPad))', + variants: [ + { + props: { size: 'small' }, + style: { + '--_topPad': '1px', + '--comp-topPad': `var(${vars.smallTopPad}, var(--_topPad))`, + }, + }, + { + props: ({ ownerState }) => ownerState.multiline, + style: { + paddingTop: 0, + paddingBottom: 0, + }, + }, + ], +}); const Input = React.forwardRef(function Input(inProps, ref) { const props = useDefaultProps({ props: inProps, name: 'MuiInput' }); diff --git a/packages/mui-material/src/Input/index.d.ts b/packages/mui-material/src/Input/index.d.ts index bcb7f9544db7f0..0cc35c54162dac 100644 --- a/packages/mui-material/src/Input/index.d.ts +++ b/packages/mui-material/src/Input/index.d.ts @@ -2,3 +2,5 @@ export { default } from './Input'; export * from './Input'; export { default as inputClasses } from './inputClasses'; export * from './inputClasses'; + +export { private_inputVars } from './inputVars'; diff --git a/packages/mui-material/src/Input/index.js b/packages/mui-material/src/Input/index.js index b32ad177dd8e11..accbd4b400f445 100644 --- a/packages/mui-material/src/Input/index.js +++ b/packages/mui-material/src/Input/index.js @@ -1,3 +1,5 @@ export { default } from './Input'; export { default as inputClasses } from './inputClasses'; export * from './inputClasses'; + +export { private_inputVars } from './inputVars'; diff --git a/packages/mui-material/src/Input/inputVars.ts b/packages/mui-material/src/Input/inputVars.ts new file mode 100644 index 00000000000000..aff87660a0193f --- /dev/null +++ b/packages/mui-material/src/Input/inputVars.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Input (standard variant) density token identities — internal designer knobs + * (`private_*` per the density RFC). The standard input's box is just the + * InputBase baseline input padding (`4px 0 5px`; small `1px 0 5px`). The seam + * lives on the **standard** input slot (not the shared InputBase baseline) so + * the literal stays the zero-diff fallback for Filled/Outlined, which fully + * override it. `topPad` is per size; `bottomPad` is shared (5 both). Inline is 0 + * (not tokenized); the root `marginTop:16` label reserve is typography-derived + * (out of scope). + */ +export const private_inputVars = { + mediumTopPad: '--Input-medium-topPad', + smallTopPad: '--Input-small-topPad', + bottomPad: '--Input-bottomPad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 49ad7375a1d310..e0265a5b3963b7 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -7,6 +7,7 @@ import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedIn import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; +import { private_inputVars as inVars } from '../Input/inputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -100,6 +101,12 @@ export default function enhanceComfortDensity(theme: }, ], }); + addRootOverride(enhanced.components, 'MuiInput', { + // Standard input padding = density steps (tiny; block only, inline stays 0). + [inVars.mediumTopPad]: d.xs, + [inVars.smallTopPad]: d.xxs, + [inVars.bottomPad]: d.xs, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 70d542f20ebb6a..0e38822886dd4d 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -7,6 +7,7 @@ import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedIn import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; +import { private_inputVars as inVars } from '../Input/inputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -100,6 +101,12 @@ export default function enhanceCompactDensity(theme: }, ], }); + addRootOverride(enhanced.components, 'MuiInput', { + // Standard input padding = density steps (tiny; block only, inline stays 0). + [inVars.mediumTopPad]: d.xs, + [inVars.smallTopPad]: d.xxs, + [inVars.bottomPad]: d.xs, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 5e311a03849884..8db68993ab8888 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -7,6 +7,7 @@ import { private_outlinedInputVars as oiVars } from '../OutlinedInput/outlinedIn import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; +import { private_inputVars as inVars } from '../Input/inputVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -102,5 +103,11 @@ export default function enhanceNormalDensity(theme: }, ], }); + addRootOverride(enhanced.components, 'MuiInput', { + // Standard input padding = density steps (tiny; block only, inline stays 0). + [inVars.mediumTopPad]: d.xs, + [inVars.smallTopPad]: d.xxs, + [inVars.bottomPad]: d.xs, + }); return enhanced; } From fcfb8174b7c516fa3a9bc65290a16af09e93453e Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:08:11 +0700 Subject: [PATCH 063/114] density Tabs: tokenize Tab (default + icon+label states) + Tabs root minHeight pairing - Tab default block/inline pad + minHeight; icon+label state reroutes its own block-pad + minHeight tokens (base decls re-resolve via --comp-* seam) - icon gaps split stack (top/bottom) vs inline (start/end); Tabs root minHeight - tabVars + tabsVars; re-export js+d.ts - presets: spacing->steps; minHeights->raw px with MuiTabs.minHeight == MuiTab.minHeight per preset (40/48/56, icon+label 60/72/84) - playground Tabs entry; fixture demo + scopes; Tab icon-gap test -> skipIf(isJsdom) - zero-diff maxDiffPixels:0; reflow + pairing (Tabs.minHeight===Tab.minHeight every preset) verified via getComputedStyle; 300 tests pass --- docs/pages/experiments/density-fixture.tsx | 22 +++++++ docs/pages/experiments/density-playground.tsx | 64 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 22 +++++++ packages/mui-material/src/Tab/Tab.js | 36 ++++++++--- packages/mui-material/src/Tab/Tab.test.js | 4 +- packages/mui-material/src/Tab/index.d.ts | 2 + packages/mui-material/src/Tab/index.js | 2 + packages/mui-material/src/Tab/tabVars.ts | 19 ++++++ packages/mui-material/src/Tabs/Tabs.js | 7 +- packages/mui-material/src/Tabs/index.d.ts | 2 + packages/mui-material/src/Tabs/index.js | 2 + packages/mui-material/src/Tabs/tabsVars.ts | 10 +++ .../src/styles/enhanceComfortDensity.ts | 15 +++++ .../src/styles/enhanceCompactDensity.ts | 15 +++++ .../src/styles/enhanceNormalDensity.ts | 15 +++++ 15 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 packages/mui-material/src/Tab/tabVars.ts create mode 100644 packages/mui-material/src/Tabs/tabsVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 8ca2ac5f7c7576..7b6a130bf5a512 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -104,6 +104,28 @@ const scopes: Record> = { ['--Input-bottomPad' as any]: '12px', }, }, + Tab: { + dense: { + ['--Tab-minHeight' as any]: '40px', + ['--Tabs-minHeight' as any]: '40px', + ['--Tab-iconLabel-minHeight' as any]: '60px', + ['--Tab-blockPad' as any]: '8px', + ['--Tab-iconLabel-blockPad' as any]: '6px', + ['--Tab-inlinePad' as any]: '12px', + ['--Tab-icon-stackGap' as any]: '4px', + ['--Tab-icon-inlineGap' as any]: '6px', + }, + loose: { + ['--Tab-minHeight' as any]: '60px', + ['--Tabs-minHeight' as any]: '60px', + ['--Tab-iconLabel-minHeight' as any]: '88px', + ['--Tab-blockPad' as any]: '16px', + ['--Tab-iconLabel-blockPad' as any]: '12px', + ['--Tab-inlinePad' as any]: '28px', + ['--Tab-icon-stackGap' as any]: '10px', + ['--Tab-icon-inlineGap' as any]: '12px', + }, + }, }; export default function DensityFixture() { diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 5225268ff49de8..3ceadb0f8c8fea 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -22,6 +22,8 @@ import InputAdornment, { private_inputAdornmentVars } from '@mui/material/InputA import { private_outlinedInputVars } from '@mui/material/OutlinedInput'; import { private_filledInputVars } from '@mui/material/FilledInput'; import { private_inputVars } from '@mui/material/Input'; +import Tabs, { private_tabsVars } from '@mui/material/Tabs'; +import Tab, { private_tabVars } from '@mui/material/Tab'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -479,6 +481,54 @@ function InputMatrix({ ); } +// Tabs family: Tab default + icon+label states (block pad + min-height each) + +// shared inline pad + icon gaps (stack/inline), plus the paired Tabs-root +// min-height. Spacing → density keys; min-heights → raw px (read off the theme). +const TAB_FIELDS: DensityField[] = [ + { key: 'minHeight', cssVar: private_tabVars.minHeight }, + { key: 'tabsMinHeight', cssVar: private_tabsVars.minHeight }, + { key: 'iconLabelMinHeight', cssVar: private_tabVars.iconLabelMinHeight }, + { key: 'blockPad', cssVar: private_tabVars.blockPad }, + { key: 'iconLabelBlockPad', cssVar: private_tabVars.iconLabelBlockPad }, + { key: 'inlinePad', cssVar: private_tabVars.inlinePad }, + { key: 'iconStackGap', cssVar: private_tabVars.iconStackGap }, + { key: 'iconInlineGap', cssVar: private_tabVars.iconInlineGap }, +]; + +function TabsMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + // Tokens on each Tabs instance (ancestor of its Tab children, which inherit). + const sx = mappingEnabled + ? Object.fromEntries( + TAB_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + const lbl = (t: string) => {t}; + return ( + + + + + + + + } label={lbl('Top')} iconPosition="top" /> + } label={lbl('Top')} iconPosition="top" /> + + + } label={lbl('Start')} iconPosition="start" /> + } label={lbl('Start')} iconPosition="start" /> + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -548,6 +598,20 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + Tabs: { + canvasLabel: 'Tabs — text / icon-top / icon-start (Tab+Tabs minHeight paired)', + fields: TAB_FIELDS, + // Spacing → density keys; min-heights (minHeight/tabsMinHeight/iconLabelMinHeight) + // read raw px off the theme. + prefill: { + blockPad: 'sm', + iconLabelBlockPad: 'xs', + inlinePad: 'lg', + iconStackGap: 'xs', + iconInlineGap: 'sm', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index dc61ec4189a3fc..0824eca9d1bc02 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -9,6 +9,8 @@ import ListItemText from '@mui/material/ListItemText'; import Tooltip from '@mui/material/Tooltip'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -122,6 +124,26 @@ const demos: Record = { ), + Tab: ( + // Text tabs (default state → block/inline pad + Tab/Tabs minHeight pairing), + // icon-top tabs (icon+label state → iconLabel block/minHeight + stackGap), + // icon-start tabs (inlineGap). + + + + + + + + } label="Top" iconPosition="top" /> + } label="Top" iconPosition="top" /> + + + } label="Start" iconPosition="start" /> + } label="Start" iconPosition="start" /> + + + ), }; export default demos; diff --git a/packages/mui-material/src/Tab/Tab.js b/packages/mui-material/src/Tab/Tab.js index 05ddee7f3a0ba7..6a2cb326ab45da 100644 --- a/packages/mui-material/src/Tab/Tab.js +++ b/packages/mui-material/src/Tab/Tab.js @@ -11,6 +11,7 @@ import { useDefaultProps } from '../DefaultPropsProvider'; import unsupportedProp from '../utils/unsupportedProp'; import { useRovingTabIndexContext, useRovingTabIndexItem } from '../utils/useRovingTabIndex'; import tabClasses, { getTabUtilityClass } from './tabClasses'; +import { private_tabVars as vars } from './tabVars'; const useUtilityClasses = (ownerState) => { const { classes, textColor, fullWidth, wrapped, icon, label, selected, disabled } = ownerState; @@ -51,12 +52,26 @@ const TabRoot = styled(ButtonBase, { })( memoTheme(({ theme }) => ({ ...theme.typography.button, + // Density seams (three-layer). Default state: block/inline pad + min-height. + // Icon gaps split stack (top/bottom) vs inline (start/end). min-height is + // raw px per preset (paired with Tabs); the icon+label variant below swaps + // in its own block pad + min-height defaults + tokens. + '--_blockPad': '12px', + '--_inlinePad': '16px', + '--_minHeight': '48px', + '--_iconStackGap': '6px', + '--_iconInlineGap': theme.spacing(1), + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + '--comp-minHeight': `var(${vars.minHeight}, var(--_minHeight))`, + '--comp-iconStackGap': `var(${vars.iconStackGap}, var(--_iconStackGap))`, + '--comp-iconInlineGap': `var(${vars.iconInlineGap}, var(--_iconInlineGap))`, maxWidth: 360, minWidth: 90, position: 'relative', - minHeight: 48, + minHeight: 'var(--comp-minHeight, var(--_minHeight))', flexShrink: 0, - padding: '12px 16px', + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', overflow: 'hidden', whiteSpace: 'normal', textAlign: 'center', @@ -82,9 +97,12 @@ const TabRoot = styled(ButtonBase, { { props: ({ ownerState }) => ownerState.icon && ownerState.label, style: { - minHeight: 72, - paddingTop: 9, - paddingBottom: 9, + // Second density state: its own min-height + block-pad tokens/defaults; + // the base `min-height`/`padding` declarations re-resolve via the seam. + '--_minHeight': '72px', + '--_blockPad': '9px', + '--comp-minHeight': `var(${vars.iconLabelMinHeight}, var(--_minHeight))`, + '--comp-blockPad': `var(${vars.iconLabelBlockPad}, var(--_blockPad))`, }, }, { @@ -92,7 +110,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'top', style: { [`& > .${tabClasses.icon}`]: { - marginBottom: 6, + marginBottom: 'var(--comp-iconStackGap, var(--_iconStackGap))', }, }, }, @@ -101,7 +119,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'bottom', style: { [`& > .${tabClasses.icon}`]: { - marginTop: 6, + marginTop: 'var(--comp-iconStackGap, var(--_iconStackGap))', }, }, }, @@ -110,7 +128,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'start', style: { [`& > .${tabClasses.icon}`]: { - marginRight: theme.spacing(1), + marginRight: 'var(--comp-iconInlineGap, var(--_iconInlineGap))', }, }, }, @@ -119,7 +137,7 @@ const TabRoot = styled(ButtonBase, { ownerState.icon && ownerState.label && iconPosition === 'end', style: { [`& > .${tabClasses.icon}`]: { - marginLeft: theme.spacing(1), + marginLeft: 'var(--comp-iconInlineGap, var(--_iconInlineGap))', }, }, }, diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 46f5500ebfdc82..2f5b6f0ba41c29 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -191,7 +191,9 @@ describe('', () => { expect(wrapper).to.have.class('test-icon'); }); - it('should have bottom margin when passed together with label', () => { + // The icon gap is now a CSS var (`var(--comp-iconStackGap, …)`); jsdom can't + // resolve custom properties, so this only asserts in a real browser. + it.skipIf(isJsdom())('should have bottom margin when passed together with label', () => { render( } label="foo" /> diff --git a/packages/mui-material/src/Tab/index.d.ts b/packages/mui-material/src/Tab/index.d.ts index dae620b149a0af..89276b6c19a292 100644 --- a/packages/mui-material/src/Tab/index.d.ts +++ b/packages/mui-material/src/Tab/index.d.ts @@ -3,3 +3,5 @@ export * from './Tab'; export { default as tabClasses } from './tabClasses'; export * from './tabClasses'; + +export { private_tabVars } from './tabVars'; diff --git a/packages/mui-material/src/Tab/index.js b/packages/mui-material/src/Tab/index.js index 7c828ba9dd2757..4fd44a5b99bd59 100644 --- a/packages/mui-material/src/Tab/index.js +++ b/packages/mui-material/src/Tab/index.js @@ -2,3 +2,5 @@ export { default } from './Tab'; export { default as tabClasses } from './tabClasses'; export * from './tabClasses'; + +export { private_tabVars } from './tabVars'; diff --git a/packages/mui-material/src/Tab/tabVars.ts b/packages/mui-material/src/Tab/tabVars.ts new file mode 100644 index 00000000000000..35cd21009e98b7 --- /dev/null +++ b/packages/mui-material/src/Tab/tabVars.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Tab density token identities — internal designer knobs (`private_*` per the + * density RFC). Two content-driven states (like MenuItem's `dense`): **default** + * (text/icon-only) and **icon+label** (taller, stacked) — each owns its block + * pad + min-height so a preset can tune them independently; inline pad is shared. + * `minHeight`/`iconLabelMinHeight` are sizing (raw px per preset, paired with + * `Tabs`); the rest are spacing. Icon gaps are split stack (top/bottom) vs + * inline (start/end). Seams (`--comp-*`) + defaults (`--_*`) are the plumbing. + */ +export const private_tabVars = { + minHeight: '--Tab-minHeight', + iconLabelMinHeight: '--Tab-iconLabel-minHeight', + blockPad: '--Tab-blockPad', + iconLabelBlockPad: '--Tab-iconLabel-blockPad', + inlinePad: '--Tab-inlinePad', + iconStackGap: '--Tab-icon-stackGap', + iconInlineGap: '--Tab-icon-inlineGap', +} as const; diff --git a/packages/mui-material/src/Tabs/Tabs.js b/packages/mui-material/src/Tabs/Tabs.js index c9dc851fb0c0f6..d90724542c0c7f 100644 --- a/packages/mui-material/src/Tabs/Tabs.js +++ b/packages/mui-material/src/Tabs/Tabs.js @@ -17,6 +17,7 @@ import ScrollbarSize from './ScrollbarSize'; import TabScrollButton from '../TabScrollButton'; import useEventCallback from '../utils/useEventCallback'; import tabsClasses, { getTabsUtilityClass } from './tabsClasses'; +import { private_tabsVars as vars } from './tabsVars'; import ownerWindow from '../utils/ownerWindow'; import isLayoutSupported from '../utils/isLayoutSupported'; import useSlot from '../utils/useSlot'; @@ -77,7 +78,11 @@ const TabsRoot = styled('div', { })( memoTheme(({ theme }) => ({ overflow: 'hidden', - minHeight: 48, + // Density seam: strip min-height (raw px per preset), paired with Tab's + // default min-height so the strip never mismatches its tabs. + '--_minHeight': '48px', + '--comp-minHeight': `var(${vars.minHeight}, var(--_minHeight))`, + minHeight: 'var(--comp-minHeight, var(--_minHeight))', // Add iOS momentum scrolling for iOS < 13.0 WebkitOverflowScrolling: 'touch', display: 'flex', diff --git a/packages/mui-material/src/Tabs/index.d.ts b/packages/mui-material/src/Tabs/index.d.ts index b2e190bf9420fe..e59ec8df6d03d4 100644 --- a/packages/mui-material/src/Tabs/index.d.ts +++ b/packages/mui-material/src/Tabs/index.d.ts @@ -3,3 +3,5 @@ export * from './Tabs'; export { default as tabsClasses } from './tabsClasses'; export * from './tabsClasses'; + +export { private_tabsVars } from './tabsVars'; diff --git a/packages/mui-material/src/Tabs/index.js b/packages/mui-material/src/Tabs/index.js index c0e4122744acdb..cce8ce715662dc 100644 --- a/packages/mui-material/src/Tabs/index.js +++ b/packages/mui-material/src/Tabs/index.js @@ -2,3 +2,5 @@ export { default } from './Tabs'; export { default as tabsClasses } from './tabsClasses'; export * from './tabsClasses'; + +export { private_tabsVars } from './tabsVars'; diff --git a/packages/mui-material/src/Tabs/tabsVars.ts b/packages/mui-material/src/Tabs/tabsVars.ts new file mode 100644 index 00000000000000..4c30e40acc4e8c --- /dev/null +++ b/packages/mui-material/src/Tabs/tabsVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Tabs (root) density token identity — the strip's `min-height`, sizing (raw px + * per preset). Must reflow to the **same px** as `Tab`'s default `min-height` + * (the pairing) or the strip and its tabs get a visible seam. `private_*` per + * the density RFC. + */ +export const private_tabsVars = { + minHeight: '--Tabs-minHeight', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index e0265a5b3963b7..b362b9c1c9f3a0 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -8,6 +8,8 @@ import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import { private_inputVars as inVars } from '../Input/inputVars'; +import { private_tabVars as tabVars } from '../Tab/tabVars'; +import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -107,6 +109,19 @@ export default function enhanceComfortDensity(theme: [inVars.smallTopPad]: d.xxs, [inVars.bottomPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiTab', { + // Spacing = steps; min-heights = raw px (paired with MuiTabs below). + [tabVars.minHeight]: '56px', + [tabVars.iconLabelMinHeight]: '84px', + [tabVars.blockPad]: d.sm, + [tabVars.iconLabelBlockPad]: d.xs, + [tabVars.inlinePad]: d.lg, + [tabVars.iconStackGap]: d.xs, + [tabVars.iconInlineGap]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiTabs', { + [tabsVars.minHeight]: '56px', // == MuiTab minHeight (the pairing) + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 0e38822886dd4d..8bf50213f7f75a 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -8,6 +8,8 @@ import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import { private_inputVars as inVars } from '../Input/inputVars'; +import { private_tabVars as tabVars } from '../Tab/tabVars'; +import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -107,6 +109,19 @@ export default function enhanceCompactDensity(theme: [inVars.smallTopPad]: d.xxs, [inVars.bottomPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiTab', { + // Spacing = steps; min-heights = raw px (paired with MuiTabs below). + [tabVars.minHeight]: '40px', + [tabVars.iconLabelMinHeight]: '60px', + [tabVars.blockPad]: d.sm, + [tabVars.iconLabelBlockPad]: d.xs, + [tabVars.inlinePad]: d.lg, + [tabVars.iconStackGap]: d.xs, + [tabVars.iconInlineGap]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiTabs', { + [tabsVars.minHeight]: '40px', // == MuiTab minHeight (the pairing) + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 8db68993ab8888..52fa77b9bc8233 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -8,6 +8,8 @@ import { private_inputLabelVars as ilVars } from '../InputLabel/inputLabelVars'; import { private_inputAdornmentVars as iaVars } from '../InputAdornment/inputAdornmentVars'; import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVars'; import { private_inputVars as inVars } from '../Input/inputVars'; +import { private_tabVars as tabVars } from '../Tab/tabVars'; +import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -109,5 +111,18 @@ export default function enhanceNormalDensity(theme: [inVars.smallTopPad]: d.xxs, [inVars.bottomPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiTab', { + // Spacing = steps; min-heights = raw px (paired with MuiTabs below). + [tabVars.minHeight]: '48px', + [tabVars.iconLabelMinHeight]: '72px', + [tabVars.blockPad]: d.sm, + [tabVars.iconLabelBlockPad]: d.xs, + [tabVars.inlinePad]: d.lg, + [tabVars.iconStackGap]: d.xs, + [tabVars.iconInlineGap]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiTabs', { + [tabsVars.minHeight]: '48px', // == MuiTab minHeight (the pairing) + }); return enhanced; } From afda32e4673afccb81d1baa49fa9a6e6a5da5559 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:15:02 +0700 Subject: [PATCH 064/114] density Checkbox: tokenize SwitchBase hit-area padding, route per-size from Checkbox - SwitchBase padding: 9 -> var(--comp-pad, var(--_pad)) (--_pad 9px); shared, so Radio/Switch fall back to 9px unrouted - Checkbox routes per-size public token into --comp-pad via size medium/small variants - checkboxVars (mediumPad/smallPad); re-export js+d.ts; presets map mediumPad->sm, smallPad->xs - playground Checkbox entry; fixture demo + scopes - zero-diff maxDiffPixels:0 (incl. Switch/Radio unaffected); reflow via getComputedStyle; 245 tests pass --- docs/pages/experiments/density-fixture.tsx | 10 +++++ docs/pages/experiments/density-playground.tsx | 38 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 8 ++++ .../mui-material/src/Checkbox/Checkbox.js | 10 +++++ .../mui-material/src/Checkbox/checkboxVars.ts | 11 ++++++ packages/mui-material/src/Checkbox/index.d.ts | 2 + packages/mui-material/src/Checkbox/index.js | 2 + .../mui-material/src/internal/SwitchBase.js | 6 ++- .../src/styles/enhanceComfortDensity.ts | 6 +++ .../src/styles/enhanceCompactDensity.ts | 6 +++ .../src/styles/enhanceNormalDensity.ts | 6 +++ 11 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/mui-material/src/Checkbox/checkboxVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 7b6a130bf5a512..31f8796c65530d 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -104,6 +104,16 @@ const scopes: Record> = { ['--Input-bottomPad' as any]: '12px', }, }, + Checkbox: { + dense: { + ['--Checkbox-medium-pad' as any]: '4px', + ['--Checkbox-small-pad' as any]: '2px', + }, + loose: { + ['--Checkbox-medium-pad' as any]: '14px', + ['--Checkbox-small-pad' as any]: '12px', + }, + }, Tab: { dense: { ['--Tab-minHeight' as any]: '40px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 3ceadb0f8c8fea..41b05b5b6e8cd8 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -24,6 +24,7 @@ import { private_filledInputVars } from '@mui/material/FilledInput'; import { private_inputVars } from '@mui/material/Input'; import Tabs, { private_tabsVars } from '@mui/material/Tabs'; import Tab, { private_tabVars } from '@mui/material/Tab'; +import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -529,6 +530,37 @@ function TabsMatrix({ ); } +// Checkbox family: the touch-target padding around the icon, per size (via +// SwitchBase). All spacing → density keys. +const CHECKBOX_FIELDS: DensityField[] = [ + { key: 'mediumPad', cssVar: private_checkboxVars.mediumPad }, + { key: 'smallPad', cssVar: private_checkboxVars.smallPad }, +]; + +function CheckboxMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + CHECKBOX_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -612,6 +644,12 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + Checkbox: { + canvasLabel: 'Checkbox — touch-target padding (medium + small)', + fields: CHECKBOX_FIELDS, + prefill: { mediumPad: 'sm', smallPad: 'xs' }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 0824eca9d1bc02..8bf15ce3d1a325 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -11,6 +11,7 @@ import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; +import Checkbox from '@mui/material/Checkbox'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -144,6 +145,13 @@ const demos: Record = { ), + Checkbox: ( + // Touch-target padding around the icon (medium + small). + + + + + ), }; export default demos; diff --git a/packages/mui-material/src/Checkbox/Checkbox.js b/packages/mui-material/src/Checkbox/Checkbox.js index ef4cbfc2c328f7..95f4115c631999 100644 --- a/packages/mui-material/src/Checkbox/Checkbox.js +++ b/packages/mui-material/src/Checkbox/Checkbox.js @@ -10,6 +10,7 @@ import IndeterminateCheckBoxIcon from '../internal/svg-icons/IndeterminateCheckB import capitalize from '../utils/capitalize'; import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import checkboxClasses, { getCheckboxUtilityClass } from './checkboxClasses'; +import { private_checkboxVars as vars } from './checkboxVars'; import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; @@ -55,6 +56,15 @@ const CheckboxRoot = styled(SwitchBase, { memoTheme(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, variants: [ + { + // Route the per-size touch-target padding token into SwitchBase's seam. + props: { size: 'medium' }, + style: { '--comp-pad': `var(${vars.mediumPad}, var(--_pad))` }, + }, + { + props: { size: 'small' }, + style: { '--comp-pad': `var(${vars.smallPad}, var(--_pad))` }, + }, { props: { color: 'default', disableRipple: false }, style: { diff --git a/packages/mui-material/src/Checkbox/checkboxVars.ts b/packages/mui-material/src/Checkbox/checkboxVars.ts new file mode 100644 index 00000000000000..e6f4ed20ad4dde --- /dev/null +++ b/packages/mui-material/src/Checkbox/checkboxVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Checkbox density token identities — internal designer knobs (`private_*` per + * the density RFC). The touch-target `padding` (9px both sizes today) lives on + * the shared `SwitchBase`; Checkbox routes its per-size public token into the + * agnostic seam (`--comp-pad`) over the `9px` default. `private_*`. + */ +export const private_checkboxVars = { + mediumPad: '--Checkbox-medium-pad', + smallPad: '--Checkbox-small-pad', +} as const; diff --git a/packages/mui-material/src/Checkbox/index.d.ts b/packages/mui-material/src/Checkbox/index.d.ts index 1f8f3d3996f4cf..9fbb47943a11b8 100644 --- a/packages/mui-material/src/Checkbox/index.d.ts +++ b/packages/mui-material/src/Checkbox/index.d.ts @@ -3,3 +3,5 @@ export * from './Checkbox'; export { default as checkboxClasses } from './checkboxClasses'; export * from './checkboxClasses'; + +export { private_checkboxVars } from './checkboxVars'; diff --git a/packages/mui-material/src/Checkbox/index.js b/packages/mui-material/src/Checkbox/index.js index 55883a5457b284..378efe82b502a6 100644 --- a/packages/mui-material/src/Checkbox/index.js +++ b/packages/mui-material/src/Checkbox/index.js @@ -2,3 +2,5 @@ export { default } from './Checkbox'; export { default as checkboxClasses } from './checkboxClasses'; export * from './checkboxClasses'; + +export { private_checkboxVars } from './checkboxVars'; diff --git a/packages/mui-material/src/internal/SwitchBase.js b/packages/mui-material/src/internal/SwitchBase.js index 5257bfe688e49e..dcff0970863778 100644 --- a/packages/mui-material/src/internal/SwitchBase.js +++ b/packages/mui-material/src/internal/SwitchBase.js @@ -25,7 +25,11 @@ const useUtilityClasses = (ownerState) => { const SwitchBaseRoot = styled(ButtonBase, { name: 'MuiSwitchBase', })({ - padding: 9, + // Density seam: the touch-target padding over the `9px` default. The concrete + // layer (Checkbox/Radio) routes its per-size public token into `--comp-pad`; + // unrouted (e.g. Switch) falls back to `9px`. + '--_pad': '9px', + padding: 'var(--comp-pad, var(--_pad))', borderRadius: '50%', variants: [ { diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index b362b9c1c9f3a0..eede2d2dae34bf 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -10,6 +10,7 @@ import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVar import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; +import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -122,6 +123,11 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiTabs', { [tabsVars.minHeight]: '56px', // == MuiTab minHeight (the pairing) }); + addRootOverride(enhanced.components, 'MuiCheckbox', { + // Touch-target padding (9px both sizes today) = density steps. + [cbVars.mediumPad]: d.sm, + [cbVars.smallPad]: d.xs, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 8bf50213f7f75a..da798047d17af5 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -10,6 +10,7 @@ import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVar import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; +import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -122,6 +123,11 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiTabs', { [tabsVars.minHeight]: '40px', // == MuiTab minHeight (the pairing) }); + addRootOverride(enhanced.components, 'MuiCheckbox', { + // Touch-target padding (9px both sizes today) = density steps. + [cbVars.mediumPad]: d.sm, + [cbVars.smallPad]: d.xs, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 52fa77b9bc8233..1aeca50bad6e03 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -10,6 +10,7 @@ import { private_filledInputVars as fiVars } from '../FilledInput/filledInputVar import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; +import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -124,5 +125,10 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiTabs', { [tabsVars.minHeight]: '48px', // == MuiTab minHeight (the pairing) }); + addRootOverride(enhanced.components, 'MuiCheckbox', { + // Touch-target padding (9px both sizes today) = density steps. + [cbVars.mediumPad]: d.sm, + [cbVars.smallPad]: d.xs, + }); return enhanced; } From ecf7a3537c2116ddd9d210bc37b778cd206ceb8c Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:20:29 +0700 Subject: [PATCH 065/114] density CardContent: tokenize body padding + last-child bottom padding - CardContent padding + &:last-child paddingBottom over --comp-* seams (no size axis) - cardContentVars (pad/padBottom); re-export js+d.ts; presets map pad->lg, padBottom->xl - playground CardContent entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle; normal == today (16/24) --- docs/pages/experiments/density-fixture.tsx | 10 +++++ docs/pages/experiments/density-playground.tsx | 44 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 13 ++++++ .../src/CardContent/CardContent.js | 10 ++++- .../src/CardContent/cardContentVars.ts | 10 +++++ .../mui-material/src/CardContent/index.d.ts | 2 + .../mui-material/src/CardContent/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 6 +++ .../src/styles/enhanceCompactDensity.ts | 6 +++ .../src/styles/enhanceNormalDensity.ts | 6 +++ 10 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/CardContent/cardContentVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 31f8796c65530d..4e691919b75181 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -114,6 +114,16 @@ const scopes: Record> = { ['--Checkbox-small-pad' as any]: '12px', }, }, + CardContent: { + dense: { + ['--CardContent-pad' as any]: '8px', + ['--CardContent-padBottom' as any]: '12px', + }, + loose: { + ['--CardContent-pad' as any]: '32px', + ['--CardContent-padBottom' as any]: '40px', + }, + }, Tab: { dense: { ['--Tab-minHeight' as any]: '40px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 41b05b5b6e8cd8..6e98ac0d09c7a7 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -25,6 +25,8 @@ import { private_inputVars } from '@mui/material/Input'; import Tabs, { private_tabsVars } from '@mui/material/Tabs'; import Tab, { private_tabVars } from '@mui/material/Tab'; import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; +import Card from '@mui/material/Card'; +import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -561,6 +563,42 @@ function CheckboxMatrix({ ); } +// CardContent family: base padding + last-child bottom padding (no size axis). +const CARD_CONTENT_FIELDS: DensityField[] = [ + { key: 'pad', cssVar: private_cardContentVars.pad }, + { key: 'padBottom', cssVar: private_cardContentVars.padBottom }, +]; + +function CardContentMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + CARD_CONTENT_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + + Card title + + + Body content with last-child bottom padding. + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -650,6 +688,12 @@ const COMPONENT_DEFS = { prefill: { mediumPad: 'sm', smallPad: 'xs' }, renderMatrix: (args) => , }, + CardContent: { + canvasLabel: 'CardContent — padding + last-child bottom padding', + fields: CARD_CONTENT_FIELDS, + prefill: { pad: 'lg', padBottom: 'xl' }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 8bf15ce3d1a325..a31ff63bc9d958 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -12,6 +12,9 @@ import InputAdornment from '@mui/material/InputAdornment'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Checkbox from '@mui/material/Checkbox'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -152,6 +155,16 @@ const demos: Record = { ), + CardContent: ( + + + Card title + + Body content with the last-child bottom padding. + + + + ), }; export default demos; diff --git a/packages/mui-material/src/CardContent/CardContent.js b/packages/mui-material/src/CardContent/CardContent.js index 769163a78edbad..34f37b868b9b57 100644 --- a/packages/mui-material/src/CardContent/CardContent.js +++ b/packages/mui-material/src/CardContent/CardContent.js @@ -6,6 +6,7 @@ import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getCardContentUtilityClass } from './cardContentClasses'; +import { private_cardContentVars as vars } from './cardContentVars'; const useUtilityClasses = (ownerState) => { const { classes } = ownerState; @@ -21,9 +22,14 @@ const CardContentRoot = styled('div', { name: 'MuiCardContent', slot: 'Root', })({ - padding: 16, + // Density seams over the literal defaults (no size axis). + '--_pad': '16px', + '--_padBottom': '24px', + '--comp-pad': `var(${vars.pad}, var(--_pad))`, + '--comp-padBottom': `var(${vars.padBottom}, var(--_padBottom))`, + padding: 'var(--comp-pad, var(--_pad))', '&:last-child': { - paddingBottom: 24, + paddingBottom: 'var(--comp-padBottom, var(--_padBottom))', }, }); diff --git a/packages/mui-material/src/CardContent/cardContentVars.ts b/packages/mui-material/src/CardContent/cardContentVars.ts new file mode 100644 index 00000000000000..aff10f4acb2771 --- /dev/null +++ b/packages/mui-material/src/CardContent/cardContentVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * CardContent density token identities — internal designer knobs (`private_*` + * per the density RFC). No size axis: base padding + the larger last-child + * bottom padding, over the agnostic `--comp-*` seams. + */ +export const private_cardContentVars = { + pad: '--CardContent-pad', + padBottom: '--CardContent-padBottom', +} as const; diff --git a/packages/mui-material/src/CardContent/index.d.ts b/packages/mui-material/src/CardContent/index.d.ts index 2338e721c55f9c..a455614e398e1a 100644 --- a/packages/mui-material/src/CardContent/index.d.ts +++ b/packages/mui-material/src/CardContent/index.d.ts @@ -3,3 +3,5 @@ export * from './CardContent'; export { default as cardContentClasses } from './cardContentClasses'; export * from './cardContentClasses'; + +export { private_cardContentVars } from './cardContentVars'; diff --git a/packages/mui-material/src/CardContent/index.js b/packages/mui-material/src/CardContent/index.js index 3ee40db9b57671..9ea3c886c86faa 100644 --- a/packages/mui-material/src/CardContent/index.js +++ b/packages/mui-material/src/CardContent/index.js @@ -2,3 +2,5 @@ export { default } from './CardContent'; export { default as cardContentClasses } from './cardContentClasses'; export * from './cardContentClasses'; + +export { private_cardContentVars } from './cardContentVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index eede2d2dae34bf..4a8654e0104b5c 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -11,6 +11,7 @@ import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; +import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -128,6 +129,11 @@ export default function enhanceComfortDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiCardContent', { + // No size axis: base pad + larger last-child bottom pad. + [ccVars.pad]: d.lg, + [ccVars.padBottom]: d.xl, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index da798047d17af5..2cfe8274edfce3 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -11,6 +11,7 @@ import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; +import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -128,6 +129,11 @@ export default function enhanceCompactDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiCardContent', { + // No size axis: base pad + larger last-child bottom pad. + [ccVars.pad]: d.lg, + [ccVars.padBottom]: d.xl, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 1aeca50bad6e03..859523a477fb1e 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -11,6 +11,7 @@ import { private_inputVars as inVars } from '../Input/inputVars'; import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; +import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -130,5 +131,10 @@ export default function enhanceNormalDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiCardContent', { + // No size axis: base pad + larger last-child bottom pad. + [ccVars.pad]: d.lg, + [ccVars.padBottom]: d.xl, + }); return enhanced; } From 83e50512f8e81004a0cda3ec7a017b6c2ea881aa Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:29:55 +0700 Subject: [PATCH 066/114] density Select: tokenize content-box minHeight floor (raw px per preset) - SelectInput minHeight 1.4375em -> var(--comp-minHeight, var(--_minHeight)); selectVars (minHeight); re-export js+d.ts - presets map minHeight to raw px (20/23/28); floor is typographic + mostly inert (visible density comes from the input variant) - playground Select entry; fixture demo + scopes; dedupe playground Select import - zero-diff maxDiffPixels:0; 336 Select tests pass --- docs/pages/experiments/density-fixture.tsx | 4 ++ docs/pages/experiments/density-playground.tsx | 37 ++++++++++++++++++- docs/src/modules/components/densityDemos.tsx | 12 ++++++ .../mui-material/src/Select/SelectInput.js | 6 ++- packages/mui-material/src/Select/index.d.ts | 2 + packages/mui-material/src/Select/index.js | 2 + .../mui-material/src/Select/selectVars.ts | 10 +++++ .../src/styles/enhanceComfortDensity.ts | 5 +++ .../src/styles/enhanceCompactDensity.ts | 5 +++ .../src/styles/enhanceNormalDensity.ts | 5 +++ 10 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/Select/selectVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 4e691919b75181..f60d4f8fd422d9 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -124,6 +124,10 @@ const scopes: Record> = { ['--CardContent-padBottom' as any]: '40px', }, }, + Select: { + dense: { ['--Select-minHeight' as any]: '16px' }, + loose: { ['--Select-minHeight' as any]: '40px' }, + }, Tab: { dense: { ['--Tab-minHeight' as any]: '40px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 6e98ac0d09c7a7..bf6a367365666d 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -9,7 +9,6 @@ import RadioGroup from '@mui/material/RadioGroup'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import FormControlLabel from '@mui/material/FormControlLabel'; -import Select from '@mui/material/Select'; import MenuItem, { private_menuItemVars } from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; import Menu from '@mui/material/Menu'; @@ -27,6 +26,8 @@ import Tab, { private_tabVars } from '@mui/material/Tab'; import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; +import Select, { private_selectVars } from '@mui/material/Select'; +import InputLabel from '@mui/material/InputLabel'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -599,6 +600,34 @@ function CardContentMatrix({ ); } +// Select family: the content-box min-height floor (raw px). The visible density +// mostly comes from the underlying OutlinedInput padding (tokenized separately). +const SELECT_FIELDS: DensityField[] = [{ key: 'minHeight', cssVar: private_selectVars.minHeight }]; + +function SelectMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + SELECT_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + Age + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -694,6 +723,12 @@ const COMPONENT_DEFS = { prefill: { pad: 'lg', padBottom: 'xl' }, renderMatrix: (args) => , }, + Select: { + canvasLabel: 'Select — content-box floor (padding via its OutlinedInput)', + fields: SELECT_FIELDS, + prefill: {}, // minHeight = raw px, read off the theme + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index a31ff63bc9d958..ad600741e60a4a 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -15,6 +15,9 @@ import Checkbox from '@mui/material/Checkbox'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -155,6 +158,15 @@ const demos: Record = { ), + Select: ( + + Age + + + ), CardContent: ( diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index d56648e4afb3d2..7dcef4210481a4 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -18,6 +18,7 @@ import useEventCallback from '../utils/useEventCallback'; import useForkRef from '../utils/useForkRef'; import useControlled from '../utils/useControlled'; import selectClasses, { getSelectUtilityClasses } from './selectClasses'; +import { private_selectVars as vars } from './selectVars'; import { areEqualValues, isEmpty, getOpenInteractionType } from './utils'; import { canCycleRepeatedCharacter, @@ -88,8 +89,11 @@ const SelectSelect = styled(StyledSelectSelect, { })({ // Win specificity over the input base [`&.${selectClasses.select}`]: { + // Density seam over the `1.4375em` line-height floor (raw px per preset). + '--_minHeight': '1.4375em', + '--comp-minHeight': `var(${vars.minHeight}, var(--_minHeight))`, height: 'auto', // Resets for multiple select with chips - minHeight: '1.4375em', // Required for select\text-field height consistency + minHeight: 'var(--comp-minHeight, var(--_minHeight))', // Required for select\text-field height consistency textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', diff --git a/packages/mui-material/src/Select/index.d.ts b/packages/mui-material/src/Select/index.d.ts index 017c33214ac9e0..568f9640fc8acd 100644 --- a/packages/mui-material/src/Select/index.d.ts +++ b/packages/mui-material/src/Select/index.d.ts @@ -4,3 +4,5 @@ export * from './utils'; export { default as selectClasses } from './selectClasses'; export * from './selectClasses'; + +export { private_selectVars } from './selectVars'; diff --git a/packages/mui-material/src/Select/index.js b/packages/mui-material/src/Select/index.js index f4c81c0fa31bae..504eab4ab85100 100644 --- a/packages/mui-material/src/Select/index.js +++ b/packages/mui-material/src/Select/index.js @@ -3,3 +3,5 @@ export * from './utils'; export { default as selectClasses } from './selectClasses'; export * from './selectClasses'; + +export { private_selectVars } from './selectVars'; diff --git a/packages/mui-material/src/Select/selectVars.ts b/packages/mui-material/src/Select/selectVars.ts new file mode 100644 index 00000000000000..75b37dd7f8815a --- /dev/null +++ b/packages/mui-material/src/Select/selectVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Select density token identity — the content-box `min-height` floor (`1.4375em` + * today, the text line-height). A single agnostic seam (no size layer). Sizing → + * raw px per preset; the Select's real padding density comes from its input + * variant (OutlinedInput/FilledInput/Input, all tokenized). `private_*`. + */ +export const private_selectVars = { + minHeight: '--Select-minHeight', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 4a8654e0104b5c..590a992244e511 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -12,6 +12,7 @@ import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; +import { private_selectVars as selVars } from '../Select/selectVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -134,6 +135,10 @@ export default function enhanceComfortDensity(theme: [ccVars.pad]: d.lg, [ccVars.padBottom]: d.xl, }); + addRootOverride(enhanced.components, 'MuiSelect', { + // Content-box floor (raw px); real padding comes from the input variant. + [selVars.minHeight]: '28px', + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 2cfe8274edfce3..9ff295fa8bd6b1 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -12,6 +12,7 @@ import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; +import { private_selectVars as selVars } from '../Select/selectVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -134,6 +135,10 @@ export default function enhanceCompactDensity(theme: [ccVars.pad]: d.lg, [ccVars.padBottom]: d.xl, }); + addRootOverride(enhanced.components, 'MuiSelect', { + // Content-box floor (raw px); real padding comes from the input variant. + [selVars.minHeight]: '20px', + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 859523a477fb1e..81961ae54eb5a9 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -12,6 +12,7 @@ import { private_tabVars as tabVars } from '../Tab/tabVars'; import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; +import { private_selectVars as selVars } from '../Select/selectVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -136,5 +137,9 @@ export default function enhanceNormalDensity(theme: [ccVars.pad]: d.lg, [ccVars.padBottom]: d.xl, }); + addRootOverride(enhanced.components, 'MuiSelect', { + // Content-box floor (raw px); real padding comes from the input variant. + [selVars.minHeight]: '23px', + }); return enhanced; } From 2d46444f16f828ddd0145e125055543b9fa0ba73 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:36:35 +0700 Subject: [PATCH 067/114] density Alert: tokenize root padding + icon gap (from scratch) - Alert root block/inline padding + AlertIcon marginRight (--comp-iconGap) over --comp-* seams (no size axis) - alertVars (blockPad/inlinePad/iconGap); re-export js+d.ts; presets map blockPad->xs, inlinePad->lg, iconGap->md - icon/message vertical alignment paddings left literal (row alignment, not density) - playground Alert entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle; normal == today (6/16/12); 128 tests pass --- docs/pages/experiments/density-fixture.tsx | 12 ++++++ docs/pages/experiments/density-playground.tsx | 39 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 9 +++++ packages/mui-material/src/Alert/Alert.js | 14 ++++++- packages/mui-material/src/Alert/alertVars.ts | 12 ++++++ packages/mui-material/src/Alert/index.d.ts | 2 + packages/mui-material/src/Alert/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 7 ++++ .../src/styles/enhanceCompactDensity.ts | 7 ++++ .../src/styles/enhanceNormalDensity.ts | 7 ++++ 10 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/Alert/alertVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index f60d4f8fd422d9..b7fa95db5a45d0 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Select-minHeight' as any]: '16px' }, loose: { ['--Select-minHeight' as any]: '40px' }, }, + Alert: { + dense: { + ['--Alert-blockPad' as any]: '2px', + ['--Alert-inlinePad' as any]: '10px', + ['--Alert-iconGap' as any]: '6px', + }, + loose: { + ['--Alert-blockPad' as any]: '14px', + ['--Alert-inlinePad' as any]: '28px', + ['--Alert-iconGap' as any]: '20px', + }, + }, Tab: { dense: { ['--Tab-minHeight' as any]: '40px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index bf6a367365666d..8f6679155df8f2 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -28,6 +28,7 @@ import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; import Select, { private_selectVars } from '@mui/material/Select'; import InputLabel from '@mui/material/InputLabel'; +import Alert, { private_alertVars } from '@mui/material/Alert'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -628,6 +629,38 @@ function SelectMatrix({ ); } +// Alert family: root block/inline padding + icon→message gap (no size axis). +const ALERT_FIELDS: DensityField[] = [ + { key: 'blockPad', cssVar: private_alertVars.blockPad }, + { key: 'inlinePad', cssVar: private_alertVars.inlinePad }, + { key: 'iconGap', cssVar: private_alertVars.iconGap }, +]; + +function AlertMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + ALERT_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + Info alert — icon gap + root padding. + + {}} sx={sx}> + Success alert with a close action. + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -729,6 +762,12 @@ const COMPONENT_DEFS = { prefill: {}, // minHeight = raw px, read off the theme renderMatrix: (args) => , }, + Alert: { + canvasLabel: 'Alert — root padding + icon gap', + fields: ALERT_FIELDS, + prefill: { blockPad: 'xs', inlinePad: 'lg', iconGap: 'md' }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index ad600741e60a4a..1e6fc247798d81 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -18,6 +18,7 @@ import Typography from '@mui/material/Typography'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import Select from '@mui/material/Select'; +import Alert from '@mui/material/Alert'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -158,6 +159,14 @@ const demos: Record = { ), + Alert: ( + + Info alert — icon gap + root padding. + {}}> + Success alert with a close action. + + + ), Select: ( Age diff --git a/packages/mui-material/src/Alert/Alert.js b/packages/mui-material/src/Alert/Alert.js index 5f407b2f5d2e65..0a5331084f0d41 100644 --- a/packages/mui-material/src/Alert/Alert.js +++ b/packages/mui-material/src/Alert/Alert.js @@ -11,6 +11,7 @@ import capitalize from '../utils/capitalize'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import Paper from '../Paper'; import alertClasses, { getAlertUtilityClass } from './alertClasses'; +import { private_alertVars as vars } from './alertVars'; import IconButton from '../IconButton'; import SuccessOutlinedIcon from '../internal/svg-icons/SuccessOutlined'; import ReportProblemOutlinedIcon from '../internal/svg-icons/ReportProblemOutlined'; @@ -45,9 +46,17 @@ const AlertRoot = styled(Paper, { const getBackgroundColor = theme.palette.mode === 'light' ? theme.lighten : theme.darken; return { ...theme.typography.body2, + // Density seams (no size axis): root block/inline padding + the icon gap + // (`--comp-iconGap`, consumed by the Icon slot child below). + '--_blockPad': '6px', + '--_inlinePad': '16px', + '--_iconGap': '12px', + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + '--comp-iconGap': `var(${vars.iconGap}, var(--_iconGap))`, backgroundColor: 'transparent', display: 'flex', - padding: '6px 16px', + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', variants: [ ...Object.entries(theme.palette) .filter(createSimplePaletteValueFilter(['light'])) @@ -112,7 +121,8 @@ const AlertIcon = styled('div', { name: 'MuiAlert', slot: 'Icon', })({ - marginRight: 12, + // Icon→message gap (inherits `--comp-iconGap` from the Alert root). + marginRight: 'var(--comp-iconGap, var(--_iconGap))', padding: '7px 0', display: 'flex', fontSize: 22, diff --git a/packages/mui-material/src/Alert/alertVars.ts b/packages/mui-material/src/Alert/alertVars.ts new file mode 100644 index 00000000000000..03857ab3e9c9b3 --- /dev/null +++ b/packages/mui-material/src/Alert/alertVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Alert density token identities — internal designer knobs (`private_*` per the + * density RFC). No size axis: root block/inline padding + the icon→message gap, + * over the agnostic `--comp-*` seams. The icon/message vertical alignment + * paddings (`7px`/`8px`) stay literal (internal row alignment, not density). + */ +export const private_alertVars = { + blockPad: '--Alert-blockPad', + inlinePad: '--Alert-inlinePad', + iconGap: '--Alert-iconGap', +} as const; diff --git a/packages/mui-material/src/Alert/index.d.ts b/packages/mui-material/src/Alert/index.d.ts index 1543777bf847c1..469bc80b54de3c 100644 --- a/packages/mui-material/src/Alert/index.d.ts +++ b/packages/mui-material/src/Alert/index.d.ts @@ -3,3 +3,5 @@ export * from './Alert'; export { default as alertClasses } from './alertClasses'; export * from './alertClasses'; + +export { private_alertVars } from './alertVars'; diff --git a/packages/mui-material/src/Alert/index.js b/packages/mui-material/src/Alert/index.js index ae354db3f6bcac..f630b81e0e4dc8 100644 --- a/packages/mui-material/src/Alert/index.js +++ b/packages/mui-material/src/Alert/index.js @@ -2,3 +2,5 @@ export { default } from './Alert'; export { default as alertClasses } from './alertClasses'; export * from './alertClasses'; + +export { private_alertVars } from './alertVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 590a992244e511..308f1b30098020 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -13,6 +13,7 @@ import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; +import { private_alertVars as alertVars } from '../Alert/alertVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -139,6 +140,12 @@ export default function enhanceComfortDensity(theme: // Content-box floor (raw px); real padding comes from the input variant. [selVars.minHeight]: '28px', }); + addRootOverride(enhanced.components, 'MuiAlert', { + // No size axis: root padding + icon gap (spacing steps). + [alertVars.blockPad]: d.xs, + [alertVars.inlinePad]: d.lg, + [alertVars.iconGap]: d.md, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 9ff295fa8bd6b1..06efdc3f48bafd 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -13,6 +13,7 @@ import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; +import { private_alertVars as alertVars } from '../Alert/alertVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -139,6 +140,12 @@ export default function enhanceCompactDensity(theme: // Content-box floor (raw px); real padding comes from the input variant. [selVars.minHeight]: '20px', }); + addRootOverride(enhanced.components, 'MuiAlert', { + // No size axis: root padding + icon gap (spacing steps). + [alertVars.blockPad]: d.xs, + [alertVars.inlinePad]: d.lg, + [alertVars.iconGap]: d.md, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 81961ae54eb5a9..fad70a6da3fd90 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -13,6 +13,7 @@ import { private_tabsVars as tabsVars } from '../Tabs/tabsVars'; import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; +import { private_alertVars as alertVars } from '../Alert/alertVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -141,5 +142,11 @@ export default function enhanceNormalDensity(theme: // Content-box floor (raw px); real padding comes from the input variant. [selVars.minHeight]: '23px', }); + addRootOverride(enhanced.components, 'MuiAlert', { + // No size axis: root padding + icon gap (spacing steps). + [alertVars.blockPad]: d.xs, + [alertVars.inlinePad]: d.lg, + [alertVars.iconGap]: d.md, + }); return enhanced; } From 361d6d350da520ca86782a5b9daa24c2b29571b0 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:44:35 +0700 Subject: [PATCH 068/114] density Chip: tokenize height (drives avatar/icon via calc) + label inline padding - Chip height per size (--comp-height, raw px); avatar/icon/deleteIcon derive via calc(height - inset) so they scale with height - label inline padding per size (--comp-padInline) with per-(variant,size) --_padInline defaults + size routing - chipVars (mediumHeight/smallHeight/mediumPadInline/smallPadInline); re-export js+d.ts - presets map height->raw px (28/32/36, 20/24/28), padInline->md/sm - playground Chip entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle (avatar tracks height); normal == today; 162 tests pass --- docs/pages/experiments/density-fixture.tsx | 14 +++++ docs/pages/experiments/density-playground.tsx | 41 ++++++++++++ docs/src/modules/components/densityDemos.tsx | 12 ++++ packages/mui-material/src/Chip/Chip.js | 63 ++++++++++++------- packages/mui-material/src/Chip/chipVars.ts | 14 +++++ packages/mui-material/src/Chip/index.d.ts | 2 + packages/mui-material/src/Chip/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 8 +++ .../src/styles/enhanceCompactDensity.ts | 8 +++ .../src/styles/enhanceNormalDensity.ts | 8 +++ 10 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 packages/mui-material/src/Chip/chipVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index b7fa95db5a45d0..8005e3df48a380 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,20 @@ const scopes: Record> = { dense: { ['--Select-minHeight' as any]: '16px' }, loose: { ['--Select-minHeight' as any]: '40px' }, }, + Chip: { + dense: { + ['--Chip-medium-height' as any]: '26px', + ['--Chip-small-height' as any]: '20px', + ['--Chip-medium-padInline' as any]: '8px', + ['--Chip-small-padInline' as any]: '6px', + }, + loose: { + ['--Chip-medium-height' as any]: '40px', + ['--Chip-small-height' as any]: '32px', + ['--Chip-medium-padInline' as any]: '16px', + ['--Chip-small-padInline' as any]: '12px', + }, + }, Alert: { dense: { ['--Alert-blockPad' as any]: '2px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 8f6679155df8f2..d6e8eb941c9df5 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -29,6 +29,8 @@ import CardContent, { private_cardContentVars } from '@mui/material/CardContent' import Select, { private_selectVars } from '@mui/material/Select'; import InputLabel from '@mui/material/InputLabel'; import Alert, { private_alertVars } from '@mui/material/Alert'; +import Chip, { private_chipVars } from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -661,6 +663,39 @@ function AlertMatrix({ ); } +// Chip family: height (per size — drives avatar/icon/deleteIcon via calc) + +// label inline padding (per size). Height = raw px; padInline = density keys. +const CHIP_FIELDS: DensityField[] = [ + { key: 'mediumHeight', cssVar: private_chipVars.mediumHeight }, + { key: 'smallHeight', cssVar: private_chipVars.smallHeight }, + { key: 'mediumPadInline', cssVar: private_chipVars.mediumPadInline }, + { key: 'smallPadInline', cssVar: private_chipVars.smallPadInline }, +]; + +function ChipMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + // Chip tokens on a wrapping Box (ancestor); each Chip inherits them. + const sx = mappingEnabled + ? Object.fromEntries( + CHIP_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + A} label="Avatar" /> + } label="Icon" onDelete={() => {}} /> + {}} /> + {}} /> + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -768,6 +803,12 @@ const COMPONENT_DEFS = { prefill: { blockPad: 'xs', inlinePad: 'lg', iconGap: 'md' }, renderMatrix: (args) => , }, + Chip: { + canvasLabel: 'Chip — height (drives avatar/icon) + label inline padding', + fields: CHIP_FIELDS, + prefill: { mediumPadInline: 'md', smallPadInline: 'sm' }, // heights = raw px, read off theme + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 1e6fc247798d81..f7ba6dead5a34d 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -19,6 +19,8 @@ import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import Select from '@mui/material/Select'; import Alert from '@mui/material/Alert'; +import Chip from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; import InboxIcon from '@mui/icons-material/Inbox'; // Force the tooltip open + inline (no portal) so it renders inside @@ -159,6 +161,16 @@ const demos: Record = { ), + Chip: ( + + + A} label="Avatar" /> + } label="Icon" onDelete={() => {}} /> + {}} /> + {}} /> + + + ), Alert: ( Info alert — icon gap + root padding. diff --git a/packages/mui-material/src/Chip/Chip.js b/packages/mui-material/src/Chip/Chip.js index 2f35de5dfc8d04..159efa6ad4bc19 100644 --- a/packages/mui-material/src/Chip/Chip.js +++ b/packages/mui-material/src/Chip/Chip.js @@ -14,6 +14,7 @@ import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFil import { useDefaultProps } from '../DefaultPropsProvider'; import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import chipClasses, { getChipUtilityClass } from './chipClasses'; +import { private_chipVars as vars } from './chipVars'; import useSlot from '../utils/useSlot'; import { getTransitionStyles } from '../transitions/utils'; @@ -73,7 +74,11 @@ const ChipRoot = styled('div', { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - height: 32, + // Density seam: height drives avatar/icon/deleteIcon via `calc(height - + // inset)` (the insets reproduce today's medium dims). Size variants route + // the per-size public token over `--_height`. + '--_height': '32px', + height: 'var(--comp-height, var(--_height))', lineHeight: 1.5, color: (theme.vars || theme).palette.text.primary, backgroundColor: (theme.vars || theme).palette.action.selected, @@ -96,19 +101,20 @@ const ChipRoot = styled('div', { [`& .${chipClasses.avatar}`]: { marginLeft: 5, marginRight: -6, - width: 24, - height: 24, + width: 'calc(var(--comp-height, var(--_height)) - 8px)', + height: 'calc(var(--comp-height, var(--_height)) - 8px)', color: theme.vars ? theme.vars.palette.Chip.defaultAvatarColor : textColor, fontSize: theme.typography.pxToRem(12), }, [`& .${chipClasses.icon}`]: { marginLeft: 5, marginRight: -6, + fontSize: 'calc(var(--comp-height, var(--_height)) - 8px)', }, [`& .${chipClasses.deleteIcon}`]: { WebkitTapHighlightColor: 'transparent', color: theme.alpha((theme.vars || theme).palette.text.primary, 0.26), - fontSize: 22, + fontSize: 'calc(var(--comp-height, var(--_height)) - 10px)', cursor: 'pointer', margin: '0 5px 0 -6px', '&:hover': { @@ -116,6 +122,15 @@ const ChipRoot = styled('div', { }, }, variants: [ + { + // Route the per-size public height token over the internal default. + props: { size: 'small' }, + style: { '--comp-height': `var(${vars.smallHeight}, var(--_height))` }, + }, + { + props: { size: 'medium' }, + style: { '--comp-height': `var(${vars.mediumHeight}, var(--_height))` }, + }, { props: { color: 'primary', @@ -141,21 +156,21 @@ const ChipRoot = styled('div', { { props: { size: 'small' }, style: { - height: 24, + '--_height': '24px', // small default; medium default lives in base [`& .${chipClasses.avatar}`]: { marginLeft: 4, marginRight: -4, - width: 18, - height: 18, + width: 'calc(var(--comp-height, var(--_height)) - 6px)', + height: 'calc(var(--comp-height, var(--_height)) - 6px)', fontSize: theme.typography.pxToRem(10), }, [`& .${chipClasses.icon}`]: { - fontSize: 18, + fontSize: 'calc(var(--comp-height, var(--_height)) - 6px)', marginLeft: 4, marginRight: -4, }, [`& .${chipClasses.deleteIcon}`]: { - fontSize: 16, + fontSize: 'calc(var(--comp-height, var(--_height)) - 8px)', marginRight: 4, marginLeft: -4, }, @@ -328,30 +343,32 @@ const ChipLabel = styled('span', { })({ overflow: 'hidden', textOverflow: 'ellipsis', - paddingLeft: 12, - paddingRight: 12, + // Density seam: inline padding over the per-(variant,size) default; size + // variants route the per-size public token, variant/size specialize `--_*`. + '--_padInline': '12px', + paddingLeft: 'var(--comp-padInline, var(--_padInline))', + paddingRight: 'var(--comp-padInline, var(--_padInline))', whiteSpace: 'nowrap', variants: [ + { + props: { size: 'small' }, + style: { '--comp-padInline': `var(${vars.smallPadInline}, var(--_padInline))` }, + }, + { + props: { size: 'medium' }, + style: { '--comp-padInline': `var(${vars.mediumPadInline}, var(--_padInline))` }, + }, { props: { variant: 'outlined' }, - style: { - paddingLeft: 11, - paddingRight: 11, - }, + style: { '--_padInline': '11px' }, // medium outlined default }, { props: { size: 'small' }, - style: { - paddingLeft: 8, - paddingRight: 8, - }, + style: { '--_padInline': '8px' }, // small filled default }, { props: { size: 'small', variant: 'outlined' }, - style: { - paddingLeft: 7, - paddingRight: 7, - }, + style: { '--_padInline': '7px' }, // small outlined default }, ], }); diff --git a/packages/mui-material/src/Chip/chipVars.ts b/packages/mui-material/src/Chip/chipVars.ts new file mode 100644 index 00000000000000..4dd63deb01eb82 --- /dev/null +++ b/packages/mui-material/src/Chip/chipVars.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Chip density token identities — internal designer knobs (`private_*` per the + * density RFC). The primary lever is **height** (per size, raw px): the + * avatar/icon/deleteIcon dims all derive from it via `calc(height - inset)`, so + * they scale together. The label's inline padding is the spacing lever (per + * size). Seams (`--comp-*`) + defaults (`--_*`) are the plumbing. + */ +export const private_chipVars = { + mediumHeight: '--Chip-medium-height', + smallHeight: '--Chip-small-height', + mediumPadInline: '--Chip-medium-padInline', + smallPadInline: '--Chip-small-padInline', +} as const; diff --git a/packages/mui-material/src/Chip/index.d.ts b/packages/mui-material/src/Chip/index.d.ts index 142aa252697363..f8cefa5a8833bd 100644 --- a/packages/mui-material/src/Chip/index.d.ts +++ b/packages/mui-material/src/Chip/index.d.ts @@ -3,3 +3,5 @@ export * from './Chip'; export { default as chipClasses } from './chipClasses'; export * from './chipClasses'; + +export { private_chipVars } from './chipVars'; diff --git a/packages/mui-material/src/Chip/index.js b/packages/mui-material/src/Chip/index.js index 5c7f16c51acddf..040363e82f361a 100644 --- a/packages/mui-material/src/Chip/index.js +++ b/packages/mui-material/src/Chip/index.js @@ -2,3 +2,5 @@ export { default } from './Chip'; export { default as chipClasses } from './chipClasses'; export * from './chipClasses'; + +export { private_chipVars } from './chipVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 308f1b30098020..25c77b73515ea5 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -14,6 +14,7 @@ import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; +import { private_chipVars as chipVars } from '../Chip/chipVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -146,6 +147,13 @@ export default function enhanceComfortDensity(theme: [alertVars.inlinePad]: d.lg, [alertVars.iconGap]: d.md, }); + addRootOverride(enhanced.components, 'MuiChip', { + // Height = raw px (drives avatar/icon/deleteIcon via calc); label padInline = steps. + [chipVars.mediumHeight]: '36px', + [chipVars.smallHeight]: '28px', + [chipVars.mediumPadInline]: d.md, + [chipVars.smallPadInline]: d.sm, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 06efdc3f48bafd..4abb74292807ed 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -14,6 +14,7 @@ import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; +import { private_chipVars as chipVars } from '../Chip/chipVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -146,6 +147,13 @@ export default function enhanceCompactDensity(theme: [alertVars.inlinePad]: d.lg, [alertVars.iconGap]: d.md, }); + addRootOverride(enhanced.components, 'MuiChip', { + // Height = raw px (drives avatar/icon/deleteIcon via calc); label padInline = steps. + [chipVars.mediumHeight]: '28px', + [chipVars.smallHeight]: '20px', + [chipVars.mediumPadInline]: d.md, + [chipVars.smallPadInline]: d.sm, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index fad70a6da3fd90..234d146677d703 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -14,6 +14,7 @@ import { private_checkboxVars as cbVars } from '../Checkbox/checkboxVars'; import { private_cardContentVars as ccVars } from '../CardContent/cardContentVars'; import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; +import { private_chipVars as chipVars } from '../Chip/chipVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -148,5 +149,12 @@ export default function enhanceNormalDensity(theme: [alertVars.inlinePad]: d.lg, [alertVars.iconGap]: d.md, }); + addRootOverride(enhanced.components, 'MuiChip', { + // Height = raw px (drives avatar/icon/deleteIcon via calc); label padInline = steps. + [chipVars.mediumHeight]: '32px', + [chipVars.smallHeight]: '24px', + [chipVars.mediumPadInline]: d.md, + [chipVars.smallPadInline]: d.sm, + }); return enhanced; } From e632d855fed094c5551bb3355d93ae066b5f3cca Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:52:13 +0700 Subject: [PATCH 069/114] density Accordion: tokenize summary min-height/margin/pad + details padding - AccordionSummary collapsed/expanded minHeight (raw px) + inline pad + content block margin (reduces with minHeight so it doesn't bind the header) - AccordionDetails top/inline/bottom padding - accordionSummaryVars + accordionDetailsVars; re-export js+d.ts - presets: minHeights raw px (40/48/56, 52/64/76); spacing steps (inlinePad/marginBlock/details) - playground Accordion entry; fixture demo + scopes - zero-diff maxDiffPixels:0; reflow via getComputedStyle; normal == today (48/64, 8/16/16); 91 tests pass --- docs/pages/experiments/density-fixture.tsx | 22 ++++++ docs/pages/experiments/density-playground.tsx | 69 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 16 +++++ .../src/AccordionDetails/AccordionDetails.js | 13 +++- .../AccordionDetails/accordionDetailsVars.ts | 11 +++ .../src/AccordionDetails/index.d.ts | 2 + .../src/AccordionDetails/index.js | 2 + .../src/AccordionSummary/AccordionSummary.js | 24 +++++-- .../AccordionSummary/accordionSummaryVars.ts | 15 ++++ .../src/AccordionSummary/index.d.ts | 2 + .../src/AccordionSummary/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 15 ++++ .../src/styles/enhanceCompactDensity.ts | 15 ++++ .../src/styles/enhanceNormalDensity.ts | 15 ++++ 14 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 packages/mui-material/src/AccordionDetails/accordionDetailsVars.ts create mode 100644 packages/mui-material/src/AccordionSummary/accordionSummaryVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 8005e3df48a380..292eb87a1e6a86 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,28 @@ const scopes: Record> = { dense: { ['--Select-minHeight' as any]: '16px' }, loose: { ['--Select-minHeight' as any]: '40px' }, }, + Accordion: { + dense: { + ['--AccordionSummary-minHeight' as any]: '40px', + ['--AccordionSummary-expandedMinHeight' as any]: '48px', + ['--AccordionSummary-inlinePad' as any]: '12px', + ['--AccordionSummary-marginBlock' as any]: '8px', + ['--AccordionSummary-expandedMarginBlock' as any]: '12px', + ['--AccordionDetails-topPad' as any]: '4px', + ['--AccordionDetails-inlinePad' as any]: '12px', + ['--AccordionDetails-bottomPad' as any]: '12px', + }, + loose: { + ['--AccordionSummary-minHeight' as any]: '64px', + ['--AccordionSummary-expandedMinHeight' as any]: '88px', + ['--AccordionSummary-inlinePad' as any]: '28px', + ['--AccordionSummary-marginBlock' as any]: '20px', + ['--AccordionSummary-expandedMarginBlock' as any]: '28px', + ['--AccordionDetails-topPad' as any]: '16px', + ['--AccordionDetails-inlinePad' as any]: '28px', + ['--AccordionDetails-bottomPad' as any]: '28px', + }, + }, Chip: { dense: { ['--Chip-medium-height' as any]: '26px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index d6e8eb941c9df5..465cdb7a89fc39 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -31,6 +31,10 @@ import InputLabel from '@mui/material/InputLabel'; import Alert, { private_alertVars } from '@mui/material/Alert'; import Chip, { private_chipVars } from '@mui/material/Chip'; import Avatar from '@mui/material/Avatar'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary, { private_accordionSummaryVars } from '@mui/material/AccordionSummary'; +import AccordionDetails, { private_accordionDetailsVars } from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; import ToggleButton from '@mui/material/ToggleButton'; @@ -696,6 +700,57 @@ function ChipMatrix({ ); } +// Accordion family: Summary collapsed/expanded min-height + inline pad + +// content block margin; Details top/inline/bottom padding. min-heights = raw px, +// the rest = density keys. +const ACCORDION_FIELDS: DensityField[] = [ + { key: 'minHeight', cssVar: private_accordionSummaryVars.minHeight }, + { key: 'expandedMinHeight', cssVar: private_accordionSummaryVars.expandedMinHeight }, + { key: 'inlinePad', cssVar: private_accordionSummaryVars.inlinePad }, + { key: 'marginBlock', cssVar: private_accordionSummaryVars.marginBlock }, + { key: 'expandedMarginBlock', cssVar: private_accordionSummaryVars.expandedMarginBlock }, + { key: 'detailsTopPad', cssVar: private_accordionDetailsVars.topPad }, + { key: 'detailsInlinePad', cssVar: private_accordionDetailsVars.inlinePad }, + { key: 'detailsBottomPad', cssVar: private_accordionDetailsVars.bottomPad }, +]; + +function AccordionMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + // Tokens on the Accordion (ancestor of Summary + Details, which inherit). + const sx = mappingEnabled + ? Object.fromEntries( + ACCORDION_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + }> + Expanded summary + + + Details content padding. + + + + }> + Collapsed summary + + Hidden. + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -809,6 +864,20 @@ const COMPONENT_DEFS = { prefill: { mediumPadInline: 'md', smallPadInline: 'sm' }, // heights = raw px, read off theme renderMatrix: (args) => , }, + Accordion: { + canvasLabel: 'Accordion — summary min-height/margin/pad + details padding', + fields: ACCORDION_FIELDS, + // Spacing → density keys; min-heights read raw px off the theme. + prefill: { + inlinePad: 'lg', + marginBlock: 'md', + expandedMarginBlock: 'lg', + detailsTopPad: 'sm', + detailsInlinePad: 'lg', + detailsBottomPad: 'lg', + }, + renderMatrix: (args) => , + }, } satisfies Record; type ComponentName = keyof typeof COMPONENT_DEFS; diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index f7ba6dead5a34d..58474c770c2ad0 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -21,7 +21,11 @@ import Select from '@mui/material/Select'; import Alert from '@mui/material/Alert'; import Chip from '@mui/material/Chip'; import Avatar from '@mui/material/Avatar'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; import InboxIcon from '@mui/icons-material/Inbox'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; // Force the tooltip open + inline (no portal) so it renders inside // `#density-scope` and lands in the scoped screenshot; snap the transition so @@ -161,6 +165,18 @@ const demos: Record = { ), + Accordion: ( + + + }>Expanded summary + Details content with top/inline/bottom padding. + + + }>Collapsed summary + Hidden details. + + + ), Chip: ( diff --git a/packages/mui-material/src/AccordionDetails/AccordionDetails.js b/packages/mui-material/src/AccordionDetails/AccordionDetails.js index cad8744a3350fb..5f9a7e9cd6d4d8 100644 --- a/packages/mui-material/src/AccordionDetails/AccordionDetails.js +++ b/packages/mui-material/src/AccordionDetails/AccordionDetails.js @@ -7,6 +7,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getAccordionDetailsUtilityClass } from './accordionDetailsClasses'; +import { private_accordionDetailsVars as vars } from './accordionDetailsVars'; const useUtilityClasses = (ownerState) => { const { classes } = ownerState; @@ -22,8 +23,16 @@ const AccordionDetailsRoot = styled('div', { name: 'MuiAccordionDetails', slot: 'Root', })( - memoTheme(({ theme }) => ({ - padding: theme.spacing(1, 2, 2), + memoTheme(() => ({ + // Density seams over the `8px 16px 16px` default (top/inline/bottom). + '--_topPad': '8px', + '--_inlinePad': '16px', + '--_bottomPad': '16px', + '--comp-topPad': `var(${vars.topPad}, var(--_topPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + '--comp-bottomPad': `var(${vars.bottomPad}, var(--_bottomPad))`, + padding: + 'var(--comp-topPad, var(--_topPad)) var(--comp-inlinePad, var(--_inlinePad)) var(--comp-bottomPad, var(--_bottomPad))', })), ); diff --git a/packages/mui-material/src/AccordionDetails/accordionDetailsVars.ts b/packages/mui-material/src/AccordionDetails/accordionDetailsVars.ts new file mode 100644 index 00000000000000..05c18d4abc2d88 --- /dev/null +++ b/packages/mui-material/src/AccordionDetails/accordionDetailsVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * AccordionDetails density token identities — the content padding (top / inline + * / bottom differ: `8px 16px 16px` today), over the agnostic `--comp-*` seams. + * No size axis. `private_*` per the density RFC. + */ +export const private_accordionDetailsVars = { + topPad: '--AccordionDetails-topPad', + inlinePad: '--AccordionDetails-inlinePad', + bottomPad: '--AccordionDetails-bottomPad', +} as const; diff --git a/packages/mui-material/src/AccordionDetails/index.d.ts b/packages/mui-material/src/AccordionDetails/index.d.ts index acaaa74bf57dc4..cb25b7e793b431 100644 --- a/packages/mui-material/src/AccordionDetails/index.d.ts +++ b/packages/mui-material/src/AccordionDetails/index.d.ts @@ -3,3 +3,5 @@ export * from './AccordionDetails'; export { default as accordionDetailsClasses } from './accordionDetailsClasses'; export * from './accordionDetailsClasses'; + +export { private_accordionDetailsVars } from './accordionDetailsVars'; diff --git a/packages/mui-material/src/AccordionDetails/index.js b/packages/mui-material/src/AccordionDetails/index.js index c08755bfc6b29a..4401c11ffd6ceb 100644 --- a/packages/mui-material/src/AccordionDetails/index.js +++ b/packages/mui-material/src/AccordionDetails/index.js @@ -2,3 +2,5 @@ export { default } from './AccordionDetails'; export { default as accordionDetailsClasses } from './accordionDetailsClasses'; export * from './accordionDetailsClasses'; + +export { private_accordionDetailsVars } from './accordionDetailsVars'; diff --git a/packages/mui-material/src/AccordionSummary/AccordionSummary.js b/packages/mui-material/src/AccordionSummary/AccordionSummary.js index 88b3aeb750c66b..57021fa95db454 100644 --- a/packages/mui-material/src/AccordionSummary/AccordionSummary.js +++ b/packages/mui-material/src/AccordionSummary/AccordionSummary.js @@ -12,6 +12,7 @@ import { getTransitionStyles } from '../transitions/utils'; import accordionSummaryClasses, { getAccordionSummaryUtilityClass, } from './accordionSummaryClasses'; +import { private_accordionSummaryVars as vars } from './accordionSummaryVars'; import useSlot from '../utils/useSlot'; const useUtilityClasses = (ownerState) => { @@ -34,8 +35,15 @@ const AccordionSummaryRoot = styled(ButtonBase, { memoTheme(({ theme }) => ({ display: 'flex', width: '100%', - minHeight: 48, - padding: theme.spacing(0, 2), + // Density seams: collapsed/expanded min-height (raw px) + inline padding. + '--_minHeight': '48px', + '--_expandedMinHeight': '64px', + '--_inlinePad': '16px', + '--comp-minHeight': `var(${vars.minHeight}, var(--_minHeight))`, + '--comp-expandedMinHeight': `var(${vars.expandedMinHeight}, var(--_expandedMinHeight))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + minHeight: 'var(--comp-minHeight, var(--_minHeight))', + padding: '0 var(--comp-inlinePad, var(--_inlinePad))', ...getTransitionStyles(theme, ['min-height', 'background-color'], { duration: theme.transitions.duration.shortest, }), @@ -53,7 +61,7 @@ const AccordionSummaryRoot = styled(ButtonBase, { props: (props) => !props.disableGutters, style: { [`&.${accordionSummaryClasses.expanded}`]: { - minHeight: 64, + minHeight: 'var(--comp-expandedMinHeight, var(--_expandedMinHeight))', }, }, }, @@ -69,7 +77,13 @@ const AccordionSummaryContent = styled('span', { display: 'flex', textAlign: 'start', flexGrow: 1, - margin: '12px 0', + // Content block margin (inherits the seams from the summary root). Must + // reduce with min-height or it binds the header height instead. + '--_marginBlock': '12px', + '--_expandedMarginBlock': '20px', + '--comp-marginBlock': `var(${vars.marginBlock}, var(--_marginBlock))`, + '--comp-expandedMarginBlock': `var(${vars.expandedMarginBlock}, var(--_expandedMarginBlock))`, + margin: 'var(--comp-marginBlock, var(--_marginBlock)) 0', variants: [ { props: (props) => !props.disableGutters, @@ -78,7 +92,7 @@ const AccordionSummaryContent = styled('span', { duration: theme.transitions.duration.shortest, }), [`&.${accordionSummaryClasses.expanded}`]: { - margin: '20px 0', + margin: 'var(--comp-expandedMarginBlock, var(--_expandedMarginBlock)) 0', }, }, }, diff --git a/packages/mui-material/src/AccordionSummary/accordionSummaryVars.ts b/packages/mui-material/src/AccordionSummary/accordionSummaryVars.ts new file mode 100644 index 00000000000000..5e76fdb2b33f6e --- /dev/null +++ b/packages/mui-material/src/AccordionSummary/accordionSummaryVars.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * AccordionSummary density token identities — internal designer knobs + * (`private_*` per the density RFC). The header's collapsed/expanded min-height + * (sizing → raw px) + inline padding + the content's block margin (spacing). + * The content margin must reduce alongside min-height, else it (not min-height) + * binds the header height. Seams (`--comp-*`) + defaults (`--_*`) are plumbing. + */ +export const private_accordionSummaryVars = { + minHeight: '--AccordionSummary-minHeight', + expandedMinHeight: '--AccordionSummary-expandedMinHeight', + inlinePad: '--AccordionSummary-inlinePad', + marginBlock: '--AccordionSummary-marginBlock', + expandedMarginBlock: '--AccordionSummary-expandedMarginBlock', +} as const; diff --git a/packages/mui-material/src/AccordionSummary/index.d.ts b/packages/mui-material/src/AccordionSummary/index.d.ts index 4c9950cd0868c9..dcd4f08bef7208 100644 --- a/packages/mui-material/src/AccordionSummary/index.d.ts +++ b/packages/mui-material/src/AccordionSummary/index.d.ts @@ -3,3 +3,5 @@ export * from './AccordionSummary'; export { default as accordionSummaryClasses } from './accordionSummaryClasses'; export * from './accordionSummaryClasses'; + +export { private_accordionSummaryVars } from './accordionSummaryVars'; diff --git a/packages/mui-material/src/AccordionSummary/index.js b/packages/mui-material/src/AccordionSummary/index.js index cc296ecd83e54e..0e441c017f9595 100644 --- a/packages/mui-material/src/AccordionSummary/index.js +++ b/packages/mui-material/src/AccordionSummary/index.js @@ -2,3 +2,5 @@ export { default } from './AccordionSummary'; export { default as accordionSummaryClasses } from './accordionSummaryClasses'; export * from './accordionSummaryClasses'; + +export { private_accordionSummaryVars } from './accordionSummaryVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 25c77b73515ea5..0f41772d4e7687 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -15,6 +15,8 @@ import { private_cardContentVars as ccVars } from '../CardContent/cardContentVar import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; +import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; +import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -154,6 +156,19 @@ export default function enhanceComfortDensity(theme: [chipVars.mediumPadInline]: d.md, [chipVars.smallPadInline]: d.sm, }); + addRootOverride(enhanced.components, 'MuiAccordionSummary', { + // min-heights raw px; inline pad + content block margin = steps. + [asVars.minHeight]: '56px', + [asVars.expandedMinHeight]: '76px', + [asVars.inlinePad]: d.lg, + [asVars.marginBlock]: d.md, + [asVars.expandedMarginBlock]: d.lg, + }); + addRootOverride(enhanced.components, 'MuiAccordionDetails', { + [adVars.topPad]: d.sm, + [adVars.inlinePad]: d.lg, + [adVars.bottomPad]: d.lg, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.9375rem', lineHeight: 2 }, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 4abb74292807ed..cb2595c6a385b2 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -15,6 +15,8 @@ import { private_cardContentVars as ccVars } from '../CardContent/cardContentVar import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; +import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; +import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -154,6 +156,19 @@ export default function enhanceCompactDensity(theme: [chipVars.mediumPadInline]: d.md, [chipVars.smallPadInline]: d.sm, }); + addRootOverride(enhanced.components, 'MuiAccordionSummary', { + // min-heights raw px; inline pad + content block margin = steps. + [asVars.minHeight]: '40px', + [asVars.expandedMinHeight]: '52px', + [asVars.inlinePad]: d.lg, + [asVars.marginBlock]: d.md, + [asVars.expandedMarginBlock]: d.lg, + }); + addRootOverride(enhanced.components, 'MuiAccordionDetails', { + [adVars.topPad]: d.sm, + [adVars.inlinePad]: d.lg, + [adVars.bottomPad]: d.lg, + }); enhanced.typography = { ...enhanced.typography, button: { ...enhanced.typography?.button, fontSize: '0.8125rem', lineHeight: 1.5 }, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 234d146677d703..727f2dfcfbb198 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -15,6 +15,8 @@ import { private_cardContentVars as ccVars } from '../CardContent/cardContentVar import { private_selectVars as selVars } from '../Select/selectVars'; import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; +import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; +import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -156,5 +158,18 @@ export default function enhanceNormalDensity(theme: [chipVars.mediumPadInline]: d.md, [chipVars.smallPadInline]: d.sm, }); + addRootOverride(enhanced.components, 'MuiAccordionSummary', { + // min-heights raw px; inline pad + content block margin = steps. + [asVars.minHeight]: '48px', + [asVars.expandedMinHeight]: '64px', + [asVars.inlinePad]: d.lg, + [asVars.marginBlock]: d.md, + [asVars.expandedMarginBlock]: d.lg, + }); + addRootOverride(enhanced.components, 'MuiAccordionDetails', { + [adVars.topPad]: d.sm, + [adVars.inlinePad]: d.lg, + [adVars.bottomPad]: d.lg, + }); return enhanced; } From c682b889b5b6e6cbfbd7b4da9d8ddd082f8ae1ec Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 1 Jul 2026 23:58:05 +0700 Subject: [PATCH 070/114] density Radio: route per-size touch-target padding into SwitchBase seam (mirrors Checkbox) - Radio size medium/small variants route --comp-pad; radioVars (mediumPad/smallPad); re-export js+d.ts - presets map mediumPad->sm, smallPad->xs - playground Radio entry (dedupe Radio import); fixture demo + scopes - zero-diff maxDiffPixels:0; reflow identical to Checkbox --- docs/pages/experiments/density-fixture.tsx | 10 ++++++ docs/pages/experiments/density-playground.tsx | 35 ++++++++++++++++++- docs/src/modules/components/densityDemos.tsx | 7 ++++ packages/mui-material/src/Radio/Radio.js | 10 ++++++ packages/mui-material/src/Radio/index.d.ts | 2 ++ packages/mui-material/src/Radio/index.js | 2 ++ packages/mui-material/src/Radio/radioVars.ts | 10 ++++++ .../src/styles/enhanceComfortDensity.ts | 6 ++++ .../src/styles/enhanceCompactDensity.ts | 6 ++++ .../src/styles/enhanceNormalDensity.ts | 6 ++++ 10 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 packages/mui-material/src/Radio/radioVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 292eb87a1e6a86..d435a17e60aefc 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -114,6 +114,16 @@ const scopes: Record> = { ['--Checkbox-small-pad' as any]: '12px', }, }, + Radio: { + dense: { + ['--Radio-medium-pad' as any]: '4px', + ['--Radio-small-pad' as any]: '2px', + }, + loose: { + ['--Radio-medium-pad' as any]: '14px', + ['--Radio-small-pad' as any]: '12px', + }, + }, CardContent: { dense: { ['--CardContent-pad' as any]: '8px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 465cdb7a89fc39..736c84c91e5f3b 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -4,7 +4,6 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button, { private_buttonVars } from '@mui/material/Button'; import CssBaseline from '@mui/material/CssBaseline'; -import Radio from '@mui/material/Radio'; import RadioGroup from '@mui/material/RadioGroup'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; @@ -24,6 +23,7 @@ import { private_inputVars } from '@mui/material/Input'; import Tabs, { private_tabsVars } from '@mui/material/Tabs'; import Tab, { private_tabVars } from '@mui/material/Tab'; import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; +import Radio, { private_radioVars } from '@mui/material/Radio'; import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; import Select, { private_selectVars } from '@mui/material/Select'; @@ -751,6 +751,33 @@ function AccordionMatrix({ ); } +// Radio family: touch-target padding per size (via SwitchBase, like Checkbox). +const RADIO_FIELDS: DensityField[] = [ + { key: 'mediumPad', cssVar: private_radioVars.mediumPad }, + { key: 'smallPad', cssVar: private_radioVars.smallPad }, +]; + +function RadioMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + RADIO_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -840,6 +867,12 @@ const COMPONENT_DEFS = { prefill: { mediumPad: 'sm', smallPad: 'xs' }, renderMatrix: (args) => , }, + Radio: { + canvasLabel: 'Radio — touch-target padding (medium + small)', + fields: RADIO_FIELDS, + prefill: { mediumPad: 'sm', smallPad: 'xs' }, + renderMatrix: (args) => , + }, CardContent: { canvasLabel: 'CardContent — padding + last-child bottom padding', fields: CARD_CONTENT_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 58474c770c2ad0..b08a2de99baf69 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -12,6 +12,7 @@ import InputAdornment from '@mui/material/InputAdornment'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Checkbox from '@mui/material/Checkbox'; +import Radio from '@mui/material/Radio'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; @@ -165,6 +166,12 @@ const demos: Record = { ), + Radio: ( + + + + + ), Accordion: ( diff --git a/packages/mui-material/src/Radio/Radio.js b/packages/mui-material/src/Radio/Radio.js index 48e1d7fd57df59..420632cf3c3cc9 100644 --- a/packages/mui-material/src/Radio/Radio.js +++ b/packages/mui-material/src/Radio/Radio.js @@ -10,6 +10,7 @@ import createChainedFunction from '../utils/createChainedFunction'; import useFormControl from '../FormControl/useFormControl'; import useRadioGroup from '../RadioGroup/useRadioGroup'; import radioClasses, { getRadioUtilityClass } from './radioClasses'; +import { private_radioVars as vars } from './radioVars'; import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; @@ -50,6 +51,15 @@ const RadioRoot = styled(SwitchBase, { color: (theme.vars || theme).palette.action.disabled, }, variants: [ + { + // Route the per-size touch-target padding token into SwitchBase's seam. + props: { size: 'medium' }, + style: { '--comp-pad': `var(${vars.mediumPad}, var(--_pad))` }, + }, + { + props: { size: 'small' }, + style: { '--comp-pad': `var(${vars.smallPad}, var(--_pad))` }, + }, { props: { color: 'default', disabled: false, disableRipple: false }, style: { diff --git a/packages/mui-material/src/Radio/index.d.ts b/packages/mui-material/src/Radio/index.d.ts index e3e9d1bea1a73e..9eecc452675521 100644 --- a/packages/mui-material/src/Radio/index.d.ts +++ b/packages/mui-material/src/Radio/index.d.ts @@ -3,3 +3,5 @@ export * from './Radio'; export { default as radioClasses } from './radioClasses'; export * from './radioClasses'; + +export { private_radioVars } from './radioVars'; diff --git a/packages/mui-material/src/Radio/index.js b/packages/mui-material/src/Radio/index.js index 659d29e9fd37fb..f0967c3a5c0c44 100644 --- a/packages/mui-material/src/Radio/index.js +++ b/packages/mui-material/src/Radio/index.js @@ -2,3 +2,5 @@ export { default } from './Radio'; export { default as radioClasses } from './radioClasses'; export * from './radioClasses'; + +export { private_radioVars } from './radioVars'; diff --git a/packages/mui-material/src/Radio/radioVars.ts b/packages/mui-material/src/Radio/radioVars.ts new file mode 100644 index 00000000000000..2988b548afd534 --- /dev/null +++ b/packages/mui-material/src/Radio/radioVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Radio density token identities — the touch-target `padding` around the icon + * (9px both sizes today), routed into SwitchBase's `--comp-pad` seam per size. + * Mirrors Checkbox. `private_*` per the density RFC. + */ +export const private_radioVars = { + mediumPad: '--Radio-medium-pad', + smallPad: '--Radio-small-pad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 0f41772d4e7687..380bc2fa149195 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -17,6 +17,7 @@ import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; +import { private_radioVars as radioVars } from '../Radio/radioVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -134,6 +135,11 @@ export default function enhanceComfortDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiRadio', { + // Touch-target padding via SwitchBase (mirrors Checkbox). + [radioVars.mediumPad]: d.sm, + [radioVars.smallPad]: d.xs, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index cb2595c6a385b2..b3da555f0aa99e 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -17,6 +17,7 @@ import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; +import { private_radioVars as radioVars } from '../Radio/radioVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -134,6 +135,11 @@ export default function enhanceCompactDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiRadio', { + // Touch-target padding via SwitchBase (mirrors Checkbox). + [radioVars.mediumPad]: d.sm, + [radioVars.smallPad]: d.xs, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 727f2dfcfbb198..bf5b28bb238959 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -17,6 +17,7 @@ import { private_alertVars as alertVars } from '../Alert/alertVars'; import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; +import { private_radioVars as radioVars } from '../Radio/radioVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -136,6 +137,11 @@ export default function enhanceNormalDensity(theme: [cbVars.mediumPad]: d.sm, [cbVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiRadio', { + // Touch-target padding via SwitchBase (mirrors Checkbox). + [radioVars.mediumPad]: d.sm, + [radioVars.smallPad]: d.xs, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 3d7087d5cba36e3d05b627a1eda32e17f54a0003 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 00:03:01 +0700 Subject: [PATCH 071/114] density Breadcrumbs: tokenize separator inline gap - separator marginLeft/Right 8 -> var(--comp-separatorGap, var(--_separatorGap)); breadcrumbsVars (separatorGap); re-export js+d.ts - presets map separatorGap->sm (normal 8 == today) - playground Breadcrumbs entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 4 ++ docs/pages/experiments/density-playground.tsx | 40 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 13 ++++++ .../src/Breadcrumbs/Breadcrumbs.js | 8 +++- .../src/Breadcrumbs/breadcrumbsVars.ts | 9 +++++ .../mui-material/src/Breadcrumbs/index.d.ts | 2 + .../mui-material/src/Breadcrumbs/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 4 ++ .../src/styles/enhanceCompactDensity.ts | 4 ++ .../src/styles/enhanceNormalDensity.ts | 4 ++ 10 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/Breadcrumbs/breadcrumbsVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index d435a17e60aefc..e91b741d1080d5 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -124,6 +124,10 @@ const scopes: Record> = { ['--Radio-small-pad' as any]: '12px', }, }, + Breadcrumbs: { + dense: { ['--Breadcrumbs-separatorGap' as any]: '4px' }, + loose: { ['--Breadcrumbs-separatorGap' as any]: '16px' }, + }, CardContent: { dense: { ['--CardContent-pad' as any]: '8px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 736c84c91e5f3b..e302299bee17b9 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -24,6 +24,8 @@ import Tabs, { private_tabsVars } from '@mui/material/Tabs'; import Tab, { private_tabVars } from '@mui/material/Tab'; import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; import Radio, { private_radioVars } from '@mui/material/Radio'; +import Breadcrumbs, { private_breadcrumbsVars } from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; import Select, { private_selectVars } from '@mui/material/Select'; @@ -778,6 +780,38 @@ function RadioMatrix({ ); } +// Breadcrumbs family: the separator inline gap (single token, no size axis). +const BREADCRUMBS_FIELDS: DensityField[] = [ + { key: 'separatorGap', cssVar: private_breadcrumbsVars.separatorGap }, +]; + +function BreadcrumbsMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = + mappingEnabled && active('separatorGap') + ? { [private_breadcrumbsVars.separatorGap]: resolveValue(mapping.separatorGap) } + : undefined; + return ( + + + Home + + + Catalog + + + Current + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -873,6 +907,12 @@ const COMPONENT_DEFS = { prefill: { mediumPad: 'sm', smallPad: 'xs' }, renderMatrix: (args) => , }, + Breadcrumbs: { + canvasLabel: 'Breadcrumbs — separator inline gap', + fields: BREADCRUMBS_FIELDS, + prefill: { separatorGap: 'sm' }, + renderMatrix: (args) => , + }, CardContent: { canvasLabel: 'CardContent — padding + last-child bottom padding', fields: CARD_CONTENT_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index b08a2de99baf69..04bc5e04d1441a 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -22,6 +22,8 @@ import Select from '@mui/material/Select'; import Alert from '@mui/material/Alert'; import Chip from '@mui/material/Chip'; import Avatar from '@mui/material/Avatar'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; @@ -172,6 +174,17 @@ const demos: Record = { ), + Breadcrumbs: ( + + + Home + + + Catalog + + Current + + ), Accordion: ( diff --git a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js index 63cb6bae7f3538..2a45de9adf0a2f 100644 --- a/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js +++ b/packages/mui-material/src/Breadcrumbs/Breadcrumbs.js @@ -11,6 +11,7 @@ import { useDefaultProps } from '../DefaultPropsProvider'; import Typography from '../Typography'; import BreadcrumbCollapsed from './BreadcrumbCollapsed'; import breadcrumbsClasses, { getBreadcrumbsUtilityClass } from './breadcrumbsClasses'; +import { private_breadcrumbsVars as vars } from './breadcrumbsVars'; const useUtilityClasses = (ownerState) => { const { classes } = ownerState; @@ -51,8 +52,11 @@ const BreadcrumbsSeparator = styled('li', { })({ display: 'flex', userSelect: 'none', - marginLeft: 8, - marginRight: 8, + // Density seam: separator inline gap over the 8px default. + '--_separatorGap': '8px', + '--comp-separatorGap': `var(${vars.separatorGap}, var(--_separatorGap))`, + marginLeft: 'var(--comp-separatorGap, var(--_separatorGap))', + marginRight: 'var(--comp-separatorGap, var(--_separatorGap))', }); function insertSeparators(items, className, separator, ownerState) { diff --git a/packages/mui-material/src/Breadcrumbs/breadcrumbsVars.ts b/packages/mui-material/src/Breadcrumbs/breadcrumbsVars.ts new file mode 100644 index 00000000000000..40c51edbcf5af9 --- /dev/null +++ b/packages/mui-material/src/Breadcrumbs/breadcrumbsVars.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Breadcrumbs density token identity — the separator's inline gap (8px each + * side today), over the agnostic `--comp-separatorGap` seam. `private_*` per the + * density RFC. + */ +export const private_breadcrumbsVars = { + separatorGap: '--Breadcrumbs-separatorGap', +} as const; diff --git a/packages/mui-material/src/Breadcrumbs/index.d.ts b/packages/mui-material/src/Breadcrumbs/index.d.ts index fa0714c18f5187..663dd18fc2574b 100644 --- a/packages/mui-material/src/Breadcrumbs/index.d.ts +++ b/packages/mui-material/src/Breadcrumbs/index.d.ts @@ -3,3 +3,5 @@ export * from './Breadcrumbs'; export { default as breadcrumbsClasses } from './breadcrumbsClasses'; export * from './breadcrumbsClasses'; + +export { private_breadcrumbsVars } from './breadcrumbsVars'; diff --git a/packages/mui-material/src/Breadcrumbs/index.js b/packages/mui-material/src/Breadcrumbs/index.js index 77829cbf764cab..614ead7f6e19dd 100644 --- a/packages/mui-material/src/Breadcrumbs/index.js +++ b/packages/mui-material/src/Breadcrumbs/index.js @@ -2,3 +2,5 @@ export { default } from './Breadcrumbs'; export { default as breadcrumbsClasses } from './breadcrumbsClasses'; export * from './breadcrumbsClasses'; + +export { private_breadcrumbsVars } from './breadcrumbsVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 380bc2fa149195..ca43b04a313071 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -18,6 +18,7 @@ import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; +import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -140,6 +141,9 @@ export default function enhanceComfortDensity(theme: [radioVars.mediumPad]: d.sm, [radioVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiBreadcrumbs', { + [bcVars.separatorGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index b3da555f0aa99e..a45d989bc92d0f 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -18,6 +18,7 @@ import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; +import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -140,6 +141,9 @@ export default function enhanceCompactDensity(theme: [radioVars.mediumPad]: d.sm, [radioVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiBreadcrumbs', { + [bcVars.separatorGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index bf5b28bb238959..29c2ff271eb88b 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -18,6 +18,7 @@ import { private_chipVars as chipVars } from '../Chip/chipVars'; import { private_accordionSummaryVars as asVars } from '../AccordionSummary/accordionSummaryVars'; import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; +import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -142,6 +143,9 @@ export default function enhanceNormalDensity(theme: [radioVars.mediumPad]: d.sm, [radioVars.smallPad]: d.xs, }); + addRootOverride(enhanced.components, 'MuiBreadcrumbs', { + [bcVars.separatorGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From da2e8ad3cbc0bcd2b1df1e0c57d8f0de0e764607 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 00:07:57 +0700 Subject: [PATCH 072/114] density ToggleButton: tokenize uniform padding per size - padding 11/7/15 -> var(--comp-pad, var(--_pad)) (base medium, small/large variants reroute); toggleButtonVars; re-export js+d.ts - presets map smallPad->sm, mediumPad->md, largePad->lg - playground ToggleButton entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 +++++ docs/pages/experiments/density-playground.tsx | 47 ++++++++++++++++++- docs/src/modules/components/densityDemos.tsx | 13 +++++ .../src/ToggleButton/ToggleButton.js | 13 +++-- .../mui-material/src/ToggleButton/index.d.ts | 2 + .../mui-material/src/ToggleButton/index.js | 2 + .../src/ToggleButton/toggleButtonVars.ts | 11 +++++ .../src/styles/enhanceComfortDensity.ts | 6 +++ .../src/styles/enhanceCompactDensity.ts | 6 +++ .../src/styles/enhanceNormalDensity.ts | 6 +++ 10 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 packages/mui-material/src/ToggleButton/toggleButtonVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index e91b741d1080d5..293f226b532866 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Breadcrumbs-separatorGap' as any]: '4px' }, loose: { ['--Breadcrumbs-separatorGap' as any]: '16px' }, }, + ToggleButton: { + dense: { + ['--ToggleButton-small-pad' as any]: '4px', + ['--ToggleButton-medium-pad' as any]: '7px', + ['--ToggleButton-large-pad' as any]: '10px', + }, + loose: { + ['--ToggleButton-small-pad' as any]: '12px', + ['--ToggleButton-medium-pad' as any]: '16px', + ['--ToggleButton-large-pad' as any]: '22px', + }, + }, CardContent: { dense: { ['--CardContent-pad' as any]: '8px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index e302299bee17b9..763fc2451dd144 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -39,7 +39,7 @@ import AccordionDetails, { private_accordionDetailsVars } from '@mui/material/Ac import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import Typography from '@mui/material/Typography'; import Divider from '@mui/material/Divider'; -import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButton, { private_toggleButtonVars } from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Tooltip, { private_tooltipVars } from '@mui/material/Tooltip'; import PaddingIcon from '@mui/icons-material/Padding'; @@ -812,6 +812,45 @@ function BreadcrumbsMatrix({ ); } +// ToggleButton family: uniform padding per size. +const TOGGLE_BUTTON_FIELDS: DensityField[] = [ + { key: 'smallPad', cssVar: private_toggleButtonVars.smallPad }, + { key: 'mediumPad', cssVar: private_toggleButtonVars.mediumPad }, + { key: 'largePad', cssVar: private_toggleButtonVars.largePad }, +]; + +function ToggleButtonMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + TOGGLE_BUTTON_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + + {size} L + + + C + + + ))} + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -907,6 +946,12 @@ const COMPONENT_DEFS = { prefill: { mediumPad: 'sm', smallPad: 'xs' }, renderMatrix: (args) => , }, + ToggleButton: { + canvasLabel: 'ToggleButton — uniform padding (small/medium/large)', + fields: TOGGLE_BUTTON_FIELDS, + prefill: { smallPad: 'sm', mediumPad: 'md', largePad: 'lg' }, + renderMatrix: (args) => , + }, Breadcrumbs: { canvasLabel: 'Breadcrumbs — separator inline gap', fields: BREADCRUMBS_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 04bc5e04d1441a..c44d4f303314fd 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -13,6 +13,8 @@ import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Checkbox from '@mui/material/Checkbox'; import Radio from '@mui/material/Radio'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; @@ -174,6 +176,17 @@ const demos: Record = { ), + ToggleButton: ( + + {(['small', 'medium', 'large'] as const).map((size) => ( + + L + C + R + + ))} + + ), Breadcrumbs: ( diff --git a/packages/mui-material/src/ToggleButton/ToggleButton.js b/packages/mui-material/src/ToggleButton/ToggleButton.js index bc8e7e207c0dfa..1513a3cedd8760 100644 --- a/packages/mui-material/src/ToggleButton/ToggleButton.js +++ b/packages/mui-material/src/ToggleButton/ToggleButton.js @@ -12,6 +12,7 @@ import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import { useDefaultProps } from '../DefaultPropsProvider'; import toggleButtonClasses, { getToggleButtonUtilityClass } from './toggleButtonClasses'; +import { private_toggleButtonVars as vars } from './toggleButtonVars'; import ToggleButtonGroupContext from '../ToggleButtonGroup/ToggleButtonGroupContext'; import ToggleButtonGroupButtonContext from '../ToggleButtonGroup/ToggleButtonGroupButtonContext'; import isValueSelected from '../ToggleButtonGroup/isValueSelected'; @@ -45,7 +46,11 @@ const ToggleButtonRoot = styled(ButtonBase, { memoTheme(({ theme }) => ({ ...theme.typography.button, borderRadius: (theme.vars || theme).shape.borderRadius, - padding: 11, + // Density seam: uniform padding over the per-size default (medium base; + // small/large variants below reroute + swap the default). + '--_pad': '11px', + '--comp-pad': `var(${vars.mediumPad}, var(--_pad))`, + padding: 'var(--comp-pad, var(--_pad))', border: `1px solid ${(theme.vars || theme).palette.divider}`, color: (theme.vars || theme).palette.action.active, [`&.${toggleButtonClasses.disabled}`]: { @@ -125,14 +130,16 @@ const ToggleButtonRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - padding: 7, + '--_pad': '7px', + '--comp-pad': `var(${vars.smallPad}, var(--_pad))`, fontSize: theme.typography.pxToRem(13), }, }, { props: { size: 'large' }, style: { - padding: 15, + '--_pad': '15px', + '--comp-pad': `var(${vars.largePad}, var(--_pad))`, fontSize: theme.typography.pxToRem(15), }, }, diff --git a/packages/mui-material/src/ToggleButton/index.d.ts b/packages/mui-material/src/ToggleButton/index.d.ts index dd6f335603f5aa..aef18f877b2ad0 100644 --- a/packages/mui-material/src/ToggleButton/index.d.ts +++ b/packages/mui-material/src/ToggleButton/index.d.ts @@ -3,3 +3,5 @@ export * from './ToggleButton'; export { default as toggleButtonClasses } from './toggleButtonClasses'; export * from './toggleButtonClasses'; + +export { private_toggleButtonVars } from './toggleButtonVars'; diff --git a/packages/mui-material/src/ToggleButton/index.js b/packages/mui-material/src/ToggleButton/index.js index 41bb0536de4556..9055974a7eca59 100644 --- a/packages/mui-material/src/ToggleButton/index.js +++ b/packages/mui-material/src/ToggleButton/index.js @@ -2,3 +2,5 @@ export { default } from './ToggleButton'; export { default as toggleButtonClasses } from './toggleButtonClasses'; export * from './toggleButtonClasses'; + +export { private_toggleButtonVars } from './toggleButtonVars'; diff --git a/packages/mui-material/src/ToggleButton/toggleButtonVars.ts b/packages/mui-material/src/ToggleButton/toggleButtonVars.ts new file mode 100644 index 00000000000000..28ebeca9bc616b --- /dev/null +++ b/packages/mui-material/src/ToggleButton/toggleButtonVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * ToggleButton density token identities — the uniform `padding` per size + * (11/7/15 today), over the agnostic `--comp-pad` seam. `private_*` per the + * density RFC. + */ +export const private_toggleButtonVars = { + smallPad: '--ToggleButton-small-pad', + mediumPad: '--ToggleButton-medium-pad', + largePad: '--ToggleButton-large-pad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index ca43b04a313071..03edfd313354f0 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -19,6 +19,7 @@ import { private_accordionSummaryVars as asVars } from '../AccordionSummary/acco import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; +import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -144,6 +145,11 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiBreadcrumbs', { [bcVars.separatorGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToggleButton', { + [tbVars.smallPad]: d.sm, + [tbVars.mediumPad]: d.md, + [tbVars.largePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index a45d989bc92d0f..2a5aea506d18a5 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -19,6 +19,7 @@ import { private_accordionSummaryVars as asVars } from '../AccordionSummary/acco import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; +import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -144,6 +145,11 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiBreadcrumbs', { [bcVars.separatorGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToggleButton', { + [tbVars.smallPad]: d.sm, + [tbVars.mediumPad]: d.md, + [tbVars.largePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 29c2ff271eb88b..63dc55ec4a37e4 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -19,6 +19,7 @@ import { private_accordionSummaryVars as asVars } from '../AccordionSummary/acco import { private_accordionDetailsVars as adVars } from '../AccordionDetails/accordionDetailsVars'; import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; +import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -146,6 +147,11 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiBreadcrumbs', { [bcVars.separatorGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToggleButton', { + [tbVars.smallPad]: d.sm, + [tbVars.mediumPad]: d.md, + [tbVars.largePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 054a30c213bd63dd07d6020aa73282adf502038b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 00:12:48 +0700 Subject: [PATCH 073/114] density Avatar: tokenize square size (raw px per preset) - width/height 40 -> var(--comp-size, var(--_size)); avatarVars (size); re-export js+d.ts - presets map size raw px 32/40/48 (normal == today); fontSize left literal (typography) - playground Avatar entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 4 +++ docs/pages/experiments/density-playground.tsx | 31 ++++++++++++++++++- docs/src/modules/components/densityDemos.tsx | 6 ++++ packages/mui-material/src/Avatar/Avatar.js | 8 +++-- .../mui-material/src/Avatar/avatarVars.ts | 9 ++++++ packages/mui-material/src/Avatar/index.d.ts | 2 ++ packages/mui-material/src/Avatar/index.js | 2 ++ .../src/styles/enhanceComfortDensity.ts | 5 +++ .../src/styles/enhanceCompactDensity.ts | 5 +++ .../src/styles/enhanceNormalDensity.ts | 5 +++ 10 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 packages/mui-material/src/Avatar/avatarVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 293f226b532866..400dca4d399ef8 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -124,6 +124,10 @@ const scopes: Record> = { ['--Radio-small-pad' as any]: '12px', }, }, + Avatar: { + dense: { ['--Avatar-size' as any]: '28px' }, + loose: { ['--Avatar-size' as any]: '56px' }, + }, Breadcrumbs: { dense: { ['--Breadcrumbs-separatorGap' as any]: '4px' }, loose: { ['--Breadcrumbs-separatorGap' as any]: '16px' }, diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 763fc2451dd144..23ca85a8f8b5df 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -32,7 +32,7 @@ import Select, { private_selectVars } from '@mui/material/Select'; import InputLabel from '@mui/material/InputLabel'; import Alert, { private_alertVars } from '@mui/material/Alert'; import Chip, { private_chipVars } from '@mui/material/Chip'; -import Avatar from '@mui/material/Avatar'; +import Avatar, { private_avatarVars } from '@mui/material/Avatar'; import Accordion from '@mui/material/Accordion'; import AccordionSummary, { private_accordionSummaryVars } from '@mui/material/AccordionSummary'; import AccordionDetails, { private_accordionDetailsVars } from '@mui/material/AccordionDetails'; @@ -851,6 +851,29 @@ function ToggleButtonMatrix({ ); } +// Avatar family: the square size (raw px; no size prop). +const AVATAR_FIELDS: DensityField[] = [{ key: 'size', cssVar: private_avatarVars.size }]; + +function AvatarMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = + mappingEnabled && active('size') + ? { [private_avatarVars.size]: resolveValue(mapping.size) } + : undefined; + return ( + + A + B + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -946,6 +969,12 @@ const COMPONENT_DEFS = { prefill: { mediumPad: 'sm', smallPad: 'xs' }, renderMatrix: (args) => , }, + Avatar: { + canvasLabel: 'Avatar — square size (raw px)', + fields: AVATAR_FIELDS, + prefill: {}, // size = raw px, read off the theme + renderMatrix: (args) => , + }, ToggleButton: { canvasLabel: 'ToggleButton — uniform padding (small/medium/large)', fields: TOGGLE_BUTTON_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index c44d4f303314fd..e61ca3137eb1a2 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -176,6 +176,12 @@ const demos: Record = { ), + Avatar: ( + + A + B + + ), ToggleButton: ( {(['small', 'medium', 'large'] as const).map((size) => ( diff --git a/packages/mui-material/src/Avatar/Avatar.js b/packages/mui-material/src/Avatar/Avatar.js index 70aa55b64e5907..8bedb48d70a434 100644 --- a/packages/mui-material/src/Avatar/Avatar.js +++ b/packages/mui-material/src/Avatar/Avatar.js @@ -8,6 +8,7 @@ import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import Person from '../internal/svg-icons/Person'; import { getAvatarUtilityClass } from './avatarClasses'; +import { private_avatarVars as vars } from './avatarVars'; import useSlot from '../utils/useSlot'; const useUtilityClasses = (ownerState) => { @@ -41,8 +42,11 @@ const AvatarRoot = styled('div', { alignItems: 'center', justifyContent: 'center', flexShrink: 0, - width: 40, - height: 40, + // Density seam: square size (raw px per preset) over the 40px default. + '--_size': '40px', + '--comp-size': `var(${vars.size}, var(--_size))`, + width: 'var(--comp-size, var(--_size))', + height: 'var(--comp-size, var(--_size))', fontFamily: theme.typography.fontFamily, fontSize: theme.typography.pxToRem(20), lineHeight: 1, diff --git a/packages/mui-material/src/Avatar/avatarVars.ts b/packages/mui-material/src/Avatar/avatarVars.ts new file mode 100644 index 00000000000000..a28a83824aa1f2 --- /dev/null +++ b/packages/mui-material/src/Avatar/avatarVars.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Avatar density token identity — the square `width`/`height` (40px today), + * over the agnostic `--comp-size` seam. Sizing → raw px per preset. `fontSize` + * (the initials) stays literal (typography). `private_*` per the density RFC. + */ +export const private_avatarVars = { + size: '--Avatar-size', +} as const; diff --git a/packages/mui-material/src/Avatar/index.d.ts b/packages/mui-material/src/Avatar/index.d.ts index 3eaad9c3f93f9f..423991922196b7 100644 --- a/packages/mui-material/src/Avatar/index.d.ts +++ b/packages/mui-material/src/Avatar/index.d.ts @@ -3,3 +3,5 @@ export * from './Avatar'; export { default as avatarClasses } from './avatarClasses'; export * from './avatarClasses'; + +export { private_avatarVars } from './avatarVars'; diff --git a/packages/mui-material/src/Avatar/index.js b/packages/mui-material/src/Avatar/index.js index 08248f841869fb..946a8ad12c7ead 100644 --- a/packages/mui-material/src/Avatar/index.js +++ b/packages/mui-material/src/Avatar/index.js @@ -2,3 +2,5 @@ export { default } from './Avatar'; export { default as avatarClasses } from './avatarClasses'; export * from './avatarClasses'; + +export { private_avatarVars } from './avatarVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 03edfd313354f0..6cdaf7722efb3a 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -20,6 +20,7 @@ import { private_accordionDetailsVars as adVars } from '../AccordionDetails/acco import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; +import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -150,6 +151,10 @@ export default function enhanceComfortDensity(theme: [tbVars.mediumPad]: d.md, [tbVars.largePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAvatar', { + // Square size = raw px (sizing). + [avVars.size]: '48px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 2a5aea506d18a5..a700a96766c3dc 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -20,6 +20,7 @@ import { private_accordionDetailsVars as adVars } from '../AccordionDetails/acco import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; +import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -150,6 +151,10 @@ export default function enhanceCompactDensity(theme: [tbVars.mediumPad]: d.md, [tbVars.largePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAvatar', { + // Square size = raw px (sizing). + [avVars.size]: '32px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 63dc55ec4a37e4..b59662aeaeb968 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -20,6 +20,7 @@ import { private_accordionDetailsVars as adVars } from '../AccordionDetails/acco import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; +import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -152,6 +153,10 @@ export default function enhanceNormalDensity(theme: [tbVars.mediumPad]: d.md, [tbVars.largePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAvatar', { + // Square size = raw px (sizing). + [avVars.size]: '40px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From ef7b96039c4654a1282fb4585eb623f71e296b44 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 00:18:47 +0700 Subject: [PATCH 074/114] density Badge: tokenize bubble size + padding per state (standard / dot) - standard base + dot variant: minWidth/height + padding via --comp-size/--comp-pad; badgeVars; re-export js+d.ts - presets: sizes raw px (standard 18/20/24, dot 4/6/8), standardPad '0 ' (normal == today) - playground Badge entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 ++++++ docs/pages/experiments/density-playground.tsx | 39 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 11 ++++++ packages/mui-material/src/Badge/Badge.js | 20 +++++++--- packages/mui-material/src/Badge/badgeVars.ts | 12 ++++++ packages/mui-material/src/Badge/index.d.ts | 2 + packages/mui-material/src/Badge/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 7 ++++ .../src/styles/enhanceCompactDensity.ts | 7 ++++ .../src/styles/enhanceNormalDensity.ts | 7 ++++ 10 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 packages/mui-material/src/Badge/badgeVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 400dca4d399ef8..292c7e2b8bed9f 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + Badge: { + dense: { + ['--Badge-standard-size' as any]: '16px', + ['--Badge-standard-pad' as any]: '0 4px', + ['--Badge-dot-size' as any]: '4px', + }, + loose: { + ['--Badge-standard-size' as any]: '26px', + ['--Badge-standard-pad' as any]: '0 10px', + ['--Badge-dot-size' as any]: '10px', + }, + }, Breadcrumbs: { dense: { ['--Breadcrumbs-separatorGap' as any]: '4px' }, loose: { ['--Breadcrumbs-separatorGap' as any]: '16px' }, diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 23ca85a8f8b5df..1505f8e286c3ee 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -33,6 +33,7 @@ import InputLabel from '@mui/material/InputLabel'; import Alert, { private_alertVars } from '@mui/material/Alert'; import Chip, { private_chipVars } from '@mui/material/Chip'; import Avatar, { private_avatarVars } from '@mui/material/Avatar'; +import Badge, { private_badgeVars } from '@mui/material/Badge'; import Accordion from '@mui/material/Accordion'; import AccordionSummary, { private_accordionSummaryVars } from '@mui/material/AccordionSummary'; import AccordionDetails, { private_accordionDetailsVars } from '@mui/material/AccordionDetails'; @@ -874,6 +875,38 @@ function AvatarMatrix({ ); } +// Badge family: bubble size + padding, per state (standard / dot). +const BADGE_FIELDS: DensityField[] = [ + { key: 'standardSize', cssVar: private_badgeVars.standardSize }, + { key: 'standardPad', cssVar: private_badgeVars.standardPad }, + { key: 'dotSize', cssVar: private_badgeVars.dotSize }, +]; + +function BadgeMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + BADGE_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + + + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -975,6 +1008,12 @@ const COMPONENT_DEFS = { prefill: {}, // size = raw px, read off the theme renderMatrix: (args) => , }, + Badge: { + canvasLabel: 'Badge — bubble size + padding (standard / dot)', + fields: BADGE_FIELDS, + prefill: { standardPad: '0 xs' }, // sizes = raw px, read off the theme + renderMatrix: (args) => , + }, ToggleButton: { canvasLabel: 'ToggleButton — uniform padding (small/medium/large)', fields: TOGGLE_BUTTON_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index e61ca3137eb1a2..bbfdd5794cbcc5 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -24,6 +24,7 @@ import Select from '@mui/material/Select'; import Alert from '@mui/material/Alert'; import Chip from '@mui/material/Chip'; import Avatar from '@mui/material/Avatar'; +import Badge from '@mui/material/Badge'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; import Accordion from '@mui/material/Accordion'; @@ -182,6 +183,16 @@ const demos: Record = { B ), + Badge: ( + + + + + + + + + ), ToggleButton: ( {(['small', 'medium', 'large'] as const).map((size) => ( diff --git a/packages/mui-material/src/Badge/Badge.js b/packages/mui-material/src/Badge/Badge.js index 45f29b514875a6..2c477b9530dc64 100644 --- a/packages/mui-material/src/Badge/Badge.js +++ b/packages/mui-material/src/Badge/Badge.js @@ -11,6 +11,7 @@ import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFil import { useDefaultProps } from '../DefaultPropsProvider'; import capitalize from '../utils/capitalize'; import badgeClasses, { getBadgeUtilityClass } from './badgeClasses'; +import { private_badgeVars as vars } from './badgeVars'; import useSlot from '../utils/useSlot'; import { getTransitionStyles } from '../transitions/utils'; @@ -80,10 +81,16 @@ const BadgeBadge = styled('span', { fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontWeightMedium, fontSize: theme.typography.pxToRem(12), - minWidth: RADIUS_STANDARD * 2, + // Density seams: bubble size (raw px) + padding, per state (standard base; + // dot variant reroutes). borderRadius stays literal (pill at default size). + '--_size': `${RADIUS_STANDARD * 2}px`, + '--_pad': '0 6px', + '--comp-size': `var(${vars.standardSize}, var(--_size))`, + '--comp-pad': `var(${vars.standardPad}, var(--_pad))`, + minWidth: 'var(--comp-size, var(--_size))', lineHeight: 1, - padding: '0 6px', - height: RADIUS_STANDARD * 2, + padding: 'var(--comp-pad, var(--_pad))', + height: 'var(--comp-size, var(--_size))', borderRadius: RADIUS_STANDARD, zIndex: 1, // Render the badge on top of potential ripples. '@media (forced-colors: active)': { @@ -106,10 +113,11 @@ const BadgeBadge = styled('span', { { props: { variant: 'dot' }, style: { + '--_size': `${RADIUS_DOT * 2}px`, + '--_pad': '0px', + '--comp-size': `var(${vars.dotSize}, var(--_size))`, + '--comp-pad': `var(${vars.dotPad}, var(--_pad))`, borderRadius: RADIUS_DOT, - height: RADIUS_DOT * 2, - minWidth: RADIUS_DOT * 2, - padding: 0, }, }, { diff --git a/packages/mui-material/src/Badge/badgeVars.ts b/packages/mui-material/src/Badge/badgeVars.ts new file mode 100644 index 00000000000000..354956e52209fb --- /dev/null +++ b/packages/mui-material/src/Badge/badgeVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Badge density token identities — the bubble size (min-width/height, raw px) + * and padding, per state: `standard` (count bubble, 20px / `0 6px`) and `dot` + * (6px / 0). Over the agnostic `--comp-*` seams. `private_*` per the density RFC. + */ +export const private_badgeVars = { + standardSize: '--Badge-standard-size', + standardPad: '--Badge-standard-pad', + dotSize: '--Badge-dot-size', + dotPad: '--Badge-dot-pad', +} as const; diff --git a/packages/mui-material/src/Badge/index.d.ts b/packages/mui-material/src/Badge/index.d.ts index 1d30fdc3f64b4c..c204f33269995c 100644 --- a/packages/mui-material/src/Badge/index.d.ts +++ b/packages/mui-material/src/Badge/index.d.ts @@ -3,3 +3,5 @@ export * from './Badge'; export { default as badgeClasses } from './badgeClasses'; export * from './badgeClasses'; + +export { private_badgeVars } from './badgeVars'; diff --git a/packages/mui-material/src/Badge/index.js b/packages/mui-material/src/Badge/index.js index 42f9d0102195b5..9507b80e738103 100644 --- a/packages/mui-material/src/Badge/index.js +++ b/packages/mui-material/src/Badge/index.js @@ -2,3 +2,5 @@ export { default } from './Badge'; export { default as badgeClasses } from './badgeClasses'; export * from './badgeClasses'; + +export { private_badgeVars } from './badgeVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 6cdaf7722efb3a..01adde086492f1 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -21,6 +21,7 @@ import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; +import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -155,6 +156,12 @@ export default function enhanceComfortDensity(theme: // Square size = raw px (sizing). [avVars.size]: '48px', }); + addRootOverride(enhanced.components, 'MuiBadge', { + // Bubble size = raw px; standard padding = '0 '. + [badgeVars.standardSize]: '24px', + [badgeVars.dotSize]: '8px', + [badgeVars.standardPad]: `0 `, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index a700a96766c3dc..fc3375694a41f9 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -21,6 +21,7 @@ import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; +import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -155,6 +156,12 @@ export default function enhanceCompactDensity(theme: // Square size = raw px (sizing). [avVars.size]: '32px', }); + addRootOverride(enhanced.components, 'MuiBadge', { + // Bubble size = raw px; standard padding = '0 '. + [badgeVars.standardSize]: '18px', + [badgeVars.dotSize]: '4px', + [badgeVars.standardPad]: `0 `, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index b59662aeaeb968..41a5b2dbd224ba 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -21,6 +21,7 @@ import { private_radioVars as radioVars } from '../Radio/radioVars'; import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVars'; import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; +import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -157,6 +158,12 @@ export default function enhanceNormalDensity(theme: // Square size = raw px (sizing). [avVars.size]: '40px', }); + addRootOverride(enhanced.components, 'MuiBadge', { + // Bubble size = raw px; standard padding = '0 '. + [badgeVars.standardSize]: '20px', + [badgeVars.dotSize]: '6px', + [badgeVars.standardPad]: `0 `, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From b8a237215f41cf71af3f845fd1a208a232edbfb1 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 00:26:11 +0700 Subject: [PATCH 075/114] density ButtonGroup: tokenize grouped-button min-width floor - grouped minWidth 40 -> var(--comp-minWidth, var(--_minWidth)); buttonGroupVars; re-export js+d.ts - presets map minWidth raw px 32/40/48 (normal == today) - playground ButtonGroup entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 4 ++ docs/pages/experiments/density-playground.tsx | 39 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 8 ++++ .../src/ButtonGroup/ButtonGroup.js | 6 ++- .../src/ButtonGroup/buttonGroupVars.ts | 9 +++++ .../mui-material/src/ButtonGroup/index.d.ts | 2 + .../mui-material/src/ButtonGroup/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 6 ++- .../src/styles/enhanceCompactDensity.ts | 6 ++- .../src/styles/enhanceNormalDensity.ts | 6 ++- 10 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 packages/mui-material/src/ButtonGroup/buttonGroupVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 292c7e2b8bed9f..c445631bacdc7a 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,10 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + ButtonGroup: { + dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, + loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, + }, Badge: { dense: { ['--Badge-standard-size' as any]: '16px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 1505f8e286c3ee..02087f3993431f 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button, { private_buttonVars } from '@mui/material/Button'; +import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup'; import CssBaseline from '@mui/material/CssBaseline'; import RadioGroup from '@mui/material/RadioGroup'; import FormControl from '@mui/material/FormControl'; @@ -907,6 +908,38 @@ function BadgeMatrix({ ); } +// ButtonGroup family: the grouped-button min-width floor (raw px). +const BUTTON_GROUP_FIELDS: DensityField[] = [ + { key: 'minWidth', cssVar: private_buttonGroupVars.minWidth }, +]; + +function ButtonGroupMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = + mappingEnabled && active('minWidth') + ? { [private_buttonGroupVars.minWidth]: resolveValue(mapping.minWidth) } + : undefined; + return ( + + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1008,6 +1041,12 @@ const COMPONENT_DEFS = { prefill: {}, // size = raw px, read off the theme renderMatrix: (args) => , }, + ButtonGroup: { + canvasLabel: 'ButtonGroup — grouped-button min-width floor', + fields: BUTTON_GROUP_FIELDS, + prefill: {}, // minWidth = raw px, read off the theme + renderMatrix: (args) => , + }, Badge: { canvasLabel: 'Badge — bubble size + padding (standard / dot)', fields: BADGE_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index bbfdd5794cbcc5..e173898b7e6bfa 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -183,6 +184,13 @@ const demos: Record = { B ), + ButtonGroup: ( + + + + + + ), Badge: ( diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroup.js b/packages/mui-material/src/ButtonGroup/ButtonGroup.js index 8a4d9cb753be97..c1d73252f0c27c 100644 --- a/packages/mui-material/src/ButtonGroup/ButtonGroup.js +++ b/packages/mui-material/src/ButtonGroup/ButtonGroup.js @@ -10,6 +10,7 @@ import memoTheme from '../utils/memoTheme'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import { useDefaultProps } from '../DefaultPropsProvider'; import buttonGroupClasses, { getButtonGroupUtilityClass } from './buttonGroupClasses'; +import { private_buttonGroupVars as vars } from './buttonGroupVars'; import ButtonGroupContext from './ButtonGroupContext'; import ButtonGroupButtonContext from './ButtonGroupButtonContext'; @@ -224,8 +225,11 @@ const ButtonGroupRoot = styled('div', { }, })), ], + // Density seam: grouped-button min-width floor (raw px per preset). + '--_minWidth': '40px', + '--comp-minWidth': `var(${vars.minWidth}, var(--_minWidth))`, [`& .${buttonGroupClasses.grouped}`]: { - minWidth: 40, + minWidth: 'var(--comp-minWidth, var(--_minWidth))', }, })), ); diff --git a/packages/mui-material/src/ButtonGroup/buttonGroupVars.ts b/packages/mui-material/src/ButtonGroup/buttonGroupVars.ts new file mode 100644 index 00000000000000..8ae5a284349966 --- /dev/null +++ b/packages/mui-material/src/ButtonGroup/buttonGroupVars.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * ButtonGroup density token identity — the grouped buttons' `min-width` floor + * (40px today), over `--comp-minWidth` (raw px per preset). The buttons' own + * padding density comes from Button (already tokenized). `private_*`. + */ +export const private_buttonGroupVars = { + minWidth: '--ButtonGroup-minWidth', +} as const; diff --git a/packages/mui-material/src/ButtonGroup/index.d.ts b/packages/mui-material/src/ButtonGroup/index.d.ts index 09df42ba505364..50a3d9347414b3 100644 --- a/packages/mui-material/src/ButtonGroup/index.d.ts +++ b/packages/mui-material/src/ButtonGroup/index.d.ts @@ -2,5 +2,7 @@ export { default } from './ButtonGroup'; export * from './ButtonGroup'; export { default as buttonGroupClasses } from './buttonGroupClasses'; export * from './buttonGroupClasses'; + +export { private_buttonGroupVars } from './buttonGroupVars'; export { default as ButtonGroupContext } from './ButtonGroupContext'; export { default as ButtonGroupButtonContext } from './ButtonGroupButtonContext'; diff --git a/packages/mui-material/src/ButtonGroup/index.js b/packages/mui-material/src/ButtonGroup/index.js index b0466bbdc9cfe4..4449714b88fc56 100644 --- a/packages/mui-material/src/ButtonGroup/index.js +++ b/packages/mui-material/src/ButtonGroup/index.js @@ -1,5 +1,7 @@ export { default } from './ButtonGroup'; export { default as buttonGroupClasses } from './buttonGroupClasses'; export * from './buttonGroupClasses'; + +export { private_buttonGroupVars } from './buttonGroupVars'; export { default as ButtonGroupContext } from './ButtonGroupContext'; export { default as ButtonGroupButtonContext } from './ButtonGroupButtonContext'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 01adde086492f1..d4fa8cd24eedce 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -22,6 +22,7 @@ import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVar import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; +import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -160,7 +161,10 @@ export default function enhanceComfortDensity(theme: // Bubble size = raw px; standard padding = '0 '. [badgeVars.standardSize]: '24px', [badgeVars.dotSize]: '8px', - [badgeVars.standardPad]: `0 `, + [badgeVars.standardPad]: `0 ${d.xs}`, + }); + addRootOverride(enhanced.components, 'MuiButtonGroup', { + [bgVars.minWidth]: '48px', }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index fc3375694a41f9..876b9e3b221fc8 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -22,6 +22,7 @@ import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVar import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; +import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -160,7 +161,10 @@ export default function enhanceCompactDensity(theme: // Bubble size = raw px; standard padding = '0 '. [badgeVars.standardSize]: '18px', [badgeVars.dotSize]: '4px', - [badgeVars.standardPad]: `0 `, + [badgeVars.standardPad]: `0 ${d.xs}`, + }); + addRootOverride(enhanced.components, 'MuiButtonGroup', { + [bgVars.minWidth]: '32px', }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 41a5b2dbd224ba..0307027cae8bcf 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -22,6 +22,7 @@ import { private_breadcrumbsVars as bcVars } from '../Breadcrumbs/breadcrumbsVar import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButtonVars'; import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; +import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -162,7 +163,10 @@ export default function enhanceNormalDensity(theme: // Bubble size = raw px; standard padding = '0 '. [badgeVars.standardSize]: '20px', [badgeVars.dotSize]: '6px', - [badgeVars.standardPad]: `0 `, + [badgeVars.standardPad]: `0 ${d.xs}`, + }); + addRootOverride(enhanced.components, 'MuiButtonGroup', { + [bgVars.minWidth]: '40px', }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. From 8dbcaa64209f62e9216a2966bfb65f0553675462 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 03:54:11 +0700 Subject: [PATCH 076/114] density Table (TableCell): tokenize cell padding (block per size + shared inline) - TableCell block pad per size (medium base / small variant) + inline pad via --comp-blockPad/--comp-inlinePad; tableCellVars; re-export js+d.ts - presets map mediumBlockPad->lg, smallBlockPad->xs, inlinePad->lg (normal 16/6/16 == today) - checkbox/none affordances left literal - playground TableCell entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 ++++ docs/pages/experiments/density-playground.tsx | 58 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 25 ++++++++ .../mui-material/src/TableCell/TableCell.js | 12 +++- .../mui-material/src/TableCell/index.d.ts | 2 + packages/mui-material/src/TableCell/index.js | 2 + .../src/TableCell/tableCellVars.ts | 12 ++++ .../src/styles/enhanceComfortDensity.ts | 7 +++ .../src/styles/enhanceCompactDensity.ts | 7 +++ .../src/styles/enhanceNormalDensity.ts | 7 +++ 10 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/TableCell/tableCellVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index c445631bacdc7a..2cb3f8e4a0d443 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -132,6 +132,18 @@ const scopes: Record> = { dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, }, + TableCell: { + dense: { + ['--TableCell-medium-blockPad' as any]: '8px', + ['--TableCell-small-blockPad' as any]: '3px', + ['--TableCell-inlinePad' as any]: '10px', + }, + loose: { + ['--TableCell-medium-blockPad' as any]: '24px', + ['--TableCell-small-blockPad' as any]: '12px', + ['--TableCell-inlinePad' as any]: '24px', + }, + }, Badge: { dense: { ['--Badge-standard-size' as any]: '16px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 02087f3993431f..e95cff159d8583 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -4,6 +4,11 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button, { private_buttonVars } from '@mui/material/Button'; import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TableCell, { private_tableCellVars } from '@mui/material/TableCell'; import CssBaseline from '@mui/material/CssBaseline'; import RadioGroup from '@mui/material/RadioGroup'; import FormControl from '@mui/material/FormControl'; @@ -940,6 +945,53 @@ function ButtonGroupMatrix({ ); } +// TableCell family: block padding per size (medium/small) + shared inline pad. +const TABLE_CELL_FIELDS: DensityField[] = [ + { key: 'mediumBlockPad', cssVar: private_tableCellVars.mediumBlockPad }, + { key: 'smallBlockPad', cssVar: private_tableCellVars.smallBlockPad }, + { key: 'inlinePad', cssVar: private_tableCellVars.inlinePad }, +]; + +function TableCellMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + TABLE_CELL_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + {(['medium', 'small'] as const).map((size) => ( + + + + + {size} name + + Value + + + + + Row one + 42 + + +
+ ))} +
+ ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1047,6 +1099,12 @@ const COMPONENT_DEFS = { prefill: {}, // minWidth = raw px, read off the theme renderMatrix: (args) => , }, + TableCell: { + canvasLabel: 'TableCell — block padding per size + inline padding', + fields: TABLE_CELL_FIELDS, + prefill: { mediumBlockPad: 'lg', smallBlockPad: 'xs', inlinePad: 'lg' }, + renderMatrix: (args) => , + }, Badge: { canvasLabel: 'Badge — bubble size + padding (standard / dot)', fields: BADGE_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index e173898b7e6bfa..dbfac3e13b1a2a 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -28,6 +28,11 @@ import Avatar from '@mui/material/Avatar'; import Badge from '@mui/material/Badge'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; +import Table from '@mui/material/Table'; +import TableHead from '@mui/material/TableHead'; +import TableBody from '@mui/material/TableBody'; +import TableRow from '@mui/material/TableRow'; +import TableCell from '@mui/material/TableCell'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; @@ -191,6 +196,26 @@ const demos: Record = { ), + TableCell: ( + + {(['medium', 'small'] as const).map((size) => ( + + + + Name + Value + + + + + Row one + 42 + + +
+ ))} +
+ ), Badge: ( diff --git a/packages/mui-material/src/TableCell/TableCell.js b/packages/mui-material/src/TableCell/TableCell.js index 62ea0c0656a7ac..3e7c3c7897a54f 100644 --- a/packages/mui-material/src/TableCell/TableCell.js +++ b/packages/mui-material/src/TableCell/TableCell.js @@ -10,6 +10,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import tableCellClasses, { getTableCellUtilityClass } from './tableCellClasses'; +import { private_tableCellVars as vars } from './tableCellVars'; const useUtilityClasses = (ownerState) => { const { classes, variant, align, padding, size, stickyHeader } = ownerState; @@ -59,7 +60,13 @@ const TableCellRoot = styled('td', { : theme.darken(theme.alpha(theme.palette.divider, 1), 0.68) }`, textAlign: 'left', - padding: 16, + // Density seams: block padding per size (medium base; small variant reroutes), + // inline padding shared. Checkbox/none affordances stay literal below. + '--_blockPad': '16px', + '--_inlinePad': '16px', + '--comp-blockPad': `var(${vars.mediumBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', variants: [ { props: { @@ -94,7 +101,8 @@ const TableCellRoot = styled('td', { size: 'small', }, style: { - padding: '6px 16px', + '--_blockPad': '6px', + '--comp-blockPad': `var(${vars.smallBlockPad}, var(--_blockPad))`, [`&.${tableCellClasses.paddingCheckbox}`]: { width: 24, // prevent the checkbox column from growing padding: '0 12px 0 16px', diff --git a/packages/mui-material/src/TableCell/index.d.ts b/packages/mui-material/src/TableCell/index.d.ts index c1f71ad1206189..0c48d410cd3d82 100644 --- a/packages/mui-material/src/TableCell/index.d.ts +++ b/packages/mui-material/src/TableCell/index.d.ts @@ -3,3 +3,5 @@ export * from './TableCell'; export { default as tableCellClasses } from './tableCellClasses'; export * from './tableCellClasses'; + +export { private_tableCellVars } from './tableCellVars'; diff --git a/packages/mui-material/src/TableCell/index.js b/packages/mui-material/src/TableCell/index.js index d02d3290bf6dab..fb31fd50f557d4 100644 --- a/packages/mui-material/src/TableCell/index.js +++ b/packages/mui-material/src/TableCell/index.js @@ -2,3 +2,5 @@ export { default } from './TableCell'; export { default as tableCellClasses } from './tableCellClasses'; export * from './tableCellClasses'; + +export { private_tableCellVars } from './tableCellVars'; diff --git a/packages/mui-material/src/TableCell/tableCellVars.ts b/packages/mui-material/src/TableCell/tableCellVars.ts new file mode 100644 index 00000000000000..e3f9415b7b579c --- /dev/null +++ b/packages/mui-material/src/TableCell/tableCellVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * TableCell density token identities — the cell padding. Block padding differs + * per size (`normal` 16 / `small` 6); inline padding is shared (16). Over the + * agnostic `--comp-*` seams. The `paddingCheckbox`/`padding="none"` affordances + * stay literal. `private_*` per the density RFC. + */ +export const private_tableCellVars = { + mediumBlockPad: '--TableCell-medium-blockPad', + smallBlockPad: '--TableCell-small-blockPad', + inlinePad: '--TableCell-inlinePad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index d4fa8cd24eedce..2e9a96b0c263d0 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -23,6 +23,7 @@ import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButton import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; +import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -166,6 +167,12 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiButtonGroup', { [bgVars.minWidth]: '48px', }); + addRootOverride(enhanced.components, 'MuiTableCell', { + // Block pad per size (steps); inline pad shared. + [tcVars.mediumBlockPad]: d.lg, + [tcVars.smallBlockPad]: d.xs, + [tcVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 876b9e3b221fc8..7d35190096a1b6 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -23,6 +23,7 @@ import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButton import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; +import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -166,6 +167,12 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiButtonGroup', { [bgVars.minWidth]: '32px', }); + addRootOverride(enhanced.components, 'MuiTableCell', { + // Block pad per size (steps); inline pad shared. + [tcVars.mediumBlockPad]: d.lg, + [tcVars.smallBlockPad]: d.xs, + [tcVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 0307027cae8bcf..ac488f0e1fa848 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -23,6 +23,7 @@ import { private_toggleButtonVars as tbVars } from '../ToggleButton/toggleButton import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; +import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -168,6 +169,12 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiButtonGroup', { [bgVars.minWidth]: '40px', }); + addRootOverride(enhanced.components, 'MuiTableCell', { + // Block pad per size (steps); inline pad shared. + [tcVars.mediumBlockPad]: d.lg, + [tcVars.smallBlockPad]: d.xs, + [tcVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 225a5411893fb296603e675b0d71cf6601d3b992 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 03:59:42 +0700 Subject: [PATCH 077/114] density Autocomplete: tokenize option list geometry (mirrors MenuItem) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - option minHeight (raw px, ≥sm auto via seam fallback) + block/inline pad via --comp-*; autocompleteVars; re-export js+d.ts - presets map optionMinHeight 36/44/56, blockPad->xs, inlinePad->lg (normal 6/16 == today) - input density comes from its variant (already tokenized) - playground Autocomplete entry (open inline); fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 ++++++ docs/pages/experiments/density-playground.tsx | 42 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 11 +++++ .../src/Autocomplete/Autocomplete.js | 22 +++++++--- .../src/Autocomplete/autocompleteVars.ts | 12 ++++++ .../mui-material/src/Autocomplete/index.d.ts | 2 + .../mui-material/src/Autocomplete/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 7 ++++ .../src/styles/enhanceCompactDensity.ts | 7 ++++ .../src/styles/enhanceNormalDensity.ts | 7 ++++ 10 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 packages/mui-material/src/Autocomplete/autocompleteVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 2cb3f8e4a0d443..4ba5a79c864118 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -132,6 +132,18 @@ const scopes: Record> = { dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, }, + Autocomplete: { + dense: { + ['--Autocomplete-option-minHeight' as any]: '32px', + ['--Autocomplete-option-blockPad' as any]: '3px', + ['--Autocomplete-option-inlinePad' as any]: '10px', + }, + loose: { + ['--Autocomplete-option-minHeight' as any]: '60px', + ['--Autocomplete-option-blockPad' as any]: '12px', + ['--Autocomplete-option-inlinePad' as any]: '28px', + }, + }, TableCell: { dense: { ['--TableCell-medium-blockPad' as any]: '8px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index e95cff159d8583..d11c85ee17b358 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -22,6 +22,7 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import InboxIcon from '@mui/icons-material/Inbox'; import TextField from '@mui/material/TextField'; +import Autocomplete, { private_autocompleteVars } from '@mui/material/Autocomplete'; import InputAdornment, { private_inputAdornmentVars } from '@mui/material/InputAdornment'; import { private_outlinedInputVars } from '@mui/material/OutlinedInput'; import { private_filledInputVars } from '@mui/material/FilledInput'; @@ -992,6 +993,41 @@ function TableCellMatrix({ ); } +// Autocomplete family: the option list geometry (mirrors MenuItem). The input's +// density comes from its variant (tokenized separately). +const AUTOCOMPLETE_FIELDS: DensityField[] = [ + { key: 'optionMinHeight', cssVar: private_autocompleteVars.optionMinHeight }, + { key: 'optionBlockPad', cssVar: private_autocompleteVars.optionBlockPad }, + { key: 'optionInlinePad', cssVar: private_autocompleteVars.optionInlinePad }, +]; + +function AutocompleteMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + AUTOCOMPLETE_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + } + /> + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1105,6 +1141,12 @@ const COMPONENT_DEFS = { prefill: { mediumBlockPad: 'lg', smallBlockPad: 'xs', inlinePad: 'lg' }, renderMatrix: (args) => , }, + Autocomplete: { + canvasLabel: 'Autocomplete — option list min-height + padding (open)', + fields: AUTOCOMPLETE_FIELDS, + prefill: { optionBlockPad: 'xs', optionInlinePad: 'lg' }, // minHeight raw px off theme + renderMatrix: (args) => , + }, Badge: { canvasLabel: 'Badge — bubble size + padding (standard / dot)', fields: BADGE_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index dbfac3e13b1a2a..b610364bea6bf8 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -9,6 +9,7 @@ import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Tooltip from '@mui/material/Tooltip'; import TextField from '@mui/material/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; import InputAdornment from '@mui/material/InputAdornment'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; @@ -196,6 +197,16 @@ const demos: Record = { ), + Autocomplete: ( + // Open + inline so the option list (the density lever) renders in the scope. + } + /> + ), TableCell: ( {(['medium', 'small'] as const).map((size) => ( diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index 8470ab477a273c..7149702a8a5ea1 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -22,6 +22,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import autocompleteClasses, { getAutocompleteUtilityClass } from './autocompleteClasses'; +import { private_autocompleteVars as vars } from './autocompleteVars'; import capitalize from '../utils/capitalize'; import useSlot from '../utils/useSlot'; @@ -335,21 +336,30 @@ const AutocompleteListbox = styled('ul', { isolation: 'isolate', // Prevent overlap with iOS overlay scrollbars. position: 'relative', [`& .${autocompleteClasses.option}`]: { - minHeight: 48, + // Density seams (mirrors MenuItem): min-height (raw px; ≥sm collapses to + // `auto` via the seam fallback so a preset's token still wins) + block/ + // inline padding. + '--_minHeight': '48px', + '--_blockPad': '6px', + '--_inlinePad': '16px', + '--comp-minHeight': `var(${vars.optionMinHeight}, var(--_minHeight))`, + '--comp-blockPad': `var(${vars.optionBlockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.optionInlinePad}, var(--_inlinePad))`, + minHeight: 'var(--comp-minHeight, var(--_minHeight))', display: 'flex', overflow: 'hidden', justifyContent: 'flex-start', alignItems: 'center', cursor: 'pointer', - paddingTop: 6, + paddingTop: 'var(--comp-blockPad, var(--_blockPad))', boxSizing: 'border-box', outline: '0', WebkitTapHighlightColor: 'transparent', - paddingBottom: 6, - paddingLeft: 16, - paddingRight: 16, + paddingBottom: 'var(--comp-blockPad, var(--_blockPad))', + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', [theme.breakpoints.up('sm')]: { - minHeight: 'auto', + '--comp-minHeight': `var(${vars.optionMinHeight}, auto)`, }, [`&.${autocompleteClasses.focused}`]: { backgroundColor: (theme.vars || theme).palette.action.hover, diff --git a/packages/mui-material/src/Autocomplete/autocompleteVars.ts b/packages/mui-material/src/Autocomplete/autocompleteVars.ts new file mode 100644 index 00000000000000..f5987c01950e67 --- /dev/null +++ b/packages/mui-material/src/Autocomplete/autocompleteVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Autocomplete density token identities — the option (listbox item) geometry, + * mirroring MenuItem: min-height (raw px; collapses to `auto` ≥sm via the seam + * fallback) + block/inline padding (spacing). The input's density comes from its + * variant (OutlinedInput/etc., already tokenized). `private_*` per the RFC. + */ +export const private_autocompleteVars = { + optionMinHeight: '--Autocomplete-option-minHeight', + optionBlockPad: '--Autocomplete-option-blockPad', + optionInlinePad: '--Autocomplete-option-inlinePad', +} as const; diff --git a/packages/mui-material/src/Autocomplete/index.d.ts b/packages/mui-material/src/Autocomplete/index.d.ts index 52e21ed4302867..b70007c5285fbb 100644 --- a/packages/mui-material/src/Autocomplete/index.d.ts +++ b/packages/mui-material/src/Autocomplete/index.d.ts @@ -3,3 +3,5 @@ export * from './Autocomplete'; export { default as autocompleteClasses } from './autocompleteClasses'; export * from './autocompleteClasses'; + +export { private_autocompleteVars } from './autocompleteVars'; diff --git a/packages/mui-material/src/Autocomplete/index.js b/packages/mui-material/src/Autocomplete/index.js index bc2577ca4c03c3..8472af6921db71 100644 --- a/packages/mui-material/src/Autocomplete/index.js +++ b/packages/mui-material/src/Autocomplete/index.js @@ -2,3 +2,5 @@ export { default, createFilterOptions } from './Autocomplete'; export { default as autocompleteClasses } from './autocompleteClasses'; export * from './autocompleteClasses'; + +export { private_autocompleteVars } from './autocompleteVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 2e9a96b0c263d0..0993b8deceb601 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -24,6 +24,7 @@ import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; +import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -173,6 +174,12 @@ export default function enhanceComfortDensity(theme: [tcVars.smallBlockPad]: d.xs, [tcVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAutocomplete', { + // Option list (mirrors MenuItem): minHeight raw px, block/inline pad steps. + [acVars.optionMinHeight]: '56px', + [acVars.optionBlockPad]: d.xs, + [acVars.optionInlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 7d35190096a1b6..36b2537da9f122 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -24,6 +24,7 @@ import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; +import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -173,6 +174,12 @@ export default function enhanceCompactDensity(theme: [tcVars.smallBlockPad]: d.xs, [tcVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAutocomplete', { + // Option list (mirrors MenuItem): minHeight raw px, block/inline pad steps. + [acVars.optionMinHeight]: '36px', + [acVars.optionBlockPad]: d.xs, + [acVars.optionInlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index ac488f0e1fa848..71bc757375d2f1 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -24,6 +24,7 @@ import { private_avatarVars as avVars } from '../Avatar/avatarVars'; import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; +import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -175,6 +176,12 @@ export default function enhanceNormalDensity(theme: [tcVars.smallBlockPad]: d.xs, [tcVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiAutocomplete', { + // Option list (mirrors MenuItem): minHeight raw px, block/inline pad steps. + [acVars.optionMinHeight]: '44px', + [acVars.optionBlockPad]: d.xs, + [acVars.optionInlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From c56b02639dea3b2cbe6df96d6d3220b32d47818d Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:05:41 +0700 Subject: [PATCH 078/114] density Stepper: tokenize Step gutter + StepLabel icon gap - Step horizontal paddingLeft/Right 8 -> --comp-inlinePad; StepLabel icon-container paddingRight 8 -> --comp-iconGap - stepVars (inlinePad) + stepLabelVars (iconGap); re-export js+d.ts; presets map both ->sm (normal 8 == today) - position overrides + connector left literal - playground Stepper entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 10 ++++ docs/pages/experiments/density-playground.tsx | 49 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 16 ++++++ packages/mui-material/src/Step/Step.js | 8 ++- packages/mui-material/src/Step/index.d.ts | 2 + packages/mui-material/src/Step/index.js | 2 + packages/mui-material/src/Step/stepVars.ts | 8 +++ .../mui-material/src/StepLabel/StepLabel.js | 7 ++- .../mui-material/src/StepLabel/index.d.ts | 2 + packages/mui-material/src/StepLabel/index.js | 2 + .../src/StepLabel/stepLabelVars.ts | 9 ++++ .../src/styles/enhanceComfortDensity.ts | 8 +++ .../src/styles/enhanceCompactDensity.ts | 8 +++ .../src/styles/enhanceNormalDensity.ts | 8 +++ 14 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 packages/mui-material/src/Step/stepVars.ts create mode 100644 packages/mui-material/src/StepLabel/stepLabelVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 4ba5a79c864118..539f510b346b09 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -132,6 +132,16 @@ const scopes: Record> = { dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, }, + Stepper: { + dense: { + ['--Step-inlinePad' as any]: '4px', + ['--StepLabel-iconGap' as any]: '4px', + }, + loose: { + ['--Step-inlinePad' as any]: '16px', + ['--StepLabel-iconGap' as any]: '16px', + }, + }, Autocomplete: { dense: { ['--Autocomplete-option-minHeight' as any]: '32px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index d11c85ee17b358..ba03bbc9b00f3d 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -41,6 +41,9 @@ import Alert, { private_alertVars } from '@mui/material/Alert'; import Chip, { private_chipVars } from '@mui/material/Chip'; import Avatar, { private_avatarVars } from '@mui/material/Avatar'; import Badge, { private_badgeVars } from '@mui/material/Badge'; +import Stepper from '@mui/material/Stepper'; +import Step, { private_stepVars } from '@mui/material/Step'; +import StepLabel, { private_stepLabelVars } from '@mui/material/StepLabel'; import Accordion from '@mui/material/Accordion'; import AccordionSummary, { private_accordionSummaryVars } from '@mui/material/AccordionSummary'; import AccordionDetails, { private_accordionDetailsVars } from '@mui/material/AccordionDetails'; @@ -1028,6 +1031,46 @@ function AutocompleteMatrix({ ); } +// Stepper family: Step horizontal gutter + StepLabel icon→label gap. +const STEPPER_FIELDS: DensityField[] = [ + { key: 'inlinePad', cssVar: private_stepVars.inlinePad }, + { key: 'iconGap', cssVar: private_stepLabelVars.iconGap }, +]; + +function StepperMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + STEPPER_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + + One + + + + + Two + + + + + Three + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1147,6 +1190,12 @@ const COMPONENT_DEFS = { prefill: { optionBlockPad: 'xs', optionInlinePad: 'lg' }, // minHeight raw px off theme renderMatrix: (args) => , }, + Stepper: { + canvasLabel: 'Stepper — step gutter + icon→label gap', + fields: STEPPER_FIELDS, + prefill: { inlinePad: 'sm', iconGap: 'sm' }, + renderMatrix: (args) => , + }, Badge: { canvasLabel: 'Badge — bubble size + padding (standard / dot)', fields: BADGE_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index b610364bea6bf8..c36375fbd3b119 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -34,6 +34,9 @@ import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; import TableRow from '@mui/material/TableRow'; import TableCell from '@mui/material/TableCell'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; @@ -197,6 +200,19 @@ const demos: Record = { ), + Stepper: ( + + + One + + + Two + + + Three + + + ), Autocomplete: ( // Open + inline so the option list (the density lever) renders in the scope. { const { classes, orientation, alternativeLabel, completed } = ownerState; @@ -34,17 +35,20 @@ const StepRoot = styled('li', { ]; }, })({ + // Density seam: horizontal step gutter over the 8px default. + '--_inlinePad': '8px', + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, variants: [ { props: { orientation: 'horizontal', alternativeLabel: false, hasConnector: false }, style: { - paddingLeft: 8, + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', }, }, { props: { orientation: 'horizontal', alternativeLabel: false, last: true }, style: { - paddingRight: 8, + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', }, }, { diff --git a/packages/mui-material/src/Step/index.d.ts b/packages/mui-material/src/Step/index.d.ts index 323cfcdff732df..2b36cd9bd58a40 100644 --- a/packages/mui-material/src/Step/index.d.ts +++ b/packages/mui-material/src/Step/index.d.ts @@ -4,5 +4,7 @@ export * from './Step'; export { default as stepClasses } from './stepClasses'; export * from './stepClasses'; +export { private_stepVars } from './stepVars'; + export { default as StepContext } from './StepContext'; export * from './StepContext'; diff --git a/packages/mui-material/src/Step/index.js b/packages/mui-material/src/Step/index.js index 5c8dc441973e51..0282c5957050ad 100644 --- a/packages/mui-material/src/Step/index.js +++ b/packages/mui-material/src/Step/index.js @@ -3,5 +3,7 @@ export { default } from './Step'; export { default as stepClasses } from './stepClasses'; export * from './stepClasses'; +export { private_stepVars } from './stepVars'; + export { default as StepContext } from './StepContext'; export * from './StepContext'; diff --git a/packages/mui-material/src/Step/stepVars.ts b/packages/mui-material/src/Step/stepVars.ts new file mode 100644 index 00000000000000..70626c3f09727b --- /dev/null +++ b/packages/mui-material/src/Step/stepVars.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Step density token identity — the horizontal step gutter (`paddingLeft`/ + * `paddingRight` 8px on first/last steps), over `--comp-inlinePad`. `private_*`. + */ +export const private_stepVars = { + inlinePad: '--Step-inlinePad', +} as const; diff --git a/packages/mui-material/src/StepLabel/StepLabel.js b/packages/mui-material/src/StepLabel/StepLabel.js index 655fb7537481f1..7c71127c5b1453 100644 --- a/packages/mui-material/src/StepLabel/StepLabel.js +++ b/packages/mui-material/src/StepLabel/StepLabel.js @@ -10,6 +10,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import stepLabelClasses, { getStepLabelUtilityClass } from './stepLabelClasses'; +import { private_stepLabelVars as vars } from './stepLabelVars'; import useSlot from '../utils/useSlot'; import { getTransitionStyles } from '../transitions/utils'; @@ -122,7 +123,11 @@ const StepLabelIconContainer = styled('span', { })({ flexShrink: 0, display: 'flex', - paddingRight: 8, + // Density seam: icon→label gap over the 8px default (position overrides below + // stay literal). + '--_iconGap': '8px', + '--comp-iconGap': `var(${vars.iconGap}, var(--_iconGap))`, + paddingRight: 'var(--comp-iconGap, var(--_iconGap))', [`&.${stepLabelClasses.alternativeLabel}`]: { paddingRight: 0, }, diff --git a/packages/mui-material/src/StepLabel/index.d.ts b/packages/mui-material/src/StepLabel/index.d.ts index f0a00aaa26d8dd..54acbf15bcdf96 100644 --- a/packages/mui-material/src/StepLabel/index.d.ts +++ b/packages/mui-material/src/StepLabel/index.d.ts @@ -3,3 +3,5 @@ export * from './StepLabel'; export { default as stepLabelClasses } from './stepLabelClasses'; export * from './stepLabelClasses'; + +export { private_stepLabelVars } from './stepLabelVars'; diff --git a/packages/mui-material/src/StepLabel/index.js b/packages/mui-material/src/StepLabel/index.js index b60fad9c328271..277711c38b30f1 100644 --- a/packages/mui-material/src/StepLabel/index.js +++ b/packages/mui-material/src/StepLabel/index.js @@ -2,3 +2,5 @@ export { default } from './StepLabel'; export { default as stepLabelClasses } from './stepLabelClasses'; export * from './stepLabelClasses'; + +export { private_stepLabelVars } from './stepLabelVars'; diff --git a/packages/mui-material/src/StepLabel/stepLabelVars.ts b/packages/mui-material/src/StepLabel/stepLabelVars.ts new file mode 100644 index 00000000000000..a2ab58c83d11f7 --- /dev/null +++ b/packages/mui-material/src/StepLabel/stepLabelVars.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * StepLabel density token identity — the icon→label gap (icon container + * `paddingRight` 8px), over `--comp-iconGap`. The alternativeLabel/vertical + * position overrides stay literal. `private_*` per the density RFC. + */ +export const private_stepLabelVars = { + iconGap: '--StepLabel-iconGap', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 0993b8deceb601..a65cc6af8c7adb 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -25,6 +25,8 @@ import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; +import { private_stepVars as stepVars } from '../Step/stepVars'; +import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -180,6 +182,12 @@ export default function enhanceComfortDensity(theme: [acVars.optionBlockPad]: d.xs, [acVars.optionInlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiStep', { + [stepVars.inlinePad]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiStepLabel', { + [slVars.iconGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 36b2537da9f122..63bc876b312af3 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -25,6 +25,8 @@ import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; +import { private_stepVars as stepVars } from '../Step/stepVars'; +import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -180,6 +182,12 @@ export default function enhanceCompactDensity(theme: [acVars.optionBlockPad]: d.xs, [acVars.optionInlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiStep', { + [stepVars.inlinePad]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiStepLabel', { + [slVars.iconGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 71bc757375d2f1..4812c1e3d9c5eb 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -25,6 +25,8 @@ import { private_badgeVars as badgeVars } from '../Badge/badgeVars'; import { private_buttonGroupVars as bgVars } from '../ButtonGroup/buttonGroupVars'; import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; +import { private_stepVars as stepVars } from '../Step/stepVars'; +import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -182,6 +184,12 @@ export default function enhanceNormalDensity(theme: [acVars.optionBlockPad]: d.xs, [acVars.optionInlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiStep', { + [stepVars.inlinePad]: d.sm, + }); + addRootOverride(enhanced.components, 'MuiStepLabel', { + [slVars.iconGap]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From bd2830e32774971468bde7c19278788889be7099 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:13:44 +0700 Subject: [PATCH 079/114] density AppBar/Toolbar: tokenize gutter padding + dense min-height - Toolbar gutter inline pad (16 base / 24 >=sm, rerouted at breakpoint) -> --comp-inlinePad; dense minHeight 48 -> --comp-minHeight - toolbarVars (inlinePad/wideInlinePad/denseMinHeight); re-export js+d.ts - presets map inlinePad->lg, wideInlinePad->xl (normal 16/24 == today), denseMinHeight raw px 40/48/56 - regular height stays theme.mixins.toolbar (fixed chrome) - playground Toolbar entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 +++++ docs/pages/experiments/density-playground.tsx | 48 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 16 +++++++ packages/mui-material/src/Toolbar/Toolbar.js | 17 +++++-- packages/mui-material/src/Toolbar/index.d.ts | 2 + packages/mui-material/src/Toolbar/index.js | 2 + .../mui-material/src/Toolbar/toolbarVars.ts | 12 +++++ .../src/styles/enhanceComfortDensity.ts | 7 +++ .../src/styles/enhanceCompactDensity.ts | 7 +++ .../src/styles/enhanceNormalDensity.ts | 7 +++ 10 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 packages/mui-material/src/Toolbar/toolbarVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 539f510b346b09..f334f35ba7ee0a 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -132,6 +132,18 @@ const scopes: Record> = { dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, }, + Toolbar: { + dense: { + ['--Toolbar-inlinePad' as any]: '8px', + ['--Toolbar-wideInlinePad' as any]: '12px', + ['--Toolbar-denseMinHeight' as any]: '40px', + }, + loose: { + ['--Toolbar-inlinePad' as any]: '24px', + ['--Toolbar-wideInlinePad' as any]: '40px', + ['--Toolbar-denseMinHeight' as any]: '56px', + }, + }, Stepper: { dense: { ['--Step-inlinePad' as any]: '4px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index ba03bbc9b00f3d..fbc8ca72e10449 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -41,6 +41,8 @@ import Alert, { private_alertVars } from '@mui/material/Alert'; import Chip, { private_chipVars } from '@mui/material/Chip'; import Avatar, { private_avatarVars } from '@mui/material/Avatar'; import Badge, { private_badgeVars } from '@mui/material/Badge'; +import AppBar from '@mui/material/AppBar'; +import Toolbar, { private_toolbarVars } from '@mui/material/Toolbar'; import Stepper from '@mui/material/Stepper'; import Step, { private_stepVars } from '@mui/material/Step'; import StepLabel, { private_stepLabelVars } from '@mui/material/StepLabel'; @@ -1071,6 +1073,46 @@ function StepperMatrix({ ); } +// Toolbar (AppBar) family: gutter inline padding (base + ≥sm) + dense min-height. +const TOOLBAR_FIELDS: DensityField[] = [ + { key: 'inlinePad', cssVar: private_toolbarVars.inlinePad }, + { key: 'wideInlinePad', cssVar: private_toolbarVars.wideInlinePad }, + { key: 'denseMinHeight', cssVar: private_toolbarVars.denseMinHeight }, +]; + +function ToolbarMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + TOOLBAR_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + + + Regular + + + + + + + Dense + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1196,6 +1238,12 @@ const COMPONENT_DEFS = { prefill: { inlinePad: 'sm', iconGap: 'sm' }, renderMatrix: (args) => , }, + Toolbar: { + canvasLabel: 'AppBar/Toolbar — gutter padding + dense min-height', + fields: TOOLBAR_FIELDS, + prefill: { inlinePad: 'lg', wideInlinePad: 'xl' }, // denseMinHeight raw px off theme + renderMatrix: (args) => , + }, Badge: { canvasLabel: 'Badge — bubble size + padding (standard / dot)', fields: BADGE_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index c36375fbd3b119..50612fefc594c9 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -34,6 +34,8 @@ import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; import TableRow from '@mui/material/TableRow'; import TableCell from '@mui/material/TableCell'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; import Stepper from '@mui/material/Stepper'; import Step from '@mui/material/Step'; import StepLabel from '@mui/material/StepLabel'; @@ -200,6 +202,20 @@ const demos: Record = { ), + Toolbar: ( + + + + Regular + + + + + Dense + + + + ), Stepper: ( diff --git a/packages/mui-material/src/Toolbar/Toolbar.js b/packages/mui-material/src/Toolbar/Toolbar.js index dbfde2fb54eb23..740338e4f3006b 100644 --- a/packages/mui-material/src/Toolbar/Toolbar.js +++ b/packages/mui-material/src/Toolbar/Toolbar.js @@ -7,6 +7,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getToolbarUtilityClass } from './toolbarClasses'; +import { private_toolbarVars as vars } from './toolbarVars'; const useUtilityClasses = (ownerState) => { const { classes, disableGutters, variant } = ownerState; @@ -35,11 +36,14 @@ const ToolbarRoot = styled('div', { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), + // Density seam: gutter inline padding (16 base / 24 ≥sm). + '--_inlinePad': theme.spacing(2), + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', [theme.breakpoints.up('sm')]: { - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3), + '--_inlinePad': theme.spacing(3), + '--comp-inlinePad': `var(${vars.wideInlinePad}, var(--_inlinePad))`, }, }, }, @@ -48,7 +52,10 @@ const ToolbarRoot = styled('div', { variant: 'dense', }, style: { - minHeight: 48, + // Dense bar min-height (raw px). Regular height stays the mixin below. + '--_minHeight': '48px', + '--comp-minHeight': `var(${vars.denseMinHeight}, var(--_minHeight))`, + minHeight: 'var(--comp-minHeight, var(--_minHeight))', }, }, { diff --git a/packages/mui-material/src/Toolbar/index.d.ts b/packages/mui-material/src/Toolbar/index.d.ts index ffebecfaa96526..221a0e984b8bf5 100644 --- a/packages/mui-material/src/Toolbar/index.d.ts +++ b/packages/mui-material/src/Toolbar/index.d.ts @@ -3,3 +3,5 @@ export * from './Toolbar'; export { default as toolbarClasses } from './toolbarClasses'; export * from './toolbarClasses'; + +export { private_toolbarVars } from './toolbarVars'; diff --git a/packages/mui-material/src/Toolbar/index.js b/packages/mui-material/src/Toolbar/index.js index e47b8023079b2f..de8711c437cca5 100644 --- a/packages/mui-material/src/Toolbar/index.js +++ b/packages/mui-material/src/Toolbar/index.js @@ -2,3 +2,5 @@ export { default } from './Toolbar'; export { default as toolbarClasses } from './toolbarClasses'; export * from './toolbarClasses'; + +export { private_toolbarVars } from './toolbarVars'; diff --git a/packages/mui-material/src/Toolbar/toolbarVars.ts b/packages/mui-material/src/Toolbar/toolbarVars.ts new file mode 100644 index 00000000000000..8d30910f9de904 --- /dev/null +++ b/packages/mui-material/src/Toolbar/toolbarVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Toolbar density token identities — the gutter inline padding (16 base / 24 ≥sm) + * + the `dense` variant min-height (48, raw px). The `regular` min-height comes + * from `theme.mixins.toolbar` (fixed app-bar chrome height) and stays literal. + * `private_*` per the density RFC. + */ +export const private_toolbarVars = { + inlinePad: '--Toolbar-inlinePad', + wideInlinePad: '--Toolbar-wideInlinePad', + denseMinHeight: '--Toolbar-denseMinHeight', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index a65cc6af8c7adb..14c25c8dc6b75f 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -27,6 +27,7 @@ import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; +import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -188,6 +189,12 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiStepLabel', { [slVars.iconGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToolbar', { + // Gutter inline pad (steps); dense bar min-height (raw px). + [toolbarVars.inlinePad]: d.lg, + [toolbarVars.wideInlinePad]: d.xl, + [toolbarVars.denseMinHeight]: '56px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 63bc876b312af3..63545855412670 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -27,6 +27,7 @@ import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; +import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -188,6 +189,12 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiStepLabel', { [slVars.iconGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToolbar', { + // Gutter inline pad (steps); dense bar min-height (raw px). + [toolbarVars.inlinePad]: d.lg, + [toolbarVars.wideInlinePad]: d.xl, + [toolbarVars.denseMinHeight]: '40px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 4812c1e3d9c5eb..2dc3f3c399eb3f 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -27,6 +27,7 @@ import { private_tableCellVars as tcVars } from '../TableCell/tableCellVars'; import { private_autocompleteVars as acVars } from '../Autocomplete/autocompleteVars'; import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; +import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -190,6 +191,12 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiStepLabel', { [slVars.iconGap]: d.sm, }); + addRootOverride(enhanced.components, 'MuiToolbar', { + // Gutter inline pad (steps); dense bar min-height (raw px). + [toolbarVars.inlinePad]: d.lg, + [toolbarVars.wideInlinePad]: d.xl, + [toolbarVars.denseMinHeight]: '48px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 5076b80bc36691c6df3fdb30d339fa8f4e3b27aa Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:19:52 +0700 Subject: [PATCH 080/114] density Fab: tokenize circular size per size (raw px) - width/height per size (base large 56, small 40, medium 48) -> --comp-size; fabVars; re-export js+d.ts - presets map raw px 36/44/52 . 40/48/56 . 44/52/64 (normal == today); extended variant left literal - playground Fab entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 ++++++ docs/pages/experiments/density-playground.tsx | 42 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 14 +++++++ packages/mui-material/src/Fab/Fab.js | 17 +++++--- packages/mui-material/src/Fab/fabVars.ts | 11 +++++ packages/mui-material/src/Fab/index.d.ts | 2 + packages/mui-material/src/Fab/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 7 ++++ .../src/styles/enhanceCompactDensity.ts | 7 ++++ .../src/styles/enhanceNormalDensity.ts | 7 ++++ 10 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 packages/mui-material/src/Fab/fabVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index f334f35ba7ee0a..349bb4f8163a9e 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + Fab: { + dense: { + ['--Fab-small-size' as any]: '32px', + ['--Fab-medium-size' as any]: '40px', + ['--Fab-large-size' as any]: '48px', + }, + loose: { + ['--Fab-small-size' as any]: '48px', + ['--Fab-medium-size' as any]: '56px', + ['--Fab-large-size' as any]: '64px', + }, + }, ButtonGroup: { dense: { ['--ButtonGroup-minWidth' as any]: '28px' }, loose: { ['--ButtonGroup-minWidth' as any]: '56px' }, diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index fbc8ca72e10449..fdbed7c1fe2d0d 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -4,6 +4,7 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button, { private_buttonVars } from '@mui/material/Button'; import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup'; +import Fab, { private_fabVars } from '@mui/material/Fab'; import Table from '@mui/material/Table'; import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; @@ -1113,6 +1114,41 @@ function ToolbarMatrix({ ); } +// Fab family: circular size per size (raw px). +const FAB_FIELDS: DensityField[] = [ + { key: 'smallSize', cssVar: private_fabVars.smallSize }, + { key: 'mediumSize', cssVar: private_fabVars.mediumSize }, + { key: 'largeSize', cssVar: private_fabVars.largeSize }, +]; + +function FabMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + FAB_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + + + + + + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1214,6 +1250,12 @@ const COMPONENT_DEFS = { prefill: {}, // size = raw px, read off the theme renderMatrix: (args) => , }, + Fab: { + canvasLabel: 'Fab — circular size (small / medium / large)', + fields: FAB_FIELDS, + prefill: {}, // sizes = raw px, read off the theme + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 50612fefc594c9..ecb04e124009a2 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -3,6 +3,7 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import ButtonGroup from '@mui/material/ButtonGroup'; +import Fab from '@mui/material/Fab'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -195,6 +196,19 @@ const demos: Record = { B ), + Fab: ( + + + + + + + + + + + + ), ButtonGroup: ( diff --git a/packages/mui-material/src/Fab/Fab.js b/packages/mui-material/src/Fab/Fab.js index 6e83b587cf093a..20894e3fc4ad45 100644 --- a/packages/mui-material/src/Fab/Fab.js +++ b/packages/mui-material/src/Fab/Fab.js @@ -6,6 +6,7 @@ import composeClasses from '@mui/utils/composeClasses'; import ButtonBase from '../ButtonBase'; import capitalize from '../utils/capitalize'; import fabClasses, { getFabUtilityClass } from './fabClasses'; +import { private_fabVars as vars } from './fabVars'; import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; @@ -59,8 +60,12 @@ const FabRoot = styled(ButtonBase, { borderRadius: '50%', padding: 0, minWidth: 0, - width: 56, - height: 56, + // Density seam: circular size (raw px) per size; base = large. Small/medium + // variants reroute + swap the default below. + '--_size': '56px', + '--comp-size': `var(${vars.largeSize}, var(--_size))`, + width: 'var(--comp-size, var(--_size))', + height: 'var(--comp-size, var(--_size))', zIndex: (theme.vars || theme).zIndex.fab, boxShadow: (theme.vars || theme).shadows[6], '&:active': { @@ -85,15 +90,15 @@ const FabRoot = styled(ButtonBase, { { props: { size: 'small' }, style: { - width: 40, - height: 40, + '--_size': '40px', + '--comp-size': `var(${vars.smallSize}, var(--_size))`, }, }, { props: { size: 'medium' }, style: { - width: 48, - height: 48, + '--_size': '48px', + '--comp-size': `var(${vars.mediumSize}, var(--_size))`, }, }, { diff --git a/packages/mui-material/src/Fab/fabVars.ts b/packages/mui-material/src/Fab/fabVars.ts new file mode 100644 index 00000000000000..56215bf25f1f72 --- /dev/null +++ b/packages/mui-material/src/Fab/fabVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * Fab density token identities — the circular size (width/height) per size + * (small 40 / medium 48 / large 56), over `--comp-size` (raw px per preset). + * The `extended` variant (auto width + inline padding) stays literal. `private_*`. + */ +export const private_fabVars = { + smallSize: '--Fab-small-size', + mediumSize: '--Fab-medium-size', + largeSize: '--Fab-large-size', +} as const; diff --git a/packages/mui-material/src/Fab/index.d.ts b/packages/mui-material/src/Fab/index.d.ts index b1751bf8ec2032..6567ac81a0a7eb 100644 --- a/packages/mui-material/src/Fab/index.d.ts +++ b/packages/mui-material/src/Fab/index.d.ts @@ -3,3 +3,5 @@ export * from './Fab'; export { default as fabClasses } from './fabClasses'; export * from './fabClasses'; + +export { private_fabVars } from './fabVars'; diff --git a/packages/mui-material/src/Fab/index.js b/packages/mui-material/src/Fab/index.js index e5fd203ad858c0..da640fb2666c6f 100644 --- a/packages/mui-material/src/Fab/index.js +++ b/packages/mui-material/src/Fab/index.js @@ -2,3 +2,5 @@ export { default } from './Fab'; export { default as fabClasses } from './fabClasses'; export * from './fabClasses'; + +export { private_fabVars } from './fabVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 14c25c8dc6b75f..7e81bd30b8e668 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -28,6 +28,7 @@ import { private_autocompleteVars as acVars } from '../Autocomplete/autocomplete import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; +import { private_fabVars as fabVars } from '../Fab/fabVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -195,6 +196,12 @@ export default function enhanceComfortDensity(theme: [toolbarVars.wideInlinePad]: d.xl, [toolbarVars.denseMinHeight]: '56px', }); + addRootOverride(enhanced.components, 'MuiFab', { + // Circular size = raw px per size (button-like action). + [fabVars.smallSize]: '44px', + [fabVars.mediumSize]: '52px', + [fabVars.largeSize]: '64px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 63545855412670..2f9a8a449299b0 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -28,6 +28,7 @@ import { private_autocompleteVars as acVars } from '../Autocomplete/autocomplete import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; +import { private_fabVars as fabVars } from '../Fab/fabVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -195,6 +196,12 @@ export default function enhanceCompactDensity(theme: [toolbarVars.wideInlinePad]: d.xl, [toolbarVars.denseMinHeight]: '40px', }); + addRootOverride(enhanced.components, 'MuiFab', { + // Circular size = raw px per size (button-like action). + [fabVars.smallSize]: '36px', + [fabVars.mediumSize]: '44px', + [fabVars.largeSize]: '52px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 2dc3f3c399eb3f..eacb8861be0e7e 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -28,6 +28,7 @@ import { private_autocompleteVars as acVars } from '../Autocomplete/autocomplete import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; +import { private_fabVars as fabVars } from '../Fab/fabVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -197,6 +198,12 @@ export default function enhanceNormalDensity(theme: [toolbarVars.wideInlinePad]: d.xl, [toolbarVars.denseMinHeight]: '48px', }); + addRootOverride(enhanced.components, 'MuiFab', { + // Circular size = raw px per size (button-like action). + [fabVars.smallSize]: '40px', + [fabVars.mediumSize]: '48px', + [fabVars.largeSize]: '56px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From d1ae26a5716a98ce13aa0a644e69952a4d309d18 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:25:40 +0700 Subject: [PATCH 081/114] density Pagination: tokenize PaginationItem box size per size (raw px) - PaginationItem page + ellipsis minWidth/height per size -> --comp-size (shared so they stay aligned); paginationItemVars; re-export js+d.ts - presets map raw px 22/28/36 . 26/32/40 . 30/36/44 (normal == today); padding/margin/radius left literal - playground Pagination entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 ++++++ docs/pages/experiments/density-playground.tsx | 40 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 8 ++++ .../src/PaginationItem/PaginationItem.js | 27 ++++++++----- .../src/PaginationItem/index.d.ts | 2 + .../mui-material/src/PaginationItem/index.js | 2 + .../src/PaginationItem/paginationItemVars.ts | 12 ++++++ .../src/styles/enhanceComfortDensity.ts | 7 ++++ .../src/styles/enhanceCompactDensity.ts | 7 ++++ .../src/styles/enhanceNormalDensity.ts | 7 ++++ 10 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 packages/mui-material/src/PaginationItem/paginationItemVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 349bb4f8163a9e..d56a35b1843391 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + PaginationItem: { + dense: { + ['--PaginationItem-small-size' as any]: '22px', + ['--PaginationItem-medium-size' as any]: '28px', + ['--PaginationItem-large-size' as any]: '34px', + }, + loose: { + ['--PaginationItem-small-size' as any]: '34px', + ['--PaginationItem-medium-size' as any]: '44px', + ['--PaginationItem-large-size' as any]: '52px', + }, + }, Fab: { dense: { ['--Fab-small-size' as any]: '32px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index fdbed7c1fe2d0d..26f6875eea8b2d 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -5,6 +5,8 @@ import Stack from '@mui/material/Stack'; import Button, { private_buttonVars } from '@mui/material/Button'; import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup'; import Fab, { private_fabVars } from '@mui/material/Fab'; +import Pagination from '@mui/material/Pagination'; +import { private_paginationItemVars } from '@mui/material/PaginationItem'; import Table from '@mui/material/Table'; import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; @@ -1149,6 +1151,38 @@ function FabMatrix({ ); } +// Pagination family: the item box size per size (shared page/ellipsis). +const PAGINATION_FIELDS: DensityField[] = [ + { key: 'smallSize', cssVar: private_paginationItemVars.smallSize }, + { key: 'mediumSize', cssVar: private_paginationItemVars.mediumSize }, + { key: 'largeSize', cssVar: private_paginationItemVars.largeSize }, +]; + +function PaginationMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + PAGINATION_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1256,6 +1290,12 @@ const COMPONENT_DEFS = { prefill: {}, // sizes = raw px, read off the theme renderMatrix: (args) => , }, + Pagination: { + canvasLabel: 'Pagination — item box size (small / medium / large)', + fields: PAGINATION_FIELDS, + prefill: {}, // sizes = raw px, read off the theme + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index ecb04e124009a2..064c9e4734d9cc 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -4,6 +4,7 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import ButtonGroup from '@mui/material/ButtonGroup'; import Fab from '@mui/material/Fab'; +import Pagination from '@mui/material/Pagination'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -209,6 +210,13 @@ const demos: Record = { ), + PaginationItem: ( + + + + + + ), ButtonGroup: ( diff --git a/packages/mui-material/src/PaginationItem/PaginationItem.js b/packages/mui-material/src/PaginationItem/PaginationItem.js index d013811ea287ff..d9a9ca96ef3fec 100644 --- a/packages/mui-material/src/PaginationItem/PaginationItem.js +++ b/packages/mui-material/src/PaginationItem/PaginationItem.js @@ -5,6 +5,7 @@ import clsx from 'clsx'; import composeClasses from '@mui/utils/composeClasses'; import { useRtl } from '@mui/system/RtlProvider'; import paginationItemClasses, { getPaginationItemUtilityClass } from './paginationItemClasses'; +import { private_paginationItemVars as vars } from './paginationItemVars'; import ButtonBase from '../ButtonBase'; import capitalize from '../utils/capitalize'; import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; @@ -71,7 +72,10 @@ const PaginationItemEllipsis = styled('div', { borderRadius: 32 / 2, textAlign: 'center', boxSizing: 'border-box', - minWidth: 32, + // Density seam: item box size (raw px per size), shared with the page button. + '--_size': '32px', + '--comp-size': `var(${vars.mediumSize}, var(--_size))`, + minWidth: 'var(--comp-size, var(--_size))', padding: '0 6px', margin: '0 3px', color: (theme.vars || theme).palette.text.primary, @@ -83,7 +87,8 @@ const PaginationItemEllipsis = styled('div', { { props: { size: 'small' }, style: { - minWidth: 26, + '--_size': '26px', + '--comp-size': `var(${vars.smallSize}, var(--_size))`, borderRadius: 26 / 2, margin: '0 1px', padding: '0 4px', @@ -92,7 +97,8 @@ const PaginationItemEllipsis = styled('div', { { props: { size: 'large' }, style: { - minWidth: 40, + '--_size': '40px', + '--comp-size': `var(${vars.largeSize}, var(--_size))`, borderRadius: 40 / 2, padding: '0 10px', fontSize: theme.typography.pxToRem(15), @@ -112,8 +118,11 @@ const PaginationItemPage = styled(ButtonBase, { borderRadius: 32 / 2, textAlign: 'center', boxSizing: 'border-box', - minWidth: 32, - height: 32, + // Density seam: item box size (raw px per size). + '--_size': '32px', + '--comp-size': `var(${vars.mediumSize}, var(--_size))`, + minWidth: 'var(--comp-size, var(--_size))', + height: 'var(--comp-size, var(--_size))', padding: '0 6px', margin: '0 3px', color: (theme.vars || theme).palette.text.primary, @@ -161,8 +170,8 @@ const PaginationItemPage = styled(ButtonBase, { { props: { size: 'small' }, style: { - minWidth: 26, - height: 26, + '--_size': '26px', + '--comp-size': `var(${vars.smallSize}, var(--_size))`, borderRadius: 26 / 2, margin: '0 1px', padding: '0 4px', @@ -171,8 +180,8 @@ const PaginationItemPage = styled(ButtonBase, { { props: { size: 'large' }, style: { - minWidth: 40, - height: 40, + '--_size': '40px', + '--comp-size': `var(${vars.largeSize}, var(--_size))`, borderRadius: 40 / 2, padding: '0 10px', fontSize: theme.typography.pxToRem(15), diff --git a/packages/mui-material/src/PaginationItem/index.d.ts b/packages/mui-material/src/PaginationItem/index.d.ts index 10694de421bfee..a3e33ca4d5c503 100644 --- a/packages/mui-material/src/PaginationItem/index.d.ts +++ b/packages/mui-material/src/PaginationItem/index.d.ts @@ -3,3 +3,5 @@ export * from './PaginationItem'; export { default as paginationItemClasses } from './paginationItemClasses'; export * from './paginationItemClasses'; + +export { private_paginationItemVars } from './paginationItemVars'; diff --git a/packages/mui-material/src/PaginationItem/index.js b/packages/mui-material/src/PaginationItem/index.js index 8cc1e9ecdcc739..d47f41b2928088 100644 --- a/packages/mui-material/src/PaginationItem/index.js +++ b/packages/mui-material/src/PaginationItem/index.js @@ -2,3 +2,5 @@ export { default } from './PaginationItem'; export { default as paginationItemClasses } from './paginationItemClasses'; export * from './paginationItemClasses'; + +export { private_paginationItemVars } from './paginationItemVars'; diff --git a/packages/mui-material/src/PaginationItem/paginationItemVars.ts b/packages/mui-material/src/PaginationItem/paginationItemVars.ts new file mode 100644 index 00000000000000..70481de46db906 --- /dev/null +++ b/packages/mui-material/src/PaginationItem/paginationItemVars.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * PaginationItem density token identities — the item box size (min-width/height, + * 32/26/40 per size) shared by the page button and the ellipsis, over + * `--comp-size` (raw px per preset). Inline padding + inter-item margin stay + * literal (secondary). `private_*` per the density RFC. + */ +export const private_paginationItemVars = { + smallSize: '--PaginationItem-small-size', + mediumSize: '--PaginationItem-medium-size', + largeSize: '--PaginationItem-large-size', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 7e81bd30b8e668..e8c53f94cec5a7 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -29,6 +29,7 @@ import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; +import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -202,6 +203,12 @@ export default function enhanceComfortDensity(theme: [fabVars.mediumSize]: '52px', [fabVars.largeSize]: '64px', }); + addRootOverride(enhanced.components, 'MuiPaginationItem', { + // Item box size = raw px per size. + [piVars.smallSize]: '30px', + [piVars.mediumSize]: '36px', + [piVars.largeSize]: '44px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 2f9a8a449299b0..388bee67a9181b 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -29,6 +29,7 @@ import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; +import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -202,6 +203,12 @@ export default function enhanceCompactDensity(theme: [fabVars.mediumSize]: '44px', [fabVars.largeSize]: '52px', }); + addRootOverride(enhanced.components, 'MuiPaginationItem', { + // Item box size = raw px per size. + [piVars.smallSize]: '22px', + [piVars.mediumSize]: '28px', + [piVars.largeSize]: '36px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index eacb8861be0e7e..89aa0f16a870b3 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -29,6 +29,7 @@ import { private_stepVars as stepVars } from '../Step/stepVars'; import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; +import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -204,6 +205,12 @@ export default function enhanceNormalDensity(theme: [fabVars.mediumSize]: '48px', [fabVars.largeSize]: '56px', }); + addRootOverride(enhanced.components, 'MuiPaginationItem', { + // Item box size = raw px per size. + [piVars.smallSize]: '26px', + [piVars.mediumSize]: '32px', + [piVars.largeSize]: '40px', + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From eb4be8620026f5860527e99cb8410ecb23499d04 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:29:46 +0700 Subject: [PATCH 082/114] density Snackbar (SnackbarContent): tokenize root padding - SnackbarContent root block/inline padding 6px 16px -> --comp-*; snackbarContentVars; re-export js+d.ts - presets map blockPad->xs, inlinePad->lg (normal 6/16 == today) - playground SnackbarContent entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 10 +++++ docs/pages/experiments/density-playground.tsx | 39 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 12 ++++++ .../src/SnackbarContent/SnackbarContent.js | 8 +++- .../src/SnackbarContent/index.d.ts | 2 + .../mui-material/src/SnackbarContent/index.js | 2 + .../SnackbarContent/snackbarContentVars.ts | 10 +++++ .../src/styles/enhanceComfortDensity.ts | 5 +++ .../src/styles/enhanceCompactDensity.ts | 5 +++ .../src/styles/enhanceNormalDensity.ts | 5 +++ 10 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/mui-material/src/SnackbarContent/snackbarContentVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index d56a35b1843391..38587ed671419f 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,16 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + SnackbarContent: { + dense: { + ['--SnackbarContent-blockPad' as any]: '2px', + ['--SnackbarContent-inlinePad' as any]: '10px', + }, + loose: { + ['--SnackbarContent-blockPad' as any]: '14px', + ['--SnackbarContent-inlinePad' as any]: '28px', + }, + }, PaginationItem: { dense: { ['--PaginationItem-small-size' as any]: '22px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 26f6875eea8b2d..174eae77592a53 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -7,6 +7,7 @@ import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup' import Fab, { private_fabVars } from '@mui/material/Fab'; import Pagination from '@mui/material/Pagination'; import { private_paginationItemVars } from '@mui/material/PaginationItem'; +import SnackbarContent, { private_snackbarContentVars } from '@mui/material/SnackbarContent'; import Table from '@mui/material/Table'; import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; @@ -1183,6 +1184,38 @@ function PaginationMatrix({ ); } +// SnackbarContent family: root block/inline padding (no size axis). +const SNACKBAR_FIELDS: DensityField[] = [ + { key: 'blockPad', cssVar: private_snackbarContentVars.blockPad }, + { key: 'inlinePad', cssVar: private_snackbarContentVars.inlinePad }, +]; + +function SnackbarMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + SNACKBAR_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + Something happened} + action={ + + } + sx={{ mt: 1, width: 320, ...sx }} + /> + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1296,6 +1329,12 @@ const COMPONENT_DEFS = { prefill: {}, // sizes = raw px, read off the theme renderMatrix: (args) => , }, + SnackbarContent: { + canvasLabel: 'SnackbarContent — root padding', + fields: SNACKBAR_FIELDS, + prefill: { blockPad: 'xs', inlinePad: 'lg' }, + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 064c9e4734d9cc..d6697f84bbe85b 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -5,6 +5,7 @@ import Button from '@mui/material/Button'; import ButtonGroup from '@mui/material/ButtonGroup'; import Fab from '@mui/material/Fab'; import Pagination from '@mui/material/Pagination'; +import SnackbarContent from '@mui/material/SnackbarContent'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -217,6 +218,17 @@ const demos: Record = {
), + SnackbarContent: ( + + Undo + + } + sx={{ width: 320 }} + /> + ), ButtonGroup: ( diff --git a/packages/mui-material/src/SnackbarContent/SnackbarContent.js b/packages/mui-material/src/SnackbarContent/SnackbarContent.js index e685e5aab1faba..355c7a64206d0d 100644 --- a/packages/mui-material/src/SnackbarContent/SnackbarContent.js +++ b/packages/mui-material/src/SnackbarContent/SnackbarContent.js @@ -9,6 +9,7 @@ import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import Paper from '../Paper'; import { getSnackbarContentUtilityClass } from './snackbarContentClasses'; +import { private_snackbarContentVars as vars } from './snackbarContentVars'; const useUtilityClasses = (ownerState) => { const { classes } = ownerState; @@ -40,7 +41,12 @@ const SnackbarContentRoot = styled(Paper, { display: 'flex', alignItems: 'center', flexWrap: 'wrap', - padding: '6px 16px', + // Density seams: root block/inline padding (no size axis). + '--_blockPad': '6px', + '--_inlinePad': '16px', + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', flexGrow: 1, [theme.breakpoints.up('sm')]: { flexGrow: 'initial', diff --git a/packages/mui-material/src/SnackbarContent/index.d.ts b/packages/mui-material/src/SnackbarContent/index.d.ts index 658d42f88a58ea..5d0a2e8b440431 100644 --- a/packages/mui-material/src/SnackbarContent/index.d.ts +++ b/packages/mui-material/src/SnackbarContent/index.d.ts @@ -3,3 +3,5 @@ export * from './SnackbarContent'; export { default as snackbarContentClasses } from './snackbarContentClasses'; export * from './snackbarContentClasses'; + +export { private_snackbarContentVars } from './snackbarContentVars'; diff --git a/packages/mui-material/src/SnackbarContent/index.js b/packages/mui-material/src/SnackbarContent/index.js index 44f55061a70824..294792428459fa 100644 --- a/packages/mui-material/src/SnackbarContent/index.js +++ b/packages/mui-material/src/SnackbarContent/index.js @@ -2,3 +2,5 @@ export { default } from './SnackbarContent'; export { default as snackbarContentClasses } from './snackbarContentClasses'; export * from './snackbarContentClasses'; + +export { private_snackbarContentVars } from './snackbarContentVars'; diff --git a/packages/mui-material/src/SnackbarContent/snackbarContentVars.ts b/packages/mui-material/src/SnackbarContent/snackbarContentVars.ts new file mode 100644 index 00000000000000..c92a24166f66c2 --- /dev/null +++ b/packages/mui-material/src/SnackbarContent/snackbarContentVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * SnackbarContent density token identities — the root block/inline padding + * (`6px 16px`), over the agnostic `--comp-*` seams (no size axis). The message + * vertical alignment padding + action gap stay literal. `private_*`. + */ +export const private_snackbarContentVars = { + blockPad: '--SnackbarContent-blockPad', + inlinePad: '--SnackbarContent-inlinePad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index e8c53f94cec5a7..2c95afbc180da9 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -30,6 +30,7 @@ import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; +import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -209,6 +210,10 @@ export default function enhanceComfortDensity(theme: [piVars.mediumSize]: '36px', [piVars.largeSize]: '44px', }); + addRootOverride(enhanced.components, 'MuiSnackbarContent', { + [scVars.blockPad]: d.xs, + [scVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 388bee67a9181b..758c32b9e2c347 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -30,6 +30,7 @@ import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; +import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -209,6 +210,10 @@ export default function enhanceCompactDensity(theme: [piVars.mediumSize]: '28px', [piVars.largeSize]: '36px', }); + addRootOverride(enhanced.components, 'MuiSnackbarContent', { + [scVars.blockPad]: d.xs, + [scVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 89aa0f16a870b3..024d93a6ccfd43 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -30,6 +30,7 @@ import { private_stepLabelVars as slVars } from '../StepLabel/stepLabelVars'; import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; +import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -211,6 +212,10 @@ export default function enhanceNormalDensity(theme: [piVars.mediumSize]: '32px', [piVars.largeSize]: '40px', }); + addRootOverride(enhanced.components, 'MuiSnackbarContent', { + [scVars.blockPad]: d.xs, + [scVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 0ec3c63cc8b4f39f90efe1ab0a11e7f4469b6d9c Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:34:37 +0700 Subject: [PATCH 083/114] density BottomNavigation: tokenize bar height + action inline padding - BottomNavigation height 56 -> --comp-height (raw px); BottomNavigationAction padding 0 12px -> --comp-inlinePad - bottomNavigationVars + bottomNavigationActionVars; re-export js+d.ts; presets height 44/56/64, inlinePad->md (normal == today) - playground BottomNavigation entry; fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 10 +++++ docs/pages/experiments/density-playground.tsx | 41 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 9 ++++ .../src/BottomNavigation/BottomNavigation.js | 6 ++- .../BottomNavigation/bottomNavigationVars.ts | 8 ++++ .../src/BottomNavigation/index.d.ts | 2 + .../src/BottomNavigation/index.js | 2 + .../BottomNavigationAction.js | 6 ++- .../bottomNavigationActionVars.ts | 8 ++++ .../src/BottomNavigationAction/index.d.ts | 2 + .../src/BottomNavigationAction/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 8 ++++ .../src/styles/enhanceCompactDensity.ts | 8 ++++ .../src/styles/enhanceNormalDensity.ts | 8 ++++ 14 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material/src/BottomNavigation/bottomNavigationVars.ts create mode 100644 packages/mui-material/src/BottomNavigationAction/bottomNavigationActionVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index 38587ed671419f..fc1f0844827102 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,16 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + BottomNavigation: { + dense: { + ['--BottomNavigation-height' as any]: '44px', + ['--BottomNavigationAction-inlinePad' as any]: '8px', + }, + loose: { + ['--BottomNavigation-height' as any]: '72px', + ['--BottomNavigationAction-inlinePad' as any]: '20px', + }, + }, SnackbarContent: { dense: { ['--SnackbarContent-blockPad' as any]: '2px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 174eae77592a53..995423e675aa4c 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -8,6 +8,10 @@ import Fab, { private_fabVars } from '@mui/material/Fab'; import Pagination from '@mui/material/Pagination'; import { private_paginationItemVars } from '@mui/material/PaginationItem'; import SnackbarContent, { private_snackbarContentVars } from '@mui/material/SnackbarContent'; +import BottomNavigation, { private_bottomNavigationVars } from '@mui/material/BottomNavigation'; +import BottomNavigationAction, { + private_bottomNavigationActionVars, +} from '@mui/material/BottomNavigationAction'; import Table from '@mui/material/Table'; import TableHead from '@mui/material/TableHead'; import TableBody from '@mui/material/TableBody'; @@ -1216,6 +1220,37 @@ function SnackbarMatrix({ ); } +// BottomNavigation family: bar height + action inline padding. +const BOTTOM_NAV_FIELDS: DensityField[] = [ + { key: 'height', cssVar: private_bottomNavigationVars.height }, + { key: 'inlinePad', cssVar: private_bottomNavigationActionVars.inlinePad }, +]; + +function BottomNavigationMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + BOTTOM_NAV_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + } /> + } /> + } /> + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1335,6 +1370,12 @@ const COMPONENT_DEFS = { prefill: { blockPad: 'xs', inlinePad: 'lg' }, renderMatrix: (args) => , }, + BottomNavigation: { + canvasLabel: 'BottomNavigation — bar height + action inline padding', + fields: BOTTOM_NAV_FIELDS, + prefill: { inlinePad: 'md' }, // height raw px off theme + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index d6697f84bbe85b..d6da803be9b760 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -6,6 +6,8 @@ import ButtonGroup from '@mui/material/ButtonGroup'; import Fab from '@mui/material/Fab'; import Pagination from '@mui/material/Pagination'; import SnackbarContent from '@mui/material/SnackbarContent'; +import BottomNavigation from '@mui/material/BottomNavigation'; +import BottomNavigationAction from '@mui/material/BottomNavigationAction'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -218,6 +220,13 @@ const demos: Record = { ), + BottomNavigation: ( + + } /> + } /> + } /> + + ), SnackbarContent: ( { const { classes } = ownerState; @@ -26,7 +27,10 @@ const BottomNavigationRoot = styled('div', { memoTheme(({ theme }) => ({ display: 'flex', justifyContent: 'center', - height: 56, + // Density seam: bar height (raw px per preset). + '--_height': '56px', + '--comp-height': `var(${vars.height}, var(--_height))`, + height: 'var(--comp-height, var(--_height))', backgroundColor: (theme.vars || theme).palette.background.paper, })), ); diff --git a/packages/mui-material/src/BottomNavigation/bottomNavigationVars.ts b/packages/mui-material/src/BottomNavigation/bottomNavigationVars.ts new file mode 100644 index 00000000000000..7781b9d06780c1 --- /dev/null +++ b/packages/mui-material/src/BottomNavigation/bottomNavigationVars.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * BottomNavigation density token identity — the bar height (56, raw px per + * preset), over `--comp-height`. `private_*` per the density RFC. + */ +export const private_bottomNavigationVars = { + height: '--BottomNavigation-height', +} as const; diff --git a/packages/mui-material/src/BottomNavigation/index.d.ts b/packages/mui-material/src/BottomNavigation/index.d.ts index cbf2cd3907412c..3341795d732d09 100644 --- a/packages/mui-material/src/BottomNavigation/index.d.ts +++ b/packages/mui-material/src/BottomNavigation/index.d.ts @@ -3,3 +3,5 @@ export * from './BottomNavigation'; export { default as bottomNavigationClasses } from './bottomNavigationClasses'; export * from './bottomNavigationClasses'; + +export { private_bottomNavigationVars } from './bottomNavigationVars'; diff --git a/packages/mui-material/src/BottomNavigation/index.js b/packages/mui-material/src/BottomNavigation/index.js index e35a344d097b26..2af301bfc76b2b 100644 --- a/packages/mui-material/src/BottomNavigation/index.js +++ b/packages/mui-material/src/BottomNavigation/index.js @@ -2,3 +2,5 @@ export { default } from './BottomNavigation'; export { default as bottomNavigationClasses } from './bottomNavigationClasses'; export * from './bottomNavigationClasses'; + +export { private_bottomNavigationVars } from './bottomNavigationVars'; diff --git a/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js b/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js index 83d9e0eb529832..2a87304ed5a70b 100644 --- a/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js +++ b/packages/mui-material/src/BottomNavigationAction/BottomNavigationAction.js @@ -12,6 +12,7 @@ import { getTransitionStyles } from '../transitions/utils'; import bottomNavigationActionClasses, { getBottomNavigationActionUtilityClass, } from './bottomNavigationActionClasses'; +import { private_bottomNavigationActionVars as vars } from './bottomNavigationActionVars'; import useSlot from '../utils/useSlot'; const useUtilityClasses = (ownerState) => { @@ -38,7 +39,10 @@ const BottomNavigationActionRoot = styled(ButtonBase, { ...getTransitionStyles(theme, ['color', 'padding-top'], { duration: theme.transitions.duration.short, }), - padding: '0px 12px', + // Density seam: inline padding (0 block / 12 inline). + '--_inlinePad': '12px', + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + padding: '0px var(--comp-inlinePad, var(--_inlinePad))', minWidth: 80, maxWidth: 168, color: (theme.vars || theme).palette.text.secondary, diff --git a/packages/mui-material/src/BottomNavigationAction/bottomNavigationActionVars.ts b/packages/mui-material/src/BottomNavigationAction/bottomNavigationActionVars.ts new file mode 100644 index 00000000000000..24eea97e369201 --- /dev/null +++ b/packages/mui-material/src/BottomNavigationAction/bottomNavigationActionVars.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * BottomNavigationAction density token identity — the inline padding (`0 12px`), + * over `--comp-inlinePad`. `private_*` per the density RFC. + */ +export const private_bottomNavigationActionVars = { + inlinePad: '--BottomNavigationAction-inlinePad', +} as const; diff --git a/packages/mui-material/src/BottomNavigationAction/index.d.ts b/packages/mui-material/src/BottomNavigationAction/index.d.ts index 634c4ea8cbf592..d75422b51f1a95 100644 --- a/packages/mui-material/src/BottomNavigationAction/index.d.ts +++ b/packages/mui-material/src/BottomNavigationAction/index.d.ts @@ -3,3 +3,5 @@ export * from './BottomNavigationAction'; export { default as bottomNavigationActionClasses } from './bottomNavigationActionClasses'; export * from './bottomNavigationActionClasses'; + +export { private_bottomNavigationActionVars } from './bottomNavigationActionVars'; diff --git a/packages/mui-material/src/BottomNavigationAction/index.js b/packages/mui-material/src/BottomNavigationAction/index.js index 377af4979d46ac..83fe51b2f6513b 100644 --- a/packages/mui-material/src/BottomNavigationAction/index.js +++ b/packages/mui-material/src/BottomNavigationAction/index.js @@ -2,3 +2,5 @@ export { default } from './BottomNavigationAction'; export { default as bottomNavigationActionClasses } from './bottomNavigationActionClasses'; export * from './bottomNavigationActionClasses'; + +export { private_bottomNavigationActionVars } from './bottomNavigationActionVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 2c95afbc180da9..366d39dbbd4e7a 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -31,6 +31,8 @@ import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; +import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; +import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -214,6 +216,12 @@ export default function enhanceComfortDensity(theme: [scVars.blockPad]: d.xs, [scVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiBottomNavigation', { + [bnVars.height]: '64px', + }); + addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { + [bnaVars.inlinePad]: d.md, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 758c32b9e2c347..80d8f9c04bd4d7 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -31,6 +31,8 @@ import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; +import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; +import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -214,6 +216,12 @@ export default function enhanceCompactDensity(theme: [scVars.blockPad]: d.xs, [scVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiBottomNavigation', { + [bnVars.height]: '48px', + }); + addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { + [bnaVars.inlinePad]: d.md, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 024d93a6ccfd43..7877055b648d21 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -31,6 +31,8 @@ import { private_toolbarVars as toolbarVars } from '../Toolbar/toolbarVars'; import { private_fabVars as fabVars } from '../Fab/fabVars'; import { private_paginationItemVars as piVars } from '../PaginationItem/paginationItemVars'; import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; +import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; +import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -216,6 +218,12 @@ export default function enhanceNormalDensity(theme: [scVars.blockPad]: d.xs, [scVars.inlinePad]: d.lg, }); + addRootOverride(enhanced.components, 'MuiBottomNavigation', { + [bnVars.height]: '56px', + }); + addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { + [bnaVars.inlinePad]: d.md, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 27147cfc28422aa20136f98c8e8f4fdfc0e1572f Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:41:06 +0700 Subject: [PATCH 084/114] density Dialog: tokenize title/content/actions padding - DialogTitle (16x24) + DialogContent (20x24) block/inline pad + DialogActions (8) pad, each over --comp-* - dialogTitleVars/dialogContentVars/dialogActionsVars; re-export js+d.ts - presets: title lg/xl, content lg/xl, actions sm (normal == today except content block 16 vs 20, flagged) - dividers variant + paddingTop-0 reset + action gap left literal - playground Dialog entry (Paper surface, no modal); fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 16 ++++++ docs/pages/experiments/density-playground.tsx | 54 +++++++++++++++++++ docs/src/modules/components/densityDemos.tsx | 16 ++++++ .../src/DialogActions/DialogActions.js | 6 ++- .../src/DialogActions/dialogActionsVars.ts | 8 +++ .../mui-material/src/DialogActions/index.d.ts | 2 + .../mui-material/src/DialogActions/index.js | 2 + .../src/DialogContent/DialogContent.js | 8 ++- .../src/DialogContent/dialogContentVars.ts | 10 ++++ .../mui-material/src/DialogContent/index.d.ts | 2 + .../mui-material/src/DialogContent/index.js | 2 + .../src/DialogTitle/DialogTitle.js | 8 ++- .../src/DialogTitle/dialogTitleVars.ts | 9 ++++ .../mui-material/src/DialogTitle/index.d.ts | 2 + .../mui-material/src/DialogTitle/index.js | 2 + .../src/styles/enhanceComfortDensity.ts | 14 +++++ .../src/styles/enhanceCompactDensity.ts | 14 +++++ .../src/styles/enhanceNormalDensity.ts | 14 +++++ 18 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 packages/mui-material/src/DialogActions/dialogActionsVars.ts create mode 100644 packages/mui-material/src/DialogContent/dialogContentVars.ts create mode 100644 packages/mui-material/src/DialogTitle/dialogTitleVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index fc1f0844827102..fb1277d4bff6c7 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,22 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + Dialog: { + dense: { + ['--DialogTitle-blockPad' as any]: '10px', + ['--DialogTitle-inlinePad' as any]: '16px', + ['--DialogContent-blockPad' as any]: '12px', + ['--DialogContent-inlinePad' as any]: '16px', + ['--DialogActions-pad' as any]: '4px', + }, + loose: { + ['--DialogTitle-blockPad' as any]: '24px', + ['--DialogTitle-inlinePad' as any]: '32px', + ['--DialogContent-blockPad' as any]: '28px', + ['--DialogContent-inlinePad' as any]: '32px', + ['--DialogActions-pad' as any]: '16px', + }, + }, BottomNavigation: { dense: { ['--BottomNavigation-height' as any]: '44px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 995423e675aa4c..0d66a617ea72db 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -41,6 +41,10 @@ import Checkbox, { private_checkboxVars } from '@mui/material/Checkbox'; import Radio, { private_radioVars } from '@mui/material/Radio'; import Breadcrumbs, { private_breadcrumbsVars } from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; +import Paper from '@mui/material/Paper'; +import DialogTitle, { private_dialogTitleVars } from '@mui/material/DialogTitle'; +import DialogContent, { private_dialogContentVars } from '@mui/material/DialogContent'; +import DialogActions, { private_dialogActionsVars } from '@mui/material/DialogActions'; import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; import Select, { private_selectVars } from '@mui/material/Select'; @@ -1251,6 +1255,44 @@ function BottomNavigationMatrix({ ); } +// Dialog family: title + content block/inline padding + actions padding. +const DIALOG_FIELDS: DensityField[] = [ + { key: 'titleBlockPad', cssVar: private_dialogTitleVars.blockPad }, + { key: 'titleInlinePad', cssVar: private_dialogTitleVars.inlinePad }, + { key: 'contentBlockPad', cssVar: private_dialogContentVars.blockPad }, + { key: 'contentInlinePad', cssVar: private_dialogContentVars.inlinePad }, + { key: 'actionsPad', cssVar: private_dialogActionsVars.pad }, +]; + +function DialogMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + DIALOG_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), + ) + : undefined; + return ( + + + Dialog title + + + Dialog content body text goes here. + + + + + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1376,6 +1418,18 @@ const COMPONENT_DEFS = { prefill: { inlinePad: 'md' }, // height raw px off theme renderMatrix: (args) => , }, + Dialog: { + canvasLabel: 'Dialog — title / content / actions padding', + fields: DIALOG_FIELDS, + prefill: { + titleBlockPad: 'lg', + titleInlinePad: 'xl', + contentBlockPad: 'lg', + contentInlinePad: 'xl', + actionsPad: 'sm', + }, + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index d6da803be9b760..34deced78363bb 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -22,6 +22,10 @@ import Checkbox from '@mui/material/Checkbox'; import Radio from '@mui/material/Radio'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Paper from '@mui/material/Paper'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; @@ -220,6 +224,18 @@ const demos: Record = { ), + Dialog: ( + // The dialog surface (Paper) with its 3 slots — no modal/portal so it renders + // inline in the scope; density lives in the title/content/actions padding. + + Dialog title + Dialog content body text goes here. + + + + + + ), BottomNavigation: ( } /> diff --git a/packages/mui-material/src/DialogActions/DialogActions.js b/packages/mui-material/src/DialogActions/DialogActions.js index 8e1b730bc4c096..b5ba611fe10615 100644 --- a/packages/mui-material/src/DialogActions/DialogActions.js +++ b/packages/mui-material/src/DialogActions/DialogActions.js @@ -6,6 +6,7 @@ import composeClasses from '@mui/utils/composeClasses'; import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getDialogActionsUtilityClass } from './dialogActionsClasses'; +import { private_dialogActionsVars as vars } from './dialogActionsVars'; const useUtilityClasses = (ownerState) => { const { classes, disableSpacing } = ownerState; @@ -28,7 +29,10 @@ const DialogActionsRoot = styled('div', { })({ display: 'flex', alignItems: 'center', - padding: 8, + // Density seam: action-bar padding. + '--_pad': '8px', + '--comp-pad': `var(${vars.pad}, var(--_pad))`, + padding: 'var(--comp-pad, var(--_pad))', justifyContent: 'flex-end', flex: '0 0 auto', variants: [ diff --git a/packages/mui-material/src/DialogActions/dialogActionsVars.ts b/packages/mui-material/src/DialogActions/dialogActionsVars.ts new file mode 100644 index 00000000000000..815bc01f641fbd --- /dev/null +++ b/packages/mui-material/src/DialogActions/dialogActionsVars.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * DialogActions density token identity — the action-bar padding (8), over + * `--comp-pad`. The inter-action gap stays literal. `private_*` per the RFC. + */ +export const private_dialogActionsVars = { + pad: '--DialogActions-pad', +} as const; diff --git a/packages/mui-material/src/DialogActions/index.d.ts b/packages/mui-material/src/DialogActions/index.d.ts index 0ab1e25b9cbb93..26b19fe3dbbd3b 100644 --- a/packages/mui-material/src/DialogActions/index.d.ts +++ b/packages/mui-material/src/DialogActions/index.d.ts @@ -3,3 +3,5 @@ export * from './DialogActions'; export { default as dialogActionsClasses } from './dialogActionsClasses'; export * from './dialogActionsClasses'; + +export { private_dialogActionsVars } from './dialogActionsVars'; diff --git a/packages/mui-material/src/DialogActions/index.js b/packages/mui-material/src/DialogActions/index.js index e01426ff8b3ecb..b9e57b37f1ee1f 100644 --- a/packages/mui-material/src/DialogActions/index.js +++ b/packages/mui-material/src/DialogActions/index.js @@ -2,3 +2,5 @@ export { default } from './DialogActions'; export { default as dialogActionsClasses } from './dialogActionsClasses'; export * from './dialogActionsClasses'; + +export { private_dialogActionsVars } from './dialogActionsVars'; diff --git a/packages/mui-material/src/DialogContent/DialogContent.js b/packages/mui-material/src/DialogContent/DialogContent.js index c5acfb7c3ef304..fd0f911cd552a5 100644 --- a/packages/mui-material/src/DialogContent/DialogContent.js +++ b/packages/mui-material/src/DialogContent/DialogContent.js @@ -7,6 +7,7 @@ import { styled } from '../zero-styled'; import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getDialogContentUtilityClass } from './dialogContentClasses'; +import { private_dialogContentVars as vars } from './dialogContentVars'; import dialogTitleClasses from '../DialogTitle/dialogTitleClasses'; const useUtilityClasses = (ownerState) => { @@ -33,7 +34,12 @@ const DialogContentRoot = styled('div', { // Add iOS momentum scrolling for iOS < 13.0 WebkitOverflowScrolling: 'touch', overflowY: 'auto', - padding: '20px 24px', + // Density seams: block/inline padding (dividers variant stays literal below). + '--_blockPad': '20px', + '--_inlinePad': '24px', + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', variants: [ { props: ({ ownerState }) => ownerState.dividers, diff --git a/packages/mui-material/src/DialogContent/dialogContentVars.ts b/packages/mui-material/src/DialogContent/dialogContentVars.ts new file mode 100644 index 00000000000000..0b0015026ca132 --- /dev/null +++ b/packages/mui-material/src/DialogContent/dialogContentVars.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * DialogContent density token identities — block/inline padding (`20px 24px`), + * over the agnostic `--comp-*` seams. The `dividers` variant + `& + &` paddingTop + * reset stay literal. `private_*` per the density RFC. + */ +export const private_dialogContentVars = { + blockPad: '--DialogContent-blockPad', + inlinePad: '--DialogContent-inlinePad', +} as const; diff --git a/packages/mui-material/src/DialogContent/index.d.ts b/packages/mui-material/src/DialogContent/index.d.ts index 9dcabf854bd99f..69fa5c933e3b55 100644 --- a/packages/mui-material/src/DialogContent/index.d.ts +++ b/packages/mui-material/src/DialogContent/index.d.ts @@ -3,3 +3,5 @@ export * from './DialogContent'; export { default as dialogContentClasses } from './dialogContentClasses'; export * from './dialogContentClasses'; + +export { private_dialogContentVars } from './dialogContentVars'; diff --git a/packages/mui-material/src/DialogContent/index.js b/packages/mui-material/src/DialogContent/index.js index 6a64cd0760cb1e..0beeb1b30c679d 100644 --- a/packages/mui-material/src/DialogContent/index.js +++ b/packages/mui-material/src/DialogContent/index.js @@ -2,3 +2,5 @@ export { default } from './DialogContent'; export { default as dialogContentClasses } from './dialogContentClasses'; export * from './dialogContentClasses'; + +export { private_dialogContentVars } from './dialogContentVars'; diff --git a/packages/mui-material/src/DialogTitle/DialogTitle.js b/packages/mui-material/src/DialogTitle/DialogTitle.js index b40d2f5af7b2ca..ef4da31351284b 100644 --- a/packages/mui-material/src/DialogTitle/DialogTitle.js +++ b/packages/mui-material/src/DialogTitle/DialogTitle.js @@ -7,6 +7,7 @@ import Typography from '../Typography'; import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import { getDialogTitleUtilityClass } from './dialogTitleClasses'; +import { private_dialogTitleVars as vars } from './dialogTitleVars'; import DialogContext from '../Dialog/DialogContext'; const useUtilityClasses = (ownerState) => { @@ -23,7 +24,12 @@ const DialogTitleRoot = styled(Typography, { name: 'MuiDialogTitle', slot: 'Root', })({ - padding: '16px 24px', + // Density seams: block/inline padding. + '--_blockPad': '16px', + '--_inlinePad': '24px', + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + padding: 'var(--comp-blockPad, var(--_blockPad)) var(--comp-inlinePad, var(--_inlinePad))', flex: '0 0 auto', }); diff --git a/packages/mui-material/src/DialogTitle/dialogTitleVars.ts b/packages/mui-material/src/DialogTitle/dialogTitleVars.ts new file mode 100644 index 00000000000000..a53cfaba9a720d --- /dev/null +++ b/packages/mui-material/src/DialogTitle/dialogTitleVars.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * DialogTitle density token identities — block/inline padding (`16px 24px`), + * over the agnostic `--comp-*` seams. `private_*` per the density RFC. + */ +export const private_dialogTitleVars = { + blockPad: '--DialogTitle-blockPad', + inlinePad: '--DialogTitle-inlinePad', +} as const; diff --git a/packages/mui-material/src/DialogTitle/index.d.ts b/packages/mui-material/src/DialogTitle/index.d.ts index 41da2f6fb80089..bdf9c1462c2a8c 100644 --- a/packages/mui-material/src/DialogTitle/index.d.ts +++ b/packages/mui-material/src/DialogTitle/index.d.ts @@ -3,3 +3,5 @@ export * from './DialogTitle'; export { default as dialogTitleClasses } from './dialogTitleClasses'; export * from './dialogTitleClasses'; + +export { private_dialogTitleVars } from './dialogTitleVars'; diff --git a/packages/mui-material/src/DialogTitle/index.js b/packages/mui-material/src/DialogTitle/index.js index 05be8d699e12dd..3a42b180ca69cc 100644 --- a/packages/mui-material/src/DialogTitle/index.js +++ b/packages/mui-material/src/DialogTitle/index.js @@ -2,3 +2,5 @@ export { default } from './DialogTitle'; export { default as dialogTitleClasses } from './dialogTitleClasses'; export * from './dialogTitleClasses'; + +export { private_dialogTitleVars } from './dialogTitleVars'; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 366d39dbbd4e7a..4af207cb9e3a47 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -33,6 +33,9 @@ import { private_paginationItemVars as piVars } from '../PaginationItem/paginati import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; +import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; +import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; +import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -222,6 +225,17 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { [bnaVars.inlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiDialogTitle', { + [dtVars.blockPad]: d.lg, + [dtVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogContent', { + [dcVars.blockPad]: d.lg, + [dcVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogActions', { + [daVars.pad]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index 80d8f9c04bd4d7..b4a898fc707a3a 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -33,6 +33,9 @@ import { private_paginationItemVars as piVars } from '../PaginationItem/paginati import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; +import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; +import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; +import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -222,6 +225,17 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { [bnaVars.inlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiDialogTitle', { + [dtVars.blockPad]: d.lg, + [dtVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogContent', { + [dcVars.blockPad]: d.lg, + [dcVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogActions', { + [daVars.pad]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index 7877055b648d21..eac08e49272300 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -33,6 +33,9 @@ import { private_paginationItemVars as piVars } from '../PaginationItem/paginati import { private_snackbarContentVars as scVars } from '../SnackbarContent/snackbarContentVars'; import { private_bottomNavigationVars as bnVars } from '../BottomNavigation/bottomNavigationVars'; import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigationAction/bottomNavigationActionVars'; +import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; +import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; +import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -224,6 +227,17 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiBottomNavigationAction', { [bnaVars.inlinePad]: d.md, }); + addRootOverride(enhanced.components, 'MuiDialogTitle', { + [dtVars.blockPad]: d.lg, + [dtVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogContent', { + [dcVars.blockPad]: d.lg, + [dcVars.inlinePad]: d.xl, + }); + addRootOverride(enhanced.components, 'MuiDialogActions', { + [daVars.pad]: d.sm, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From ed8a545c7cdb4b19a48746b398501a0e5c46dddb Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 2 Jul 2026 04:48:14 +0700 Subject: [PATCH 085/114] density List: tokenize ListItemButton block/dense/gutters padding - ListItemButton block pad (8, dense 4) + gutters inline pad (16) over --comp-* (mirrors MenuItem dense axis) - listItemButtonVars (blockPad/denseBlockPad/inlinePad); re-export js+d.ts; presets map blockPad->sm, denseBlockPad->xxs, inlinePad->lg (normal == today) - List blockPad already tokenized (Menu pass); plain ListItem insets left literal - playground ListItemButton entry (dedupe List import); fixture demo + scopes - zero-diff maxDiffPixels:0 --- docs/pages/experiments/density-fixture.tsx | 12 +++++ docs/pages/experiments/density-playground.tsx | 47 ++++++++++++++++++- docs/src/modules/components/densityDemos.tsx | 15 ++++++ .../src/ListItemButton/ListItemButton.js | 19 +++++--- .../src/ListItemButton/index.d.ts | 2 + .../mui-material/src/ListItemButton/index.js | 2 + .../src/ListItemButton/listItemButtonVars.ts | 11 +++++ .../src/styles/enhanceComfortDensity.ts | 6 +++ .../src/styles/enhanceCompactDensity.ts | 6 +++ .../src/styles/enhanceNormalDensity.ts | 6 +++ 10 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 packages/mui-material/src/ListItemButton/listItemButtonVars.ts diff --git a/docs/pages/experiments/density-fixture.tsx b/docs/pages/experiments/density-fixture.tsx index fb1277d4bff6c7..d550a5f8e7be21 100644 --- a/docs/pages/experiments/density-fixture.tsx +++ b/docs/pages/experiments/density-fixture.tsx @@ -128,6 +128,18 @@ const scopes: Record> = { dense: { ['--Avatar-size' as any]: '28px' }, loose: { ['--Avatar-size' as any]: '56px' }, }, + ListItemButton: { + dense: { + ['--ListItemButton-blockPad' as any]: '4px', + ['--ListItemButton-dense-blockPad' as any]: '2px', + ['--ListItemButton-inlinePad' as any]: '10px', + }, + loose: { + ['--ListItemButton-blockPad' as any]: '14px', + ['--ListItemButton-dense-blockPad' as any]: '8px', + ['--ListItemButton-inlinePad' as any]: '28px', + }, + }, Dialog: { dense: { ['--DialogTitle-blockPad' as any]: '10px', diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index 0d66a617ea72db..a386f72c00d7bc 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -25,7 +25,8 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import MenuItem, { private_menuItemVars } from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; import Menu from '@mui/material/Menu'; -import { private_listVars } from '@mui/material/List'; +import ListItemButton, { private_listItemButtonVars } from '@mui/material/ListItemButton'; +import List, { private_listVars } from '@mui/material/List'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import InboxIcon from '@mui/icons-material/Inbox'; @@ -1293,6 +1294,44 @@ function DialogMatrix({ ); } +// ListItemButton family: block padding (+ dense) + gutters inline padding. +const LIST_ITEM_BUTTON_FIELDS: DensityField[] = [ + { key: 'blockPad', cssVar: private_listItemButtonVars.blockPad }, + { key: 'denseBlockPad', cssVar: private_listItemButtonVars.denseBlockPad }, + { key: 'inlinePad', cssVar: private_listItemButtonVars.inlinePad }, +]; + +function ListItemButtonMatrix({ + mapping, + mappingEnabled, +}: { + mapping: Record; + mappingEnabled: boolean; +}) { + const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; + const sx = mappingEnabled + ? Object.fromEntries( + LIST_ITEM_BUTTON_FIELDS.filter((f) => active(f.key)).map((f) => [ + f.cssVar, + resolveValue(mapping[f.key]), + ]), + ) + : undefined; + return ( + + + Regular item} /> + + + Selected item} /> + + + Dense item} /> + + + ); +} + const COMPONENT_DEFS = { Button: { canvasLabel: 'Button (color="primary")', @@ -1430,6 +1469,12 @@ const COMPONENT_DEFS = { }, renderMatrix: (args) => , }, + ListItemButton: { + canvasLabel: 'ListItemButton — block padding (+ dense) + gutters', + fields: LIST_ITEM_BUTTON_FIELDS, + prefill: { blockPad: 'sm', denseBlockPad: 'xxs', inlinePad: 'lg' }, + renderMatrix: (args) => , + }, ButtonGroup: { canvasLabel: 'ButtonGroup — grouped-button min-width floor', fields: BUTTON_GROUP_FIELDS, diff --git a/docs/src/modules/components/densityDemos.tsx b/docs/src/modules/components/densityDemos.tsx index 34deced78363bb..f7753cf60c7572 100644 --- a/docs/src/modules/components/densityDemos.tsx +++ b/docs/src/modules/components/densityDemos.tsx @@ -10,6 +10,8 @@ import BottomNavigation from '@mui/material/BottomNavigation'; import BottomNavigationAction from '@mui/material/BottomNavigationAction'; import MenuList from '@mui/material/MenuList'; import MenuItem from '@mui/material/MenuItem'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Tooltip from '@mui/material/Tooltip'; @@ -224,6 +226,19 @@ const demos: Record = { ), + ListItemButton: ( + + + + + + + + + + + + ), Dialog: ( // The dialog surface (Paper) with its 3 slots — no modal/portal so it renders // inline in the scope; density lives in the title/content/actions padding. diff --git a/packages/mui-material/src/ListItemButton/ListItemButton.js b/packages/mui-material/src/ListItemButton/ListItemButton.js index 32e6699882e90e..b1a2e3ca8f0dfe 100644 --- a/packages/mui-material/src/ListItemButton/ListItemButton.js +++ b/packages/mui-material/src/ListItemButton/ListItemButton.js @@ -12,6 +12,7 @@ import useEnhancedEffect from '../utils/useEnhancedEffect'; import useForkRef from '../utils/useForkRef'; import ListContext from '../List/ListContext'; import listItemButtonClasses, { getListItemButtonUtilityClass } from './listItemButtonClasses'; +import { private_listItemButtonVars as vars } from './listItemButtonVars'; import { getTransitionStyles } from '../transitions/utils'; export const overridesResolver = (props, styles) => { @@ -65,8 +66,12 @@ const ListItemButtonRoot = styled(ButtonBase, { minWidth: 0, boxSizing: 'border-box', textAlign: 'left', - paddingTop: 8, - paddingBottom: 8, + // Density seams (mirrors MenuItem dense axis): block padding over the default; + // `dense`/`gutters` variants reroute below. + '--_blockPad': '8px', + '--comp-blockPad': `var(${vars.blockPad}, var(--_blockPad))`, + paddingTop: 'var(--comp-blockPad, var(--_blockPad))', + paddingBottom: 'var(--comp-blockPad, var(--_blockPad))', ...getTransitionStyles(theme, 'background-color', { duration: theme.transitions.duration.shortest, }), @@ -128,15 +133,17 @@ const ListItemButtonRoot = styled(ButtonBase, { { props: ({ ownerState }) => !ownerState.disableGutters, style: { - paddingLeft: 16, - paddingRight: 16, + '--_inlinePad': '16px', + '--comp-inlinePad': `var(${vars.inlinePad}, var(--_inlinePad))`, + paddingLeft: 'var(--comp-inlinePad, var(--_inlinePad))', + paddingRight: 'var(--comp-inlinePad, var(--_inlinePad))', }, }, { props: ({ ownerState }) => ownerState.dense, style: { - paddingTop: 4, - paddingBottom: 4, + '--_blockPad': '4px', + '--comp-blockPad': `var(${vars.denseBlockPad}, var(--_blockPad))`, }, }, ], diff --git a/packages/mui-material/src/ListItemButton/index.d.ts b/packages/mui-material/src/ListItemButton/index.d.ts index 57fdf7f3962ea2..daeae734d96a50 100644 --- a/packages/mui-material/src/ListItemButton/index.d.ts +++ b/packages/mui-material/src/ListItemButton/index.d.ts @@ -3,3 +3,5 @@ export * from './ListItemButton'; export { default as listItemButtonClasses } from './listItemButtonClasses'; export * from './listItemButtonClasses'; + +export { private_listItemButtonVars } from './listItemButtonVars'; diff --git a/packages/mui-material/src/ListItemButton/index.js b/packages/mui-material/src/ListItemButton/index.js index feb9791f4194cc..f4a772a7f67a53 100644 --- a/packages/mui-material/src/ListItemButton/index.js +++ b/packages/mui-material/src/ListItemButton/index.js @@ -2,3 +2,5 @@ export { default } from './ListItemButton'; export { default as listItemButtonClasses } from './listItemButtonClasses'; export * from './listItemButtonClasses'; + +export { private_listItemButtonVars } from './listItemButtonVars'; diff --git a/packages/mui-material/src/ListItemButton/listItemButtonVars.ts b/packages/mui-material/src/ListItemButton/listItemButtonVars.ts new file mode 100644 index 00000000000000..63319d3ba24b7c --- /dev/null +++ b/packages/mui-material/src/ListItemButton/listItemButtonVars.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention, import/prefer-default-export */ +/** + * ListItemButton density token identities — block padding (8, dense 4) + gutters + * inline padding (16), over the agnostic `--comp-*` seams (mirrors MenuItem's + * `dense` axis). `private_*` per the density RFC. + */ +export const private_listItemButtonVars = { + blockPad: '--ListItemButton-blockPad', + denseBlockPad: '--ListItemButton-dense-blockPad', + inlinePad: '--ListItemButton-inlinePad', +} as const; diff --git a/packages/mui-material/src/styles/enhanceComfortDensity.ts b/packages/mui-material/src/styles/enhanceComfortDensity.ts index 4af207cb9e3a47..6c29aa549d573c 100644 --- a/packages/mui-material/src/styles/enhanceComfortDensity.ts +++ b/packages/mui-material/src/styles/enhanceComfortDensity.ts @@ -36,6 +36,7 @@ import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigati import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; +import { private_listItemButtonVars as libVars } from '../ListItemButton/listItemButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -236,6 +237,11 @@ export default function enhanceComfortDensity(theme: addRootOverride(enhanced.components, 'MuiDialogActions', { [daVars.pad]: d.sm, }); + addRootOverride(enhanced.components, 'MuiListItemButton', { + [libVars.blockPad]: d.sm, + [libVars.denseBlockPad]: d.xxs, + [libVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceCompactDensity.ts b/packages/mui-material/src/styles/enhanceCompactDensity.ts index b4a898fc707a3a..e9b3f6347d3d0e 100644 --- a/packages/mui-material/src/styles/enhanceCompactDensity.ts +++ b/packages/mui-material/src/styles/enhanceCompactDensity.ts @@ -36,6 +36,7 @@ import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigati import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; +import { private_listItemButtonVars as libVars } from '../ListItemButton/listItemButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; const scale: DensityScale = { @@ -236,6 +237,11 @@ export default function enhanceCompactDensity(theme: addRootOverride(enhanced.components, 'MuiDialogActions', { [daVars.pad]: d.sm, }); + addRootOverride(enhanced.components, 'MuiListItemButton', { + [libVars.blockPad]: d.sm, + [libVars.denseBlockPad]: d.xxs, + [libVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, diff --git a/packages/mui-material/src/styles/enhanceNormalDensity.ts b/packages/mui-material/src/styles/enhanceNormalDensity.ts index eac08e49272300..90e063589196dc 100644 --- a/packages/mui-material/src/styles/enhanceNormalDensity.ts +++ b/packages/mui-material/src/styles/enhanceNormalDensity.ts @@ -36,6 +36,7 @@ import { private_bottomNavigationActionVars as bnaVars } from '../BottomNavigati import { private_dialogTitleVars as dtVars } from '../DialogTitle/dialogTitleVars'; import { private_dialogContentVars as dcVars } from '../DialogContent/dialogContentVars'; import { private_dialogActionsVars as daVars } from '../DialogActions/dialogActionsVars'; +import { private_listItemButtonVars as libVars } from '../ListItemButton/listItemButtonVars'; import inputLabelClasses from '../InputLabel/inputLabelClasses'; // Explicit px (self-contained, not spacing-derived). Normal keeps today's Button @@ -238,6 +239,11 @@ export default function enhanceNormalDensity(theme: addRootOverride(enhanced.components, 'MuiDialogActions', { [daVars.pad]: d.sm, }); + addRootOverride(enhanced.components, 'MuiListItemButton', { + [libVars.blockPad]: d.sm, + [libVars.denseBlockPad]: d.xxs, + [libVars.inlinePad]: d.lg, + }); addRootOverride(enhanced.components, 'MuiCardContent', { // No size axis: base pad + larger last-child bottom pad. [ccVars.pad]: d.lg, From 8adbd79ff99d2a48fdec30c0bdebdd32122a5b12 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sun, 5 Jul 2026 11:12:57 +0700 Subject: [PATCH 086/114] density Button: strip seam to master; presets emit padding overrides (epic 3 template) --- docs/pages/experiments/density-playground.tsx | 1148 +++++++---------- packages/mui-material/src/Button/Button.js | 48 +- .../mui-material/src/Button/buttonVars.ts | 14 - packages/mui-material/src/Button/index.d.ts | 2 - packages/mui-material/src/Button/index.js | 2 - .../mui-material/src/styles/densityScale.ts | 4 +- .../src/styles/enhanceComfortDensity.ts | 10 +- .../src/styles/enhanceCompactDensity.ts | 10 +- .../src/styles/enhanceNormalDensity.ts | 10 +- 9 files changed, 474 insertions(+), 774 deletions(-) delete mode 100644 packages/mui-material/src/Button/buttonVars.ts diff --git a/docs/pages/experiments/density-playground.tsx b/docs/pages/experiments/density-playground.tsx index a386f72c00d7bc..8d1de613a2b780 100644 --- a/docs/pages/experiments/density-playground.tsx +++ b/docs/pages/experiments/density-playground.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; -import Button, { private_buttonVars } from '@mui/material/Button'; +import Button from '@mui/material/Button'; import ButtonGroup, { private_buttonGroupVars } from '@mui/material/ButtonGroup'; import Fab, { private_fabVars } from '@mui/material/Fab'; import Pagination from '@mui/material/Pagination'; @@ -18,6 +18,7 @@ import TableBody from '@mui/material/TableBody'; import TableRow from '@mui/material/TableRow'; import TableCell, { private_tableCellVars } from '@mui/material/TableCell'; import CssBaseline from '@mui/material/CssBaseline'; +import GlobalStyles from '@mui/material/GlobalStyles'; import RadioGroup from '@mui/material/RadioGroup'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; @@ -48,6 +49,9 @@ import DialogContent, { private_dialogContentVars } from '@mui/material/DialogCo import DialogActions, { private_dialogActionsVars } from '@mui/material/DialogActions'; import Card from '@mui/material/Card'; import CardContent, { private_cardContentVars } from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import CardHeader from '@mui/material/CardHeader'; +import Rating from '@mui/material/Rating'; import Select, { private_selectVars } from '@mui/material/Select'; import InputLabel from '@mui/material/InputLabel'; import Alert, { private_alertVars } from '@mui/material/Alert'; @@ -85,8 +89,6 @@ const SIZES = ['small', 'medium', 'large'] as const; const VARIANTS = ['text', 'outlined', 'contained'] as const; type Preset = (typeof PRESETS)[number]; -type Size = (typeof SIZES)[number]; -type MappingKey = `${Size}Pad`; // Visual-debug overlays, toggled by `data-debug-*` on the canvas. Pure CSS, // layout-safe (absolute ::before + pointer-events:none), never touches the @@ -130,8 +132,6 @@ const PRESET_LABEL: Record = { comfort: 'comfort', }; -const buttonVar = (size: Size) => private_buttonVars[`${size}Pad` as MappingKey]; - const isDensityKey = (t: string) => (SCALE_KEYS as readonly string[]).includes(t); const tokenize = (input: string) => input.trim().split(/\s+/).filter(Boolean); @@ -163,6 +163,22 @@ const previewText = (input: string, scalePx: Record | null) => .map((t) => (isDensityKey(t) ? (scalePx?.[t] ?? t) : t)) .join(' '); +// A readable property label from the token identity, e.g. +// '--Button-small-pad' → 'Button small padding', '--MenuItem-dense-blockPad' → +// 'Menu item dense block padding'. Split on `-` + camelCase, lowercase, cap the +// component word, expand `pad` → `padding`. +const fieldLabel = (cssVar: string) => { + const words = cssVar + .replace(/^--/, '') + .split('-') + .flatMap((seg) => seg.replace(/([a-z])([A-Z])/g, '$1 $2').split(' ')) + .map((w) => w.toLowerCase()); + return words + .map((w, i) => (i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w)) + .join(' ') + .replace(/\bpad\b/g, 'padding'); +}; + // Each preset maps to its `enhance*Density` fn; `unset` applies none. const PRESET_FN = { compact: enhanceCompactDensity, @@ -177,58 +193,50 @@ const PRESET_FN = { // --------------------------------------------------------------------------- interface DensityField { key: string; // mapping-state key, e.g. 'smallPad' - cssVar: string; // e.g. '--Button-small-pad' + cssVar: string; // token identity — raw-px placeholder lookup + var-mode override target + selector: string; // canvas-relative selector the preset emits on (no `#density-canvas` prefix) + // The real CSS property (or properties) to override in-scope — the emitted-override + // model writes the property directly, so it survives the source's seam removal. + // Omit → var-mode: write `cssVar` instead, letting the source's own seam route it. + // Used for calc-coupling children (Chip height) and multi-route/media fields with + // no discriminating class (Tab icon gaps, Toolbar gutters, Step gutter, Tooltip offset/arrow). + prop?: string | string[]; } interface DensityComponentDef { canvasLabel: string; fields: DensityField[]; prefill: Record; - renderMatrix: (args: { mapping: Record; mappingEnabled: boolean }) => React.ReactNode; + note?: string; // shown under the mapping group (stub / out-of-scope axis) + // Overrides are applied globally via GlobalStyles (see `overrideCss`), so a + // matrix is a plain demo render — no per-element token plumbing. + renderMatrix: () => React.ReactNode; } -function ButtonMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { +function ButtonMatrix() { return ( - {SIZES.map((size) => { - const key = `${size}Pad`; - const value = mapping[key] ?? ''; - // TO5/TO6: element-level token wins over the preset's styleOverride. - // At `unset`/empty/invalid emit NO token → falls back to the literal - // `--_pad` default (unset) or the preset's own mapping. - const sx = - mappingEnabled && parseMapping(value).state === 'ok' - ? { [buttonVar(size)]: resolveValue(value) } - : undefined; - return ( - - - - {size} - - - - {VARIANTS.map((variant) => ( - - ))} - - - ); - })} + {SIZES.map((size) => ( + + + + {size} + + + + {VARIANTS.map((variant) => ( + + ))} + + + ))} ); } @@ -238,25 +246,25 @@ function ButtonMatrix({ // mapping-state key. Sizing tokens (`minHeight`) accept raw px like any other — // a density key is just sugar; heights ship as raw px per preset. const MENU_FIELDS: DensityField[] = [ - { key: 'listBlockPad', cssVar: private_listVars.blockPad }, - { key: 'blockPad', cssVar: private_menuItemVars.blockPad }, - { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad }, - { key: 'minHeight', cssVar: private_menuItemVars.minHeight }, - { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad }, - { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad }, - { key: 'denseMinHeight', cssVar: private_menuItemVars.denseMinHeight }, + { key: 'listBlockPad', cssVar: private_listVars.blockPad, prop: 'paddingBlock', selector: '.MuiList-padding' }, + { key: 'blockPad', cssVar: private_menuItemVars.blockPad, prop: 'paddingBlock', selector: '.MuiMenuItem-root:not(.MuiMenuItem-dense)' }, + { key: 'inlinePad', cssVar: private_menuItemVars.inlinePad, prop: 'paddingInline', selector: '.MuiMenuItem-gutters:not(.MuiMenuItem-dense)' }, + { key: 'minHeight', cssVar: private_menuItemVars.minHeight, prop: 'minHeight', selector: '.MuiMenuItem-root:not(.MuiMenuItem-dense)' }, + { key: 'denseBlockPad', cssVar: private_menuItemVars.denseBlockPad, prop: 'paddingBlock', selector: '.MuiMenuItem-dense' }, + { key: 'denseInlinePad', cssVar: private_menuItemVars.denseInlinePad, prop: 'paddingInline', selector: '.MuiMenuItem-gutters.MuiMenuItem-dense' }, + { key: 'denseMinHeight', cssVar: private_menuItemVars.denseMinHeight, prop: 'minHeight', selector: '.MuiMenuItem-dense' }, ]; -function MenuDemoItems({ itemSx }: { itemSx: Record | undefined }) { +function MenuDemoItems() { return ( - + Default item - + Selected item - + @@ -264,13 +272,13 @@ function MenuDemoItems({ itemSx }: { itemSx: Record | undefined With icon - + With divider - + Dense item - + @@ -282,47 +290,21 @@ function MenuDemoItems({ itemSx }: { itemSx: Record | undefined ); } -function MenuMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { +function MenuMatrix() { const [anchorEl, setAnchorEl] = React.useState(null); - // Element-level tokens win over the preset's styleOverride. `--List-blockPad` - // goes on the list root; `--MenuItem-*` on each item (regular reads plain, - // dense reads `dense-*` — unused set is inert). Empty/invalid → emit none, so - // the preset's own value shows through (e.g. blank min-height keeps the preset px). - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const listSx = - mappingEnabled && active('listBlockPad') - ? { [private_listVars.blockPad]: resolveValue(mapping.listBlockPad) } - : undefined; - const itemSx = mappingEnabled - ? Object.fromEntries( - MENU_FIELDS.filter((f) => f.key !== 'listBlockPad' && active(f.key)).map((f) => [ - f.cssVar, - resolveValue(mapping[f.key]), - ]), - ) - : undefined; + // The popover portals outside `#density-canvas`, so manual overrides (scoped + // there) reach only the static list; both still follow the preset via context. return ( - - + +
- setAnchorEl(null)} - slotProps={{ list: { sx: listSx } }} - > - + setAnchorEl(null)}> +
@@ -333,35 +315,27 @@ function MenuMatrix({ // Padding + anchor offset are spacing (prefill density keys); arrow size ships // as raw px per preset (read live off the theme), like MenuItem min-height. const TOOLTIP_FIELDS: DensityField[] = [ - { key: 'blockPad', cssVar: private_tooltipVars.blockPad }, - { key: 'inlinePad', cssVar: private_tooltipVars.inlinePad }, - { key: 'offset', cssVar: private_tooltipVars.offset }, - { key: 'arrowSize', cssVar: private_tooltipVars.arrowSize }, + { key: 'blockPad', cssVar: private_tooltipVars.blockPad, prop: 'paddingBlock', selector: '.MuiTooltip-tooltip' }, + { key: 'inlinePad', cssVar: private_tooltipVars.inlinePad, prop: 'paddingInline', selector: '.MuiTooltip-tooltip' }, + // var-mode: one offset var drives a per-placement margin (4 placements, no class). + { key: 'offset', cssVar: private_tooltipVars.offset, selector: '.MuiTooltip-tooltip' }, + // var-mode: the arrow's width + height (calc) both derive from this var. + { key: 'arrowSize', cssVar: private_tooltipVars.arrowSize, selector: '.MuiTooltip-tooltip' }, ]; -function TooltipMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - // Element-level tokens win over the preset. All four land on the bubble - // (`tooltip` slot); the arrow inherits `--comp-arrowSize` from it. - const tooltipSx = mappingEnabled - ? Object.fromEntries( - TOOLTIP_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), - ) - : undefined; - // Force open + inline (no portal) so the bubble sits inside the debug scope - // and picks up the padding-ring / text-box overlays. +function TooltipMatrix() { + // Force open + inline (no portal) so the bubble sits inside the debug scope, + // picks up the padding-ring / text-box overlays, and receives the canvas-scoped + // token overrides. const slotProps = { popper: { disablePortal: true }, - tooltip: { sx: tooltipSx }, } as const; return ( - + Default tooltip} open @@ -387,52 +361,34 @@ function TooltipMatrix({ // (per size). All spacing → prefill density keys. The label resting-Y is a // derived bridge (not a direct field). const OUTLINED_INPUT_FIELDS: DensityField[] = [ - { key: 'mediumBlockPad', cssVar: private_outlinedInputVars.mediumBlockPad }, - { key: 'smallBlockPad', cssVar: private_outlinedInputVars.smallBlockPad }, - { key: 'mediumInlinePad', cssVar: private_outlinedInputVars.mediumInlinePad }, - { key: 'smallInlinePad', cssVar: private_outlinedInputVars.smallInlinePad }, - { key: 'mediumGap', cssVar: private_inputAdornmentVars.mediumGap }, - { key: 'smallGap', cssVar: private_inputAdornmentVars.smallGap }, + { key: 'mediumBlockPad', cssVar: private_outlinedInputVars.mediumBlockPad, prop: 'paddingBlock', selector: '.MuiOutlinedInput-root:not(.MuiInputBase-sizeSmall) .MuiOutlinedInput-input' }, + { key: 'smallBlockPad', cssVar: private_outlinedInputVars.smallBlockPad, prop: 'paddingBlock', selector: '.MuiInputBase-sizeSmall .MuiOutlinedInput-input' }, + { key: 'mediumInlinePad', cssVar: private_outlinedInputVars.mediumInlinePad, prop: 'paddingInline', selector: '.MuiOutlinedInput-root:not(.MuiInputBase-sizeSmall) .MuiOutlinedInput-input' }, + { key: 'smallInlinePad', cssVar: private_outlinedInputVars.smallInlinePad, prop: 'paddingInline', selector: '.MuiInputBase-sizeSmall .MuiOutlinedInput-input' }, + // var-mode: one gap var → start marginRight / end marginLeft (no per-side class). + { key: 'mediumGap', cssVar: private_inputAdornmentVars.mediumGap, selector: '.MuiInputAdornment-root:not(.MuiInputAdornment-sizeSmall)' }, + { key: 'smallGap', cssVar: private_inputAdornmentVars.smallGap, selector: '.MuiInputAdornment-sizeSmall' }, ]; -function OutlinedInputMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - // Tokens go on the TextField (ancestor of label + input + adornment) so the - // `:has(~ &)` label bridge sees them — element-level on the input root can't - // reach the sibling label. - const sx = mappingEnabled - ? Object.fromEntries( - OUTLINED_INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [ - f.cssVar, - resolveValue(mapping[f.key]), - ]), - ) - : undefined; +function OutlinedInputMatrix() { return ( - Medium} variant="outlined" sx={sx} /> + Medium} variant="outlined" /> Small} variant="outlined" size="small" - sx={sx} /> Start adornment} variant="outlined" - sx={sx} - slotProps={{ input: { startAdornment: $ } }} + slotProps={{ + input: { startAdornment: $ }, + }} /> End adornment} variant="outlined" - sx={sx} slotProps={{ input: { endAdornment: kg } }} /> @@ -443,44 +399,27 @@ function OutlinedInputMatrix({ // prefill density keys. The label rest/shrink Y follow the active preset (tuned // raw px, not editable here). const FILLED_INPUT_FIELDS: DensityField[] = [ - { key: 'mediumTopPad', cssVar: private_filledInputVars.mediumTopPad }, - { key: 'smallTopPad', cssVar: private_filledInputVars.smallTopPad }, - { key: 'mediumBottomPad', cssVar: private_filledInputVars.mediumBottomPad }, - { key: 'smallBottomPad', cssVar: private_filledInputVars.smallBottomPad }, - { key: 'mediumInlinePad', cssVar: private_filledInputVars.mediumInlinePad }, - { key: 'smallInlinePad', cssVar: private_filledInputVars.smallInlinePad }, + { key: 'mediumTopPad', cssVar: private_filledInputVars.mediumTopPad, prop: 'paddingTop', selector: '.MuiFilledInput-root:not(.MuiInputBase-sizeSmall) .MuiFilledInput-input' }, + { key: 'smallTopPad', cssVar: private_filledInputVars.smallTopPad, prop: 'paddingTop', selector: '.MuiInputBase-sizeSmall .MuiFilledInput-input' }, + { key: 'mediumBottomPad', cssVar: private_filledInputVars.mediumBottomPad, prop: 'paddingBottom', selector: '.MuiFilledInput-root:not(.MuiInputBase-sizeSmall) .MuiFilledInput-input' }, + { key: 'smallBottomPad', cssVar: private_filledInputVars.smallBottomPad, prop: 'paddingBottom', selector: '.MuiInputBase-sizeSmall .MuiFilledInput-input' }, + { key: 'mediumInlinePad', cssVar: private_filledInputVars.mediumInlinePad, prop: 'paddingInline', selector: '.MuiFilledInput-root:not(.MuiInputBase-sizeSmall) .MuiFilledInput-input' }, + { key: 'smallInlinePad', cssVar: private_filledInputVars.smallInlinePad, prop: 'paddingInline', selector: '.MuiInputBase-sizeSmall .MuiFilledInput-input' }, ]; -function FilledInputMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const sx = mappingEnabled - ? Object.fromEntries( - FILLED_INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [ - f.cssVar, - resolveValue(mapping[f.key]), - ]), - ) - : undefined; +function FilledInputMatrix() { return ( - Medium} variant="filled" sx={sx} /> + Medium} variant="filled" /> Small} variant="filled" size="small" - sx={sx} /> Filled value} variant="filled" defaultValue="Value" - sx={sx} /> ); @@ -489,32 +428,19 @@ function FilledInputMatrix({ // Input (standard) family: input top/bottom padding (top per size, bottom // shared). Inline is 0; the label floats above (no bridge). const INPUT_FIELDS: DensityField[] = [ - { key: 'mediumTopPad', cssVar: private_inputVars.mediumTopPad }, - { key: 'smallTopPad', cssVar: private_inputVars.smallTopPad }, - { key: 'bottomPad', cssVar: private_inputVars.bottomPad }, + { key: 'mediumTopPad', cssVar: private_inputVars.mediumTopPad, prop: 'paddingTop', selector: '.MuiInput-root:not(.MuiInputBase-sizeSmall) .MuiInput-input' }, + { key: 'smallTopPad', cssVar: private_inputVars.smallTopPad, prop: 'paddingTop', selector: '.MuiInputBase-sizeSmall .MuiInput-input' }, + { key: 'bottomPad', cssVar: private_inputVars.bottomPad, prop: 'paddingBottom', selector: '.MuiInput-input' }, ]; -function InputMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const sx = mappingEnabled - ? Object.fromEntries( - INPUT_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), - ) - : undefined; +function InputMatrix() { return ( - Medium} variant="standard" sx={sx} /> + Medium} variant="standard" /> Small} variant="standard" size="small" - sx={sx} /> ); @@ -524,43 +450,31 @@ function InputMatrix({ // shared inline pad + icon gaps (stack/inline), plus the paired Tabs-root // min-height. Spacing → density keys; min-heights → raw px (read off the theme). const TAB_FIELDS: DensityField[] = [ - { key: 'minHeight', cssVar: private_tabVars.minHeight }, - { key: 'tabsMinHeight', cssVar: private_tabsVars.minHeight }, - { key: 'iconLabelMinHeight', cssVar: private_tabVars.iconLabelMinHeight }, - { key: 'blockPad', cssVar: private_tabVars.blockPad }, - { key: 'iconLabelBlockPad', cssVar: private_tabVars.iconLabelBlockPad }, - { key: 'inlinePad', cssVar: private_tabVars.inlinePad }, - { key: 'iconStackGap', cssVar: private_tabVars.iconStackGap }, - { key: 'iconInlineGap', cssVar: private_tabVars.iconInlineGap }, + { key: 'minHeight', cssVar: private_tabVars.minHeight, prop: 'minHeight', selector: '.MuiTab-root:not(.MuiTab-labelIcon)' }, + { key: 'tabsMinHeight', cssVar: private_tabsVars.minHeight, prop: 'minHeight', selector: '.MuiTabs-root' }, + { key: 'iconLabelMinHeight', cssVar: private_tabVars.iconLabelMinHeight, prop: 'minHeight', selector: '.MuiTab-root.MuiTab-labelIcon' }, + { key: 'blockPad', cssVar: private_tabVars.blockPad, prop: 'paddingBlock', selector: '.MuiTab-root:not(.MuiTab-labelIcon)' }, + { key: 'iconLabelBlockPad', cssVar: private_tabVars.iconLabelBlockPad, prop: 'paddingBlock', selector: '.MuiTab-root.MuiTab-labelIcon' }, + { key: 'inlinePad', cssVar: private_tabVars.inlinePad, prop: 'paddingInline', selector: '.MuiTab-root' }, + // var-mode: one gap var → icon margin per iconPosition (top/bottom, start/end; no class). + { key: 'iconStackGap', cssVar: private_tabVars.iconStackGap, selector: '.MuiTab-root' }, + { key: 'iconInlineGap', cssVar: private_tabVars.iconInlineGap, selector: '.MuiTab-root' }, ]; -function TabsMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - // Tokens on each Tabs instance (ancestor of its Tab children, which inherit). - const sx = mappingEnabled - ? Object.fromEntries( - TAB_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), - ) - : undefined; +function TabsMatrix() { const lbl = (t: string) => {t}; return ( - + - + } label={lbl('Top')} iconPosition="top" /> } label={lbl('Top')} iconPosition="top" /> - + } label={lbl('Start')} iconPosition="start" /> } label={lbl('Start')} iconPosition="start" /> @@ -571,89 +485,72 @@ function TabsMatrix({ // Checkbox family: the touch-target padding around the icon, per size (via // SwitchBase). All spacing → density keys. const CHECKBOX_FIELDS: DensityField[] = [ - { key: 'mediumPad', cssVar: private_checkboxVars.mediumPad }, - { key: 'smallPad', cssVar: private_checkboxVars.smallPad }, + { key: 'mediumPad', cssVar: private_checkboxVars.mediumPad, prop: 'padding', selector: '.MuiCheckbox-root.MuiCheckbox-sizeMedium' }, + { key: 'smallPad', cssVar: private_checkboxVars.smallPad, prop: 'padding', selector: '.MuiCheckbox-root.MuiCheckbox-sizeSmall' }, ]; -function CheckboxMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const sx = mappingEnabled - ? Object.fromEntries( - CHECKBOX_FIELDS.filter((f) => active(f.key)).map((f) => [ - f.cssVar, - resolveValue(mapping[f.key]), - ]), - ) - : undefined; +function CheckboxMatrix() { return ( - - + + ); } -// CardContent family: base padding + last-child bottom padding (no size axis). -const CARD_CONTENT_FIELDS: DensityField[] = [ - { key: 'pad', cssVar: private_cardContentVars.pad }, - { key: 'padBottom', cssVar: private_cardContentVars.padBottom }, +// Card family: CardContent padding (+ last-child) — tokenized; CardActions/CardHeader +// padding + gaps are stubs (not yet tokenized in source; no size axis). +const CARD_FIELDS: DensityField[] = [ + { key: 'pad', cssVar: private_cardContentVars.pad, prop: 'padding', selector: '.MuiCardContent-root' }, + { key: 'padBottom', cssVar: private_cardContentVars.padBottom, prop: 'paddingBottom', selector: '.MuiCardContent-root:last-child' }, + // Stub — CardActions/CardHeader not yet tokenized in source (no preset reflow yet); + // the direct-property override still applies over today's literals. + { key: 'actionsPad', cssVar: '--CardActions-pad', prop: 'padding', selector: '.MuiCardActions-root' }, + { key: 'actionsGap', cssVar: '--CardActions-childGap', prop: 'marginLeft', selector: '.MuiCardActions-spacing > :not(:first-of-type)' }, + { key: 'headerPad', cssVar: '--CardHeader-pad', prop: 'padding', selector: '.MuiCardHeader-root' }, + { key: 'headerAvatarGap', cssVar: '--CardHeader-avatarGap', prop: 'marginRight', selector: '.MuiCardHeader-avatar' }, ]; -function CardContentMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const sx = mappingEnabled - ? Object.fromEntries( - CARD_CONTENT_FIELDS.filter((f) => active(f.key)).map((f) => [ - f.cssVar, - resolveValue(mapping[f.key]), - ]), - ) - : undefined; +function CardMatrix() { return ( - - - - Card title - + + R} + title={Card header} + subheader="With avatar" + /> + Body content with last-child bottom padding. + + + + ); } +function RatingMatrix() { + return ( + + + + + + ); +} + // Select family: the content-box min-height floor (raw px). The visible density // mostly comes from the underlying OutlinedInput padding (tokenized separately). -const SELECT_FIELDS: DensityField[] = [{ key: 'minHeight', cssVar: private_selectVars.minHeight }]; - -function SelectMatrix({ - mapping, - mappingEnabled, -}: { - mapping: Record; - mappingEnabled: boolean; -}) { - const active = (key: string) => parseMapping(mapping[key] ?? '').state === 'ok'; - const sx = mappingEnabled - ? Object.fromEntries( - SELECT_FIELDS.filter((f) => active(f.key)).map((f) => [f.cssVar, resolveValue(mapping[f.key])]), - ) - : undefined; +const SELECT_FIELDS: DensityField[] = [ + { key: 'minHeight', cssVar: private_selectVars.minHeight, prop: 'minHeight', selector: '.MuiSelect-select' }, +]; + +function SelectMatrix() { return ( - + Age