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
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,41 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
in the editor canvas and inline styles + a `<style
data-ve-position>` block on the frontend via the Blade renderer.
Inspector shows a warning notice when `position: absolute` is
applied but no ancestor is positioned.
applied but no ancestor is positioned. Per-breakpoint authoring
follows the top-bar viewport switcher (via the shared
active-breakpoint store) — no separate control lives in the panel,
matching every sibling responsive-aware inspector panel.
- **Enabled `supports.position: true` on 82 top-level artisanpack
blocks (#645).** Every `resources/js/visual-editor/blocks/*/block.json`
that doesn't declare a `parent` or `ancestor` restriction now
opts in. Child-only blocks (accordion internals, list items,
columns, buttons children, query pagination, etc.) are
deliberately skipped — positioning a child that a container
relies on for layout breaks the container's contract.
`artisanpack/group` already carried Gutenberg's
`supports.position: { sticky: true }` object shape, which
`positionEnabled()` also recognizes; no change needed.
- **Position support on core containers (#646).** Functionally
subsumed by #645 given the I7 (#415) cutover — this repo no
longer calls `registerCoreBlocks()`, so no `core/*` block enters
the editor. Every container listed in the parent issue
(`core/group`, `core/columns`, `core/column`, `core/cover`,
`core/row`, `core/stack`, `core/grid`, `core/buttons`,
`core/image`) has an `artisanpack/*` fork covered by #645.
- **Integration + e2e test coverage (#647).** Round-trip
integration tests exercising resolver + emitter for a full
base + per-breakpoint payload, legacy sticky no-churn, and the
static-toggle-preserves-orphan-fields flow. Playwright e2e
spec (`tests/E2E/positioning.spec.ts`) documents the runtime
contract for sticky groups, absolute covers, ancestor
warnings, per-breakpoint viewport switching, and legacy
sticky — commented pending the Playwright runner (matches
the `animations.spec.ts` precedent).
- **Docs (#648).** New `docs/position.md` covering opt-in,
attribute shape, position values, offset units, per-breakpoint
inheritance, the ancestor warning, the two frontend emission
channels (inline + `<style data-ve-position>`), and a
troubleshooting section. Linked from `docs/home.md`.

### Upgrade notes

Expand Down
1 change: 1 addition & 0 deletions docs/home.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ The block library — what ships, how to author your own, and how to wire respon
- [[Animations]] — Block entrance, hover, and continuous animations (v1.1)
- [[Border Gradients]] — Linear / radial / conic borders + tabbed color/gradient picker (v1.1)
- [[Block Bindings]] — Bind block attributes to parent post/page/CPT data (v1.1)
- [[Position]] — CSS positioning (static / relative / absolute / fixed / sticky) with per-side offsets, z-index, and per-breakpoint overrides (v1.4)

---

Expand Down
180 changes: 180 additions & 0 deletions docs/position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# CSS Position

The Position panel lets editors pin, layer, or precisely place any
block that opts into `supports.position`. It extends Gutenberg's
built-in `position` support — the native shape stores a plain
`"sticky"` string; this system layers a structured object on top of
the same attribute path so all five CSS `position` values, per-side
offsets, `z-index`, and per-breakpoint overrides are first-class.

Parent tracking issue: [#640](https://github.com/ArtisanPack-UI/visual-editor/issues/640).

## Attribute shape

Stored on the block as `attributes.style.position`:

```json
{
"value": "sticky",
"offsets": {
"top": { "value": 0, "unit": "px" },
"bottom": { "value": 16, "unit": "px" }
},
"zIndex": 10
}
```

- `value`: one of `static | relative | absolute | fixed | sticky`.
- `offsets.<side>`: `{ value: number, unit: 'px' | '%' | 'rem' | 'em' | 'vh' | 'vw' | 'auto' } | null`.
Empty means unset (not `0`).
- `zIndex`: integer or `null`.

Per-breakpoint overrides ride the standard responsive bag,
`attributes.responsive['style.position']`, keyed by the breakpoint
prefix (`sm`, `md`, `lg`, `xl`, `2xl`). Missing fields inherit from
the next-smaller defined breakpoint, then from `base` — the same
mobile-first cascade the rest of the responsive system uses (see
[#487](https://github.com/ArtisanPack-UI/visual-editor/issues/487)).

Legacy content that stored `style.position` as a bare string (Gutenberg's
native sticky shape) is coerced to `{ "value": "sticky" }` on read
without touching the persisted attribute — no migration required.

## Opting a custom block in

Set `supports.position: true` in the block's `block.json`:

```json
{
"name": "myplugin/callout",
"supports": {
"position": true
}
}
```

Gutenberg's own object shape (`"position": { "sticky": true }`) also
counts as opted in — both flip the same gate. If a block has neither,
the Position panel doesn't render for it and no attributes are
stamped.

Child-only blocks (those with a `parent` or `ancestor` restriction that
locks them inside a specific container — `column` inside `columns`,
`accordion-title` inside `accordion`) should be deliberately left off;
positioning a child that a container relies on for layout will break
the container's contract.

## Position values

- **static** (default) — normal flow, no CSS emitted. Offsets and
`z-index` are preserved on the attribute so a flip back to a
positioned value doesn't lose values.
- **relative** — creates a positioning context for absolute descendants
and honors offsets from its natural flow location.
- **absolute** — takes the block out of flow. Positioned against the
nearest positioned ancestor (see the warning notice below).
- **fixed** — positioned against the viewport. Ignores containing
block scroll.
- **sticky** — behaves as `relative` until the block's containing
scroll container reaches the offset, then pins.

## Offset units

`px`, `%`, `rem`, `em`, `vh`, `vw`, `auto`. `auto` is unit-only — no
numeric value. Emitted as `top: auto` etc., matching CSS semantics.

## Per-breakpoint inheritance

Values are resolved mobile-first. Setting an offset only at `md`
means: `base` and `sm` inherit whatever was set at `base` (or
nothing); `md` and larger get the `md` override. The panel follows
the editor's top-bar viewport switcher — flip to a different
breakpoint there and the panel re-reads its effective values for
that breakpoint. The inspector surfaces an **Inherited** hint next
to any field that isn't explicitly overridden at the currently-viewed
breakpoint so authors know which value they'd have to change to
break inheritance.

The three values (position, offsets, `z-index`) inherit
independently — you can override just `z-index` at `md` and the
position + offsets keep flowing from `base`.

## Positioned-ancestor warning

When a block is set to `position: absolute` at the currently-viewed
breakpoint and none of its ancestor blocks is positioned, the
inspector renders a warning:

> This block is set to `position: absolute` but none of its ancestor
> blocks is positioned. It will position relative to the nearest
> positioned ancestor — often the page.

No ancestor is mutated automatically. The block still applies its
position — the warning is a heads-up that the placement will resolve
against the page (or whichever ancestor happens to be positioned),
not necessarily against the block's immediate parent.

To fix, set the parent block's position to any non-static value
(`relative` is the usual choice — it creates a positioning context
without changing the parent's own placement).

## Rendered CSS

Two channels emit on the frontend:

- **Wrapper inline style** — the base layer stamps as inline
declarations (`style="position: sticky !important; top: 0px !important;"`)
so the position survives hosts that strip `<style>` blocks from
block markup.
- **`<style data-ve-position>` block** — the Blade renderer's per-request
accumulator emits a single `<style>` element at the top of the
response with per-breakpoint rules wrapped in `@media (min-width:...)`
queries. Scope class is `.ve-pos-<id>` — same id lands on the
wrapper.

Both use `!important` on every declaration. The editor canvas's own
`.block-editor-block-list__layout .block-editor-block-list__block`
rule sets `position: relative` at higher specificity than a single
scope class, and theme resets frequently ship `!important` position
declarations on wrapper classes. Matching that specificity level is
the only way to reliably honor the user's explicit pick.

## Troubleshooting

**Sticky doesn't stick.** Sticky needs three things: (1) a non-`auto`
offset (usually `top`), (2) a scroll context — the block's nearest
ancestor with a defined height and `overflow: auto | scroll | hidden`,
and (3) the sticky block cannot fill its scroll container. If the
container's height exactly matches the sticky block's height, there's
nowhere to scroll and no sticky travel. Check that the parent column
has enough content below the sticky block to trigger scrolling.

**Absolute block sits in the wrong place.** The warning notice above
covers the common cause. If the parent is positioned but the offsets
still look wrong, check for a Gutenberg layout wrapper (e.g. `.wp-block-group__inner-container`)
between the block and its declared parent — layout wrappers create
their own containing block and may shift the coordinate origin.

**Frontend doesn't pick up new rules after an upgrade.** Hosts that
previously ran `php artisan vendor:publish --tag=visual-editor-blade-views`
have published copies of the Blade templates that shadow the package
source. Older published copies don't include the `<style data-ve-position>`
output block — the accumulator flushes into the void. Republish with:

```bash
php artisan vendor:publish --tag=visual-editor-blade-views --force
```

**Editor canvas shows position but the frontend doesn't.** Check the
page source for a `<style data-ve-position>` element AND a
`style="position: …"` inline attribute on the wrapper `<div>`. If
neither is present, the block's parent partial isn't calling
`BlockSupports::wrapperAttrs()` — verify the block's Blade template
uses that helper (or an equivalent that pipes `compile()` through
the wrapper's `class` + `style` attributes).

## Related

- [[Animations]] — same accumulator / scope-class pattern.
- [[Box Shadows]] — same accumulator pattern.
- [[Border Gradients]] — same accumulator pattern.
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/accordions/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"textdomain": "artisanpack-visual-editor",
"attributes": {},
"supports": {
"position": true,
"artisanpackAnimations": true,
"align": [
"wide",
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/archives/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": true,
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/audio/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": ["wide", "full"],
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/avatar/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"artisanpack/postPreview"
],
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"html": false,
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/breadcrumbs/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"align": [
"wide",
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/buttons/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
"textdomain": "artisanpack-visual-editor",
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": [
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/callout/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"className": true,
"html": false
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/categories/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"enhancedPagination"
],
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": true,
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/code/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"align": [
"wide"
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/columns/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"artisanpack/commentPreview"
],
"supports": {
"position": true,
"html": false,
"inserter": false,
"interactivity": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"viewportWidth": 300
},
"supports": {
"position": true,
"html": false,
"spacing": {
"margin": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"viewportWidth": 300
},
"supports": {
"position": true,
"html": false,
"color": {
"gradients": true,
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/comment-date/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"viewportWidth": 300
},
"supports": {
"position": true,
"html": false,
"color": {
"gradients": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"viewportWidth": 300
},
"supports": {
"position": true,
"html": false,
"color": {
"link": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"viewportWidth": 300
},
"supports": {
"position": true,
"color": {
"gradients": true,
"link": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"align": [
"wide",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"comments/paginationArrow": "paginationArrow"
},
"supports": {
"position": true,
"align": true,
"reusable": false,
"html": false,
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/comments/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"postType"
],
"supports": {
"position": true,
"artisanpackAnimations": true,
"anchor": true,
"align": [
Expand Down
1 change: 1 addition & 0 deletions resources/js/visual-editor/blocks/copyright/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
}
},
"supports": {
"position": true,
"artisanpackAnimations": true,
"align": [
"wide",
Expand Down
Loading