@minhspark/spacing-skill 2.12.1 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +18 -18
- package/.claude-plugin/marketplace.json +20 -20
- package/.claude-plugin/plugin.json +22 -22
- package/.codex-plugin/plugin.json +6 -6
- package/CHANGELOG.md +27 -14
- package/LEARNINGS.md +233 -188
- package/LICENSE +21 -21
- package/README.md +245 -223
- package/gemini-extension.json +6 -6
- package/package.json +22 -22
- package/skills/spacing-skill/SKILL.md +16 -0
|
@@ -267,6 +267,7 @@ Unbounded text on wide viewports destroys readability. **Cap the measure at 45
|
|
|
267
267
|
|
|
268
268
|
- Mixed-height inline items must align to text → `align-items:baseline`, not `center`.
|
|
269
269
|
- Equal-height cards in a row → `align-items:stretch` + a flex-column card with `margin-top:auto` on the footer (the one sanctioned auto-margin — it pushes *within* a flex child, owns nothing between siblings).
|
|
270
|
+
- **That auto-margin only pushes when the child has slack — otherwise it is silently `0`.** `margin-top:auto` consumes *free space*; in a column that hugs its content (`flex:none`, `height:auto`, a grid cell sized to `max-content`) there is none, so the footer lands flush against the block above and the gap you "set" never existed. No warning, no overflow, nothing to see in the rule — the declaration is present and its computed value is zero. Measured across one layout's two variants: an identical footer meant to clear its subtitle rendered **44px** in the landscape build, where the column was the flex item absorbing leftover height, and **0px** in the portrait build, where the same column was `flex:none`. Reach for `auto` only where stretch is guaranteed; everywhere else give the gap a real value from the scale (§2). The symptom generalizes: *a spacing rule whose value depends on free space disappears the moment the container stops having any.*
|
|
270
271
|
- A bottom-aligned row that **mixes an editable control with static text** aligns the *boxes*, not the *text*: the control carries a touch-target `min-height` (44/48px) and centers its text, while a bare `<span>` is one line tall and sits flush at the bottom — so the two labels land at visibly different heights. Give every slot the same box (`min-height` + padding) and center the static ones (`display:flex; align-items:center`). The touch-target floor (§9) is an alignment constraint, not just an ergonomic one.
|
|
271
272
|
- **A hug-width child inside a Stack still fills the cross axis.** `width:auto` (`w-auto`, `fit-content`) says nothing about the cross axis of a `flex-direction:column` parent — `align-items:stretch`, the default, does. A secondary button sized to hug its label renders full-bleed until the child gets `align-self:start` (or the Stack gets `align-items:start`). Measured on one settings screen: `sm:w-auto` alone left the button at **640px**; adding `sm:self-start` brought it to **184px**, its true content width. Same trap for any hug-sized chip, badge, or card stacked vertically — and it hides on mobile, where full-bleed is what you wanted anyway, then surfaces at the first wide breakpoint.
|
|
272
273
|
- **A leading icon opens a text column — the row's actions belong in it.** In an icon + title/description row, the copy starts at `icon width + gap` while a control placed after it, being a sibling of the icon block, starts back at the container inset; the eye reads two competing left edges. Indent the action by exactly that amount (icon 44 + gap 12 → `padding-inline-start: 56px`) so title, description, and control share one spine. Mandatory at `RIGOR ≥ 7`. Below `sm`, where the control goes full-width for touch, drop the indent and let it align to the card edge instead — one spine per breakpoint, never a half-step between them.
|
|
@@ -485,6 +486,8 @@ Author with `padding-block` / `padding-inline` / `margin-inline` / `inset-*`, no
|
|
|
485
486
|
|
|
486
487
|
Pad against `env(safe-area-inset-*)` for edge-anchored UI (fixed headers, bottom tab bars, FABs). Requires `<meta name="viewport" content="…, viewport-fit=cover">` or insets resolve to 0. **Add to your token, never replace it:** `padding-block-end: calc(1rem + env(safe-area-inset-bottom, 0px))`.
|
|
487
488
|
|
|
489
|
+
**Anything composited *over* the layout is a safe area too** — burnt-in subtitles, a player's control bar, a stream HUD, a watermark, a floating cookie bar. `env()` knows nothing about these, so the band has to be reserved by hand: measure the overlay's real box (font size × line count + its own bottom margin), then add it to the container's block-end padding. Measured on a 1920×1080 slide: content was padded 72px from the bottom while a 46px caption sat 56px up, so the caption's box covered the last two lines of a panel; padding the layout to 184px cleared it. The trap is *when* it appears — the layout is correct in the browser and only breaks once the overlay is composited, so it survives every check that looks at the page instead of the finished frame. Reserve the band in the layout; never rely on the overlay landing somewhere empty.
|
|
490
|
+
|
|
488
491
|
### 8.F `vw` pitfalls
|
|
489
492
|
|
|
490
493
|
Never size spacing/type with bare `vw` (no floor/ceiling, ignores zoom) — always wrap in `clamp()` with a `rem` term. Avoid `100vw` (scrollbar overflow → use `100%`/`100dvw`); prefer `dvh`/`svh`/`lvh` over `vh` so mobile URL-bar changes don't jump the layout.
|
|
@@ -502,6 +505,15 @@ html { scroll-padding-block-start: calc(var(--header-h) + var(--space-2));
|
|
|
502
505
|
|
|
503
506
|
Animate `opacity`/`transform`, never layout (`gap`/`padding`/`width`) — animated layout reflows the grid and breaks rhythm. Stagger list reveals by *time*, not by spacing. Honor `prefers-reduced-motion`.
|
|
504
507
|
|
|
508
|
+
### 8.I A resize is a round trip — re-measure what the toolkit cached
|
|
509
|
+
|
|
510
|
+
Checking a layout once at each width proves each width, not the trip between them. Toolkits keep sizes they measured while the container was narrow: grid rows auto-sized only while visible, controls that auto-size and only ever grow (WinForms `GrowOnly`), fill columns whose working ratio shifted when one of them hit its minimum width, virtualized lists that cache measured row heights. Widen the container and those sizes stay: rows too tall, a toolbar still wrapped, the least important column still wide. Measured on one WinForms table: two rows grown to 78px at 1000px kept 78px at 1680px where they needed the 54px floor, and the name / status / file columns came back as 216 / 151 / 112px instead of 226 / 157 / 96px at the same width. **Once the width settles, re-measure every item — off-screen ones included — and put working ratios back to the declared ones;** debounce it (~150ms) so dragging stays cheap (a full pass cost 131ms at 300 rows, a drag step 15ms). Test the round trip narrow → wide → narrow, not each width once.
|
|
511
|
+
|
|
512
|
+
- **WinForms `DataGridView`:** while an `AutoSizeRowsMode` is active, `AutoResizeRows` only caches heights — set the mode to `None`, run `AutoResizeRows(AllCellsExceptHeaders)`, restore the mode. Nudging one `FillWeight` up and back makes fill columns redistribute from their current weights (a column the user dragged keeps its width: the drag already rewrote its `FillWeight`).
|
|
513
|
+
- **The settle pass must not re-arm itself.** Resetting a ratio or re-measuring changes widths, which fires the same width-changed event that scheduled the pass. Ignore the events your own pass raises, and re-measure only when the widths really moved; otherwise the pass runs every debounce interval forever (measured: a 150ms timer that never settled, so a test harness waiting for the table to go idle timed out after 240s).
|
|
514
|
+
- **Virtualized lists:** reset the measured-size cache when the container width changes (react-window `resetAfterIndex(0)`, react-virtualized `CellMeasurerCache.clearAll()`).
|
|
515
|
+
- **Auto-sized controls:** a label that changes with data must keep a constant width or reserve its widest text; `GrowOnly` never gives the space back, so one transient long caption keeps a toolbar wrapped for the rest of the session.
|
|
516
|
+
|
|
505
517
|
---
|
|
506
518
|
|
|
507
519
|
## 9. ACCESSIBILITY — Spacing Floors That Override Every Dial
|
|
@@ -623,7 +635,10 @@ Spottable in a screenshot or diff in under three seconds. The meta-rule: **spaci
|
|
|
623
635
|
| 19 | **Centering a clamped text box** (`items-center` on a lockup whose name wraps) | the item is clamped to the container, so the centered box is wider than its ink and the group drifts toward the leading edge — worse the wider the viewport | make the wrap deterministic so `max-content` = the longest line, then center (§4.E) |
|
|
624
636
|
| 20 | **Unlayered critical CSS beside a layered design system** (a bare `<style>` holding `input{font:inherit}`) | layer order is settled before specificity, so the plain selector beats every `@layer utilities` class — the class is on the element, the rule is in the sheet, and the value is still wrong | wrap the block in the system's own layer: `@layer base { … }` (§12) |
|
|
625
637
|
| 22 | **`@page { margin: 0 }` on a multi-page document to kill the browser's print header/footer** | root padding is not per-page — left/right repeats, top/bottom lands only on the first and last sheet, so middle pages print edge-to-edge and the printer's hardware margin clips their top and bottom rows | scope the zero-margin page to single-page documents with a named `@page`, leave long reports on a real margin (§12) |
|
|
638
|
+
| 23 | **`margin-top:auto` as a gap in a hug-height container** (`flex:none` column, `max-content` cell) | no free space to consume, so it computes to `0` and the footer sits flush — the rule is in the sheet and the gap is not on screen | `auto` only where stretch is guaranteed; otherwise a real scale value (§4.C) |
|
|
639
|
+
| 24 | **Layout padded as if nothing will be drawn over it** (burnt-in caption / HUD / control bar lands on content) | `env()` does not see composited overlays, and the page looks correct right up until the frame is rendered | reserve the band: block-end padding ≥ the overlay's measured box (§8.E) |
|
|
626
640
|
| 21 | **Bottom-aligning a row that mixes inputs with plain text** (`margin-top:auto` on both) | the input's touch-target `min-height` makes its box ~2× taller than the one-line span, so the two texts sit at different heights even though both boxes are flush at the bottom | give every slot the same `min-height` + padding and vertically center the static ones (§4.C) |
|
|
641
|
+
| 25 | **Checking each width once, never the trip between them** (layout verified at 1000px and at 1680px, separately) | toolkits keep what they measured while narrow, so after widening rows stay tall, toolbars stay wrapped and fill ratios stay skewed — every single-width check still passes | re-measure every item once the width settles, off-screen rows included, and test narrow → wide → narrow (§8.I) |
|
|
627
642
|
|
|
628
643
|
**Magic-number triage:** snap `5/6/7→8`, `10–15→8/12/16`, `17–22→16`, `23–26→24`. The only sanctioned non-multiples are hairline borders (1px), 0.5px retina rules, and optical nudges ≤4px (typically 1–2px; up to 4px only for large display glyphs). Font-driven values (line-height, cap offsets, derived control insets) are computed, not magic — exempt.
|
|
629
644
|
|
|
@@ -748,6 +763,7 @@ Override deltas apply to the **current** dial value and clamp to each dial's `[1
|
|
|
748
763
|
- [ ] Dials re-evaluated per breakpoint; layout reflows, never shrinks to illegible.
|
|
749
764
|
- [ ] No horizontal scroll at 320px; grids collapse to a stack; 2-D-scroll content isolated in its own container.
|
|
750
765
|
- [ ] Section/component spacing fluid via `clamp()` with a `rem` term; safe-area insets on edge-anchored UI.
|
|
766
|
+
- [ ] Round trip narrow → wide → narrow returns the same layout; rows, columns and controls measured while narrow are re-measured once the width settles, off-screen rows included (§8.I).
|
|
751
767
|
|
|
752
768
|
**A11y**
|
|
753
769
|
- [ ] Interactive targets ≥ 24×24px (or the 24px-circle spacing exception); 44/48 on touch.
|