Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions configurator/src/data/classes.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion core/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion demo/index.html

Large diffs are not rendered by default.

35 changes: 29 additions & 6 deletions docs/api-index.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@
"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": {
"consumption": 488,
"knob": 253
},
"tokens": 741,
"classes": 327,
"sf_classes": 304,
"classes": 328,
"sf_classes": 305,
"is_classes": 15,
"unprefixed_classes": 8,
"by_category": {
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 5 additions & 4 deletions docs/api-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 <form> or <fieldset>. 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 |
|---|---|---|---|---|
Expand Down Expand Up @@ -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. |
Expand Down
5 changes: 3 additions & 2 deletions docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|
Expand Down Expand Up @@ -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` |
Expand Down
49 changes: 49 additions & 0 deletions docs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-*` |
Expand Down Expand Up @@ -164,3 +165,51 @@ explicitly by composing `.sf-cq`:
<div class="sf-grid-cols-3">…</div>
</div>
```

## 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
<div class="sf-place-center" style="min-block-size: 12rem">
<p>centred on both axes</p>
</div>
```

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.
3 changes: 2 additions & 1 deletion docs/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
],
"counts": {
"tokens": 741,
"sf_classes": 304,
"sf_classes": 305,
"is_classes": 15
}
},
Expand Down Expand Up @@ -978,6 +978,7 @@
"sf-overlap-host",
"sf-overlay",
"sf-pancake",
"sf-place-center",
"sf-prose",
"sf-reel",
"sf-scale-down",
Expand Down
1 change: 1 addition & 0 deletions docs/token-annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
5 changes: 3 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions tests/layout.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<div id="t" class="sf-place-center"><p>x</p></div>`);
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, `
<div id="box" class="sf-place-center" style="width:300px; height:200px">
<div id="c" style="width:60px; height:40px">x</div>
</div>
`);
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 }) => {
Expand Down