diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a9776f..4738f526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## Unreleased +### Features +- **layout:** add `.sf-place-center` — box-neutral centring primitive (`grid` + `place-items:center`) that centres content on both axes without adopting cluster/stack/imposter semantics + ## [0.7.27] - 2026-07-23 ### Breaking Changes diff --git a/configurator/src/data/classes.generated.json b/configurator/src/data/classes.generated.json index 6e6ecb76..b7afc728 100644 --- a/configurator/src/data/classes.generated.json +++ b/configurator/src/data/classes.generated.json @@ -2304,6 +2304,16 @@ "optional": false, "layer": "slashed.layout" }, + { + "name": "sf-place-center", + "selector": ".sf-place-center", + "kind": "layout", + "category": "Layout primitives", + "group": "", + "description": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", + "optional": false, + "layer": "slashed.layout" + }, { "name": "sf-prose", "selector": ".sf-prose", diff --git a/core/layout.css b/core/layout.css index ebedefb9..ceafed57 100644 --- a/core/layout.css +++ b/core/layout.css @@ -246,7 +246,19 @@ align-items: center; } - + /* Place-center — the box-neutral centring primitive. Unlike .sf-center (which + centres ITSELF as a max-width wrapper) this centres an element's CONTENT on + both axes, and unlike the flow primitives' --center modifiers it imposes no + row/column/wrap semantics: a plain element becomes a centring box. It is + the framework's named form of the community `display:grid; place-items: + center` idiom (cf. Tailwind's place-items-center). Give the box a size + (min-block-size / height) for the block-axis centring to have room. */ + .sf-place-center { + display: grid; + place-items: center; + } + + /* Cluster */ .sf-cluster { display: flex; diff --git a/demo/index.html b/demo/index.html index c03fcbf7..b0b22d3c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -3166,6 +3166,10 @@

Layout primitives .sf-center--intrinsic public

Modifier for sf-center: constrains the container to the natural width of its children (no stretching to fill available space).

centered & width-limited
+
+
.sf-place-center public

Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes, without adopting cluster/stack/imposter semantics. Give the box a height for block-axis centring.

+
centred both axes
+
.sf-cluster public

Flex-wrap row of variable-width items with automatic gap. Children wrap when the container is too narrow. Use for tag lists, button groups, and icon rows.

OneTwoThreeFourFive
@@ -5077,7 +5081,7 @@

Design tokens 7
--sf-z-tooltip
- + diff --git a/docs/api-index.json b/docs/api-index.json index 47fa5d25..94e1c870 100644 --- a/docs/api-index.json +++ b/docs/api-index.json @@ -25,14 +25,14 @@ "optimal" ], "counts": { - "total": 1068, + "total": 1069, "by_type": { - "class": 327, + "class": 328, "token": 741 }, "by_tier": { "INTERNAL": 1, - "PUBLIC": 1018, + "PUBLIC": 1019, "PUBLIC-ADVANCED": 49 }, "by_role": { @@ -40,8 +40,8 @@ "knob": 253 }, "tokens": 741, - "classes": 327, - "sf_classes": 304, + "classes": 328, + "sf_classes": 305, "is_classes": 15, "unprefixed_classes": 8, "by_category": { @@ -50,7 +50,7 @@ "Components": 30, "Core tokens": 610, "Forms": 1, - "Layout primitives": 145, + "Layout primitives": 146, "Layout tokens": 53, "Macro classes": 51, "Macro tokens": 32, @@ -24555,6 +24555,29 @@ "optimal" ] }, + { + "name": "sf-place-center", + "type": "class", + "tier": "PUBLIC", + "selector": ".sf-place-center", + "prefix": "sf", + "kind": "layout", + "category": "Layout primitives", + "area": "layout", + "group": "", + "description": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", + "isVariant": false, + "baseClass": null, + "optional": false, + "layer": "slashed.layout", + "sourceFiles": [ + "core/layout.css" + ], + "bundles": [ + "full", + "optimal" + ] + }, { "name": "sf-prose", "type": "class", diff --git a/docs/api-index.md b/docs/api-index.md index 2f2f3f2f..ea0acb94 100644 --- a/docs/api-index.md +++ b/docs/api-index.md @@ -10,11 +10,11 @@ and a short description. The machine-readable companion (with all columns) is [registry.json](registry.json); for the tier contract see [architecture.md](architecture.md). -**1068 elements** — 741 tokens, 327 classes. +**1069 elements** — 741 tokens, 328 classes. | Tier | Count | Meaning | |---|---|---| -| PUBLIC | 1018 | Everyday surface. SemVer-stable. | +| PUBLIC | 1019 | Everyday surface. SemVer-stable. | | PUBLIC-ADVANCED | 49 | Same SemVer guarantee; niche/powerful. | | INTERNAL | 1 | Implementation detail; may change without a major bump. | @@ -781,7 +781,7 @@ and a short description. The machine-readable companion (with all columns) is | `--sf-surface-bg-size` | PUBLIC | knob | surface | `cover` | background-size for the .sf-surface-bg image. Default: cover. | | `--sf-surface-color` | PUBLIC | consumption | surface | `var(--sf-color-base)` | Input for the generic .sf-surface macro. Set any color (including palette shades); the macro derives background, auto-contrast foreground, and the contextual token set from it. | -## Classes (327) +## Classes (328) ### Accessibility (9) @@ -838,7 +838,7 @@ and a short description. The machine-readable companion (with all columns) is |---|---|---|---|---| | `.sf-live-validate` | PUBLIC | form | — | Scopes native constraint-validation feedback to this subtree: within it, :user-invalid/:user-valid on input/select/textarea drive --sf-field-border-color (the same token .sf-is-invalid/.sf-is-valid set explicitly). Apply to a
or
. Off by default so a still-empty required field isn't marked invalid before a submit is attempted. | -### Layout primitives (145) +### Layout primitives (146) | Class | Tier | Kind | Group | Description | |---|---|---|---|---| @@ -957,6 +957,7 @@ and a short description. The machine-readable companion (with all columns) is | `.sf-imposter--fixed` | PUBLIC | layout | — | Imposter variant that uses position:fixed instead of absolute. Use for viewport-level overlays and modals. | | `.sf-overlay` | PUBLIC | layout | — | Overlay layout helper — absolutely fills its positioned parent (`position: absolute; inset: 0`) for overlays, backdrops, and media scrims. | | `.sf-pancake` | PUBLIC | layout | — | Three-row layout (header, main, footer) where main takes all remaining vertical space via flex-grow. The footer always sticks to the bottom on tall viewports. | +| `.sf-place-center` | PUBLIC | layout | — | Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom. | | `.sf-reel` | PUBLIC | layout | — | Horizontally scrollable row of fixed-height items with snap scrolling. Children don't shrink below their intrinsic width, creating a card carousel effect. | | `.sf-section` | PUBLIC | layout | — | Full-width page section with vertical padding (--sf-section-pad-default). Use to create visually distinct content bands. | | `.sf-section--2xl` | PUBLIC | layout | — | Section variant with 2xl vertical padding. | diff --git a/docs/classes.md b/docs/classes.md index 06c3ebd6..f7dc4ea0 100644 --- a/docs/classes.md +++ b/docs/classes.md @@ -3,12 +3,12 @@ > **Generated** from source by `scripts/gen-class-reference.js` — > run `npm run docs:classes` to refresh. Do not edit by hand. -**304 .sf-classes, 15 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class. +**305 .sf-classes, 15 .sf-is-classes.** Every `.sf-*` layout/macro class and `.sf-is-*` state class. See [architecture.md](architecture.md) for layer order and naming conventions. ## Layout primitives (`core/layout.css`) -145 classes. +146 classes. | Class | |---| @@ -127,6 +127,7 @@ See [architecture.md](architecture.md) for layer order and naming conventions. | `.sf-imposter--fixed` | | `.sf-overlay` | | `.sf-pancake` | +| `.sf-place-center` | | `.sf-reel` | | `.sf-section` | | `.sf-section--2xl` | diff --git a/docs/layout.md b/docs/layout.md index 3170df67..cdc49683 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -16,6 +16,7 @@ All primitives are exercised live in the [demo](/demo/). | `.sf-container` | centered max-width wrapper; declares the named `sf-layout` container; `--narrow` | `--sf-container-*`, `--sf-gutter` | | `.sf-box` | isolated unit with padding and optional border outline | `--sf-box-padding`, `--sf-box-border-width`, `--sf-box-border-color` | | `.sf-center` | intrinsic centering with max-width and gutters; `--intrinsic` | `--sf-center-max`, `--sf-center-gutter` | +| `.sf-place-center` | box-neutral centring: centres content on both axes (`grid` + `place-items:center`) in a plain element | — | | `.sf-stack` | vertical flow with consistent gap (the "owl") | `--sf-stack-gap` | | `.sf-cluster` | wrapping inline group; `--no-wrap` | `--sf-cluster-gap/-align/-justify` | | `.sf-sidebar` | content + fixed-ish side panel that wraps when narrow | `--sf-sidebar-*` | @@ -164,3 +165,51 @@ explicitly by composing `.sf-cq`:
``` + +## Centring: pick the primitive, not a `.center` utility + +Coming from a utility framework, `.sf-center` is a false friend. There, a +`.center` class usually *centres the content inside it* (flex `place-items`, +`margin: auto`, `text-align`). In SLASHED that job belongs to +**`.sf-place-center`**; `.sf-center` is the Every-Layout **Center primitive**: a +`max-width`, guttered wrapper that centres **itself** in its parent — it does not +centre its own children (unless you add `--intrinsic`). The two are a pair: +`.sf-center` centres *itself*, `.sf-place-center` centres *its content*. + +`.sf-place-center` is the box-neutral centring primitive — `display: grid; +place-items: center` — the named form of the community +`display:grid; place-items:center` idiom. Reach for it when you have a plain +element and just want its content centred on both axes, without adopting a +row/column/wrap primitive: + +```html +
+

centred on both axes

+
+``` + +Otherwise centring is expressed as intent on the right primitive: + +| To centre… | Use | Mechanism | +|---|---|---| +| content in a plain box, both axes | `.sf-place-center` | `grid` + `place-items` | +| a block wrapper horizontally | `.sf-center` (prose measure) or `.sf-container` (app wrapper, adds CQ) | `margin-inline: auto` | +| children in a row | `.sf-cluster--center` | `justify-content` | +| children in a column | `.sf-stack--center` | `align-items` | +| leftover grid items | `.sf-grid-flex--center` | `justify-content` | +| an element on both axes (overlay) | `.sf-imposter` | absolute + translate | +| content in a full-height region | `.sf-cover__center` | `margin-block: auto` | +| a wrapper's children by their intrinsic width | `.sf-center--intrinsic` | flex column + `align-items` | + +Two things to remember about `.sf-center` specifically: + +- **It centres itself, not its content.** For content-centring reach for the + modifiers above. +- **It needs a block-level element in normal flow.** `margin-inline: auto` + doesn't centre an inline element, and inside a flex or grid container the + parent's layout takes over the item's sizing and alignment, so the primitive's + own `width`/`margin` model no longer drives the result. `.sf-center` is + `content-box`, so `--sf-center-max` bounds the + *content* and the gutter sits outside it — pick it over `.sf-container` when + you want a typographic measure; pick `.sf-container` when you want an + app-level wrapper that also establishes a container-query scope. diff --git a/docs/registry.json b/docs/registry.json index c7394152..29bc1b52 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -20,7 +20,7 @@ ], "counts": { "tokens": 741, - "sf_classes": 304, + "sf_classes": 305, "is_classes": 15 } }, @@ -978,6 +978,7 @@ "sf-overlap-host", "sf-overlay", "sf-pancake", + "sf-place-center", "sf-prose", "sf-reel", "sf-scale-down", diff --git a/docs/token-annotations.json b/docs/token-annotations.json index 98471a68..ea8113aa 100644 --- a/docs/token-annotations.json +++ b/docs/token-annotations.json @@ -907,6 +907,7 @@ "sf-breakout": "Breaks a child element out of a parent sf-content-grid, spanning full bleed (or a named grid area) regardless of the parent column. Useful for full-width sections inside constrained prose.", "sf-center": "Centers children both horizontally and vertically using flexbox. Optionally add sf-center--intrinsic to size the container to its content.", "sf-center--intrinsic": "Modifier for sf-center: constrains the container to the natural width of its children (no stretching to fill available space).", + "sf-place-center": "Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom.", "sf-cluster": "Flex-wrap row of variable-width items with automatic gap. Children wrap when the container is too narrow. Use for tag lists, button groups, and icon rows.", "sf-cluster--2xl": "Cluster variant with 2xl gap between items.", "sf-cluster--between": "Cluster variant that distributes items with space-between alignment.", diff --git a/llms.txt b/llms.txt index 17cebea5..486bfe4d 100644 --- a/llms.txt +++ b/llms.txt @@ -224,7 +224,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid ## Classes -327 classes, grouped by area, each with a one-line purpose. `--variant` names are BEM modifiers of the base class above them. +328 classes, grouped by area, each with a one-line purpose. `--variant` names are BEM modifiers of the base class above them. ### Accessibility (9) @@ -245,7 +245,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid - `.print-no-color` — Strips color information in print (print-color-adjust: economy). Defers to the printer's ink-saving mode for backgrounds and non-critical color fills. - `.print-only` — Hides the element on screen (display: none) but shows it in @media print. Use for print-only headers, footers, and supplementary content not needed on screen. -### Layout primitives (145) +### Layout primitives (146) - `.sf-alternate` — Zigzag / alternating two-column layout. Even children align image-left/text-right; odd children flip. Collapses to a single column below the breakpoint. - `.sf-bento` — Auto-fill bento grid for card dashboards. Children span 1 column by default; use span modifiers (sf-bento-wide, sf-bento-tall, sf-bento-full, sf-bento-featured) to break the grid. @@ -362,6 +362,7 @@ Generated from source by `npm run docs` — do not edit by hand. Full prose guid - `.sf-imposter--fixed` — Imposter variant that uses position:fixed instead of absolute. Use for viewport-level overlays and modals. - `.sf-overlay` — Overlay layout helper — absolutely fills its positioned parent (`position: absolute; inset: 0`) for overlays, backdrops, and media scrims. - `.sf-pancake` — Three-row layout (header, main, footer) where main takes all remaining vertical space via flex-grow. The footer always sticks to the bottom on tall viewports. +- `.sf-place-center` — Box-neutral centring primitive: display:grid + place-items:center centres an element's content on both axes without adopting cluster/stack/imposter semantics. Unlike sf-center (which centres itself), this centres the content; give the box a height for block-axis centring. The named form of the display:grid; place-items:center idiom. - `.sf-reel` — Horizontally scrollable row of fixed-height items with snap scrolling. Children don't shrink below their intrinsic width, creating a card carousel effect. - `.sf-section` — Full-width page section with vertical padding (--sf-section-pad-default). Use to create visually distinct content bands. - `.sf-section--2xl` — Section variant with 2xl vertical padding. diff --git a/tests/layout.spec.js b/tests/layout.spec.js index b867095a..0fd4050b 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -506,6 +506,40 @@ test.describe('layout: .sf-center', () => { }); }); +// ── .sf-place-center ──────────────────────────────────────────── +test.describe('layout: .sf-place-center', () => { + test('is a grid with place-items: center', async ({ page }) => { + await setup(page, `

x

`); + const cs = await page.locator('#t').evaluate(el => ({ + display: getComputedStyle(el).display, + align: getComputedStyle(el).alignItems, + justify: getComputedStyle(el).justifyItems, + })); + expect(cs.display).toBe('grid'); + expect(cs.align).toBe('center'); + expect(cs.justify).toBe('center'); + }); + + test('centres its content on both axes when the box has a size', async ({ page }) => { + // margin/place resolve to pixel positions; verify centring via bounding boxes. + await setup(page, ` +
+
x
+
+ `); + const res = await page.evaluate(() => { + const box = document.getElementById('box').getBoundingClientRect(); + const c = document.getElementById('c').getBoundingClientRect(); + return { + dx: Math.abs((c.left - box.left) - (box.right - c.right)), + dy: Math.abs((c.top - box.top) - (box.bottom - c.bottom)), + }; + }); + expect(res.dx).toBeLessThan(2); + expect(res.dy).toBeLessThan(2); + }); +}); + // ── .sf-box ───────────────────────────────────────────────────── test.describe('layout: .sf-box', () => { test('has non-zero padding', async ({ page }) => {