[WIP][prototype] density system#48749
Draft
siriwatknp wants to merge 87 commits into
Draft
Conversation
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.
…t of density experiment
… + --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-<size>-pad) to density scale
…seam - OutlinedInput: block-only density (--OutlinedInput-<size>-padBlock); root routes, input inherits; drop redundant size/multiline variants - InputLabel: generic --InputLabel-y seam; OutlinedInput bridges sibling label via :has(~ &) - Docs: ADR-0001 OutlinedInput + label :has bridge, CONTEXT, density-adapter-rollout guide, experiment demo
- density-fixture.tsx: per-component matrix scoped by ?c=&level (default pixel-identical) - scripts/density-screenshots: config + spec + README (maxDiffPixels 0) - density:shot / density:shot:update scripts; gitignore harness outputs
- 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
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-<size>-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.
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-<key> and only the dense override as --Component-dense-<key>. Toolbar keeps theme.mixins.toolbar for its regular height (only dense + gutters tokenized).
Boolean compactness toggles (dense) use a state token: default state is the plain seam --Component-<key> (base-token-shaped, no base routing), only the on state is qualified --Component-dense-<key>. 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.
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-<size>-pad into the seam; default 9px both sizes (pixel-identical). Switch routes its thumb (SwitchBase) padding via --Switch-<size>-pad (9/4); box geometry stays literal (size-coupled). enhanceDensity + fixture wired.
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 --_<key>-shadowing caveat. Added to CONTEXT relationships, ADR 0001 specifics, rollout Recipe C + Done list.
Tokenize Switch's four real dims per size (--Switch-<size>-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.
…lues 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.
The root padding (12/7, track inset) is its own axis -> tokenize as --Switch-<size>-pad over --_pad, consumed padding: var(--Switch-pad, var(--_pad)). Distinct from the derived thumb SwitchBase pad. Fixture scope + docs updated.
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).
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.
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.
…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
- 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
calc(var(--Chip-height) - inset) per size so they track density; insets reproduce today's medium/small sizes (pixel-identical default)
Signed-off-by: Siriwat K <siriwatkunaporn@gmail.com>
Prefix density tokens, tracking the css-var feature: --mui-Button-pad with cssVariables, bare --Button-pad without. One cached getButtonVars(theme) resolver shared by component + consumer so emitted/targeted names can't drift; as-const key map is the typed handle. Button + OutlinedInput + InputLabel converted; enhanceDensity blocks guarded to match. No theme.vars, no getCssVar, no forced prefix; custom-size inline routing dropped. Default render unchanged (Argos zero-diff). Demo: /experiments/density-var-prefix.
Rename the agnostic seam off the component name and prefix: --Button-pad ->
literal --comp-pad, --OutlinedInput-pad{Block,Inline} -> --comp-pad{Block,Inline},
--InputLabel-y -> --comp-labelY. The seam is the bare root's consumption point, not
a design knob, so it stays literal/unprefixed and out of buttonVars. buttonVars now
holds only the Material UI layer's public sized tokens (which track the prefix).
Document the per-layer naming in ADR-0003.
…e granularity Custom (theme-added) sizes are excluded from density (no inline routing) — the seam is only set for built-in sizes, so the generic-seam inheritance case is a documented non-goal, and we avoid prefixing turning the free inline custom-size string into a theme-dependent read. Record that as a concrete cost of prefixing.
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.
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.
…/comfort) Display-only; 'unset' key unchanged.
…) + 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)
…nputAdornment gap - 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<string,unknown> (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
…l 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
…andard 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)
…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
…e 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
- 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)
- 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
- 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
…nline 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
…adding - 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
…am (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
- 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
- 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
- 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
- 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 <xs>' (normal == today) - playground Badge entry; fixture demo + scopes - zero-diff maxDiffPixels:0
- 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
…ared 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #48746 (density RFC). Prototype / experiment — draft, not for merge. Scoped to Button only to keep the proposal reviewable; the mechanism generalizes to other families.
What this demonstrates
A holistic, opt-in density layer for
@mui/materialbuilt entirely on CSS theme variables — no new density prop, zero cost when unused.enhanceDensity(theme, options?)— emits a named--mui-density-*scale at:rootand maps Button's sized tokens to steps via injectedstyleOverrides. MirrorsenhanceHighContrast;createThemestays untouched.--Button-small-pad) → agnostic seam (--comp-pad) → internal default (--_pad). Unconfigured render falls back to the literal default, so it's pixel-identical to today (Argos zero-diff).private_buttonVarsmap imported by both the styled component andenhanceDensity, so emitted and targeted names can't drift./experiments/density-experiment— flip a preset (unset/compact/normal/comfort), and remap Button's internal tokens to density steps live. Element-level mapping wins over the preset — the editable view of the otherwise-internal layer. Component selector carries an All option (registry-driven) so more families can be added incrementally.scripts/density-screenshots/(pnpm density:shot) asserts the default render is pixel-identical to amasterbaseline atmaxDiffPixels: 0, plus dense/loose reflow captures.Verification
master(maxDiffPixels: 0).unset= literal defaults;compact/normal/comfortreflow every cell; element-levelsxwins over the presetstyleOverrides;unsetemits no--mui-density-*.@mui/material+docstypecheck, eslint, andButton.test.js(140 pass) all green.Scope
🤖 Generated with Claude Code