Scale spacing and sizing decisions from shared concepts - #10
Merged
Conversation
A rendered audit of the Storybook fixtures found the same relationships spelled differently in each composite, and several places where the value a component picked was wrong once it was seen at real size. Introduce `mediaScale`: one table owning the media edge, the media-to-text gap, and the name/meta type roles for every composite that pairs leading media with a text column. Identity, MediaObject, Comment, and Notification had been choosing these independently and already disagreed, using two different gaps for the identical 40px case. Avatars move off the space scale onto fixed px from that table. Density remaps whitespace, so anything on the space scale shrinks with it — right for gaps, wrong for a face. Fallback initials now scale with the circle instead of sitting at a pinned 14px. Add `measure` as a typography token (a character count, delivered as `--silk-measure-*` in `ch`) with a `measure` axis on Text. Line length is a property of the type, not of the layout column, so Container's widths were the wrong home for it. Add `orientation="horizontal"` to Field. The root becomes a grid so the control leads its label and the description aligns under the label rather than under the control — the shape a boolean control needs, which every call site previously had to hand-assemble. Also: - Composites inherit the Card padding default instead of hardcoding an inset, so siblings in a column stop ragging against each other. - CommentThread nests replies inside the parent's content column, so every level renders its own rail and indents without depth arithmetic. Removes an inline `style`, which the escape-hatch ranking puts last. - StatGroup's value role and inter-stat gap scale with `size`. - EmptyState binds its title to its description and separates the action. - Stack/Inline/Grid reset UA padding as well as margin, so `asChild` onto a list stops leaking. - Surface is a block box; the Switch thumb is circular at any track size. - Fixture rhythm: nav grouping, card title gaps, separator sectioning, and a Progress bar that read as a second slider track. Adds `scripts/spacing-audit.mjs`, the Playwright harness used to verify all of the above against actual rendering. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 029c56a The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Drop unused context/API surface, share size axes with mediaScale, and make the audit script reuse a page with proper cleanup. Co-authored-by: Cursor <cursoragent@cursor.com>
The horizontal grid pinned only direct data-field-* children to column 2, so a Stack or Inline around the description or error auto-placed into the control column even though Field already treats those wrappers as transparent when wiring aria. Match wrappers holding a slot via :has(), and pin the label's row so the control stays beside it regardless of child order. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3654e13. Configure here.
Comment renders anything in its replies slot through CommentReplies, a railed stack with reply padding. CommentThread bundled the continue button into that slot, so a comment at maxDepth with nothing nested inline drew a rail beside a lone button — the button used to sit outside the railed stack before replies moved into the content column. Give Comment a footer slot for a trailing affordance that belongs to the comment rather than to its replies, and route the button there. The content column is already a Stack gap="1" align="start", so the slot needs no wrapper or padding of its own, and the button's align-self class was inert. Also collapse the reply-count suffix, which was computed twice behind mirrored typeof checks on continueLabel. Co-authored-by: Cursor <cursoragent@cursor.com>
Field.Root's data-orientation was matching Slider's ancestor selectors and forcing vertical track height:100%, which resolved to 0 on a horizontal Field. Co-authored-by: Cursor <cursoragent@cursor.com>
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.

A rendered audit of the Storybook fixtures — screenshots plus computed geometry, not source reading — turned up two classes of problem: the same relationship spelled differently in each composite, and values that were defensible in isolation but wrong once seen at real size.
The shared concepts
mediaScale(packages/silk-core/src/recipes/mediaScale.ts) owns the media edge, the media-to-text gap, and the name/meta type roles for anything pairing leading media with a text column.Identity,MediaObject,Comment, andNotificationhad been choosing all three independently, and had already drifted into using two different gaps for the identical 40px case.Two things follow from it. Avatars move off the space scale onto fixed px, because
densityremaps whitespace — correct for gaps, wrong for a face. Compacting a layout was shrinking the person in it. And fallback initials scale with the circle rather than sitting at a pinned 14px, which had them nearly filling the small avatar and looking lost in the large one.measureis now a typography token: a character count insemantic.measure, delivered as--silk-measure-*inch, with ameasure="none" | "prose"axis onText. Line length is a property of the type, not of the layout column, soContainer's widths were the wrong home and a per-compositemax-widthhook was heading for one narrow decision per composite.Fieldgainedorientation="horizontal". The root becomes a two-column grid: the control auto-places into column one, and the label, description, and error pin to column two. That last part is the reason it earns an axis rather than a doc note — the description lands under the label that explains it, not under the checkbox. ReusesRadioGroup's existing axis name, so it costs no new vocabulary.Fixes
Cardpadding default instead of hardcoding an inset.ProfileCardandSettingsPanelwere at 20px,PostCardat 16px, andNotificationnested a 16pxBoxinside aSurface; stacked in a column their content edges ragged. All five cards in the feed now share one content edge.CommentThreadnests replies inside the parent comment's content column, so indentation equals the media column automatically and every level renders its own rail. Previously it computed the indent with an inlinestyle— the last rung of the ranked escape hatches — and drew a rail only at depth 0, making anything past one level invisible.StatGroup's value role and inter-stat gap scale withsize(20px value and a 24px gap at md, up from 16px and a hardcoded 16px).EmptyStatebinds its title to its description and separates the action, with a measure cap on the copy.Stack/Inline/Gridreset UA padding as well as margin, soasChildonto a<ul>or<dl>stops leaking. This is what let the thread drop its inline list reset.Surfaceis a block box, soasChildonto an<a>no longer breaks the border; theSwitchthumb is circular at any track size.Progressbar that read as a second track belonging to the slider above it.Test plan
yarn testgreen across all three workspaces;yarn typecheckcleanProfileCardpadding and the Avatar sizing, confirmed the corresponding tests fail, then restored65chresolves to 595px and caps a 900px column,measure="none"stays uncappedyarn docsbefore checking Storybook. The theme module regenerates its variables from a dependency, and Linaria's dev cache does not invalidate on that; the built output is correct.Notes
scripts/spacing-audit.mjsis included — the Playwright harness that produced the geometry and screenshots behind every claim here.Not addressed, deliberately: md checkbox, radio, switch, and the toast close button are 20px, under the WCAG 2.2 target minimum of 24px. It is a real gap, but resizing them changes the proportions of every form in the system and deserves its own decision rather than riding along on a spacing pass.
Made with Cursor