Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- **Native flex layout panel on group / column / columns / grid-item
(#595).** New `Flex Layout` + `Flex Item` inspector panels expose
every CSS flexbox property — direction, wrap, justify, align-items,
align-content, place-content, row/column gap, plus per-item
align-self, grow, shrink, basis, order — each per-breakpoint via the
existing `<ViewportSwitcher />`. Replaces WordPress core's narrow
Flex layout variation on `artisanpack/group` (suppressed via filter)
and layers on top of the default `artisanpack/columns` distribution.
Class output mirrors Tailwind's utility convention (`ap-flex`,
`md:ap-justify-between`, `ap-gap-x-[16px]`, …) and is asserted
byte-identical across the Blade, React, and Vue renderers via a
shared fixture set. Legacy `layout.type === 'flex'` content on
`artisanpack/group` migrates automatically on first edit. See
[[blocks/Flex Layout]] for the full surface.

## [1.1.0] — 2026-06-14

The 1.1.0 release ships the full `artisanpack/icon` block (Phases 1–7),
Expand Down
122 changes: 122 additions & 0 deletions docs/blocks/Flex-Layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Flex Layout (#595)

The Flex Layout inspector panel exposes the full CSS flexbox property
surface on `artisanpack/group`, `artisanpack/columns`,
`artisanpack/column`, and `artisanpack/grid-item`. It is per-breakpoint
via the existing `<ViewportSwitcher />` so every value cascades the
same way `spacing` / `width` do.

## Where it appears

A "Flex Layout" panel renders in the block inspector for all four
blocks above. On `artisanpack/group` it **replaces** WordPress core's
Flex layout variation (the core flex choice is hidden via a
`blocks.registerBlockType` filter). On `artisanpack/columns` it
**layers on** core's column-distribution: our values win only when
explicitly set.

A sibling "Flex Item" panel appears on `group`, `column`, and
`grid-item` whenever the block is the direct child of a flex
container. When the parent is not flex, the panel renders disabled
with a tooltip — the values are not silently deleted.

## Container controls

| Control | CSS property | Values |
| --- | --- | --- |
| **Enable Flex** | `display: flex` | toggle |
| **Direction** | `flex-direction` | `row`, `column`, `row-reverse` (advanced), `column-reverse` (advanced) |
| **Wrap** | `flex-wrap` | `nowrap`, `wrap`, `wrap-reverse` (advanced) |
| **Justify Content** | `justify-content` | `flex-start`, `center`, `flex-end`, `space-between`; advanced: `space-around`, `space-evenly`, `start`, `end`, `left`, `right` |
| **Align Items** | `align-items` | `stretch`, `flex-start`, `center`, `flex-end`, `baseline`; advanced: `start`, `end`, `self-start`, `self-end`, `first baseline`, `last baseline` |
| **Align Content** | `align-content` | `stretch`, `flex-start`, `center`, `flex-end`, `space-between`; advanced: `space-around`, `space-evenly`, `start`, `end`, `baseline`. Disabled unless Wrap is `wrap` / `wrap-reverse`. |
| **Place Content** | `place-content` (shorthand) | advanced only; greys per-axis controls when set, with a "Reset shorthand" link. |
| **Row Gap** | `row-gap` | any CSS length |
| **Column Gap** | `column-gap` | any CSS length |

## Item controls

| Control | CSS property | Values |
| --- | --- | --- |
| **Align Self** | `align-self` | `auto`, `flex-start`, `center`, `flex-end`, `stretch`, `baseline`; advanced: `start`, `end`, `self-start`, `self-end` |
| **Grow** | `flex-grow` | numeric (0–999) |
| **Shrink** | `flex-shrink` | numeric (0–999) |
| **Basis** | `flex-basis` | `auto`, `0`, `full`, `fit-content`, `max-content`, `min-content`, or any CSS length |
| **Order** | `order` | numeric (-999 to 999) |

## Generated classes

The serializer emits utility classes on the wrapper (no inline styles,
no CSS custom properties). Shape mirrors Tailwind:

```
ap-flex
ap-flex-{row|col|row-reverse|col-reverse}
ap-flex-{nowrap|wrap|wrap-reverse}
ap-justify-{start|center|end|between|around|evenly|left|right|stretch|baseline}
ap-items-{start|center|end|stretch|baseline|self-start|self-end|first-baseline|last-baseline}
ap-content-{start|center|end|between|around|evenly|stretch|baseline}
ap-place-content-[value]
ap-gap-x-[value] ap-gap-y-[value]

ap-self-{auto|start|center|end|stretch|baseline|self-start|self-end}
ap-grow-{n} ap-grow-[n]
ap-shrink-{n} ap-shrink-[n]
ap-basis-{auto|0|full|...} ap-basis-[value]
ap-order-{n} ap-order-[n]
```

Responsive prefixes: `sm:`, `md:`, `lg:`, `xl:`, `2xl:` — matching the
keys registered in `BreakpointRegistry`.

Arbitrary values (e.g. `ap-gap-x-[3.5rem]`, `ap-basis-[200px]`) come
with a per-page `<style>` snippet emitted by the renderer — they don't
balloon the static stylesheet.

## Theme.json defaults

Themes can opt out or set defaults via `settings.artisanpack.flex` in
their theme.json:

```php
'settings' => [
'artisanpack' => [
'flex' => [
'enable' => true,
'defaultDirection' => 'row',
'defaultJustifyContent' => null,
'defaultAlignItems' => null,
'defaultGap' => [ 'row' => null, 'column' => null ],
],
],
],
```

Setting `enable` to `false` hides both panels in the inspector but
does **not** change the rendering of already-saved content — wrapper
classes still emit through the renderer.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

## Renderer parity

The single source of truth is `resources/js/visual-editor/blocks/
_shared/flex-controls/serializer.ts`. The Blade `FlexSupport`, the
React renderer's `support/flex-serializer.ts`, and the Vue renderer's
`support/flex-serializer.ts` all mirror that exactly. Parity is
guarded by a shared `fixtures.json` consumed by:

- the editor Jest suite (`serializer.test.ts`)
- the Blade Pest suite (`FlexSupportTest.php`)
- the React vitest suite (`flex-serializer.test.ts`)
- the Vue vitest suite (`flex-serializer.test.ts`)

Adding a new behavior means adding a fixture; every renderer's CI run
fails until it produces the expected output.

## Migration from `layout.type === 'flex'`

Group blocks with the legacy `layout.type === 'flex'` shape migrate
to `artisanpackFlex.container` on first edit via the block
deprecation chain. Unedited content keeps rendering through its
existing markup. See [[migration/From Crosswinds Blocks]] for the
crosswinds-blocks → ArtisanPack UI mapping if you're coming from that
plugin.
33 changes: 33 additions & 0 deletions docs/migration/From-Crosswinds-Blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Migrating from `crosswinds-blocks` (#595)

The legacy `crosswinds-blocks` plugin added a narrow set of flexbox
helpers to `core/group`, `core/column`, and `core/post-template` —
three toggles plus a five-option dropdown. The ArtisanPack UI Flex
Layout panel covers the full flexbox surface and replaces the
crosswinds helpers entirely.

**There is no automatic migration**: crosswinds-blocks ran in a
separate project and we don't expect its content to land in
visual-editor pages. This page is the hand-migration reference if you
do bring content over.

## Mapping

| crosswinds attribute | New attribute | Notes |
| --- | --- | --- |
| `cbUseFlex: true` | `artisanpackFlex.container.enabled: { base: true }` | Toggle the Enable Flex switch. |
| `cbFillHeight: true` | `artisanpackFlex.item.grow: { base: 1 }` on the child you want to fill | Crosswinds applied this to the child; ours is a per-item property. |
| `cbInnerLayout: 'normal'` | (no flex; default flow) | Disable the panel. |
| `cbInnerLayout: 'equal'` | `direction: column` + `justifyContent: space-between` | Vertical column with children pushed apart. |
| `cbInnerLayout: 'center'` | `alignItems: center` (+ `justifyContent: center` if you want both axes) | Centers children. |
| `cbInnerLayout: 'bottom'` | `direction: column` + `justifyContent: flex-end` | All children pushed to the bottom. |
| `cbInnerLayout: 'last-bottom'` | On the last child: `alignSelf: flex-end` (column direction) or `marginTop: auto` (row direction) | Crosswinds pinned the last item; align-self gives equivalent control. |

## Why we didn't port the enum as-is

The crosswinds enum solved a real problem (authors couldn't reach
"fill remaining height" / "center children" without dropping into the
Advanced panel), but it masked a much richer property surface.
Replacing it with per-property controls means every author can express
any flexbox layout without writing CSS — the original goal — and the
panel grows naturally as flexbox itself does.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@php
use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
use ArtisanPackUI\VisualEditorRendererBlade\Support\FlexSupport;

$validInnerLayouts = [ 'normal', 'equal', 'center', 'bottom', 'last-bottom' ];

Expand Down Expand Up @@ -68,6 +69,7 @@
'ap-grid-item-layout-' . $innerLayout,
],
$spanClasses,
FlexSupport::wrapperForBlock( $attributes ),
);
@endphp
<div{!! BlockSupports::wrapperAttrs( $attributes, $baseClasses ) !!}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
@php
use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
use ArtisanPackUI\VisualEditorRendererBlade\Support\FlexSupport;

$baseClasses = [ 'wp-block-column' ];

if ( ! empty( $attributes['verticalAlignment'] ) ) {
$baseClasses[] = 'is-vertically-aligned-' . $attributes['verticalAlignment'];
}

$baseClasses = array_merge( $baseClasses, FlexSupport::wrapperForBlock( $attributes ) );

// `flex-basis` from the column's `width` attribute has to merge
// with any block-supports inline style — compile() lets us splice
// it in alongside the support declarations rather than emitting
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@php
use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
use ArtisanPackUI\VisualEditorRendererBlade\Support\FlexSupport;

$baseClasses = [ 'wp-block-columns' ];

Expand All @@ -12,6 +13,8 @@
$baseClasses[] = 'are-vertically-aligned-' . $attributes['verticalAlignment'];
}

$baseClasses = array_merge( $baseClasses, FlexSupport::wrapperForBlock( $attributes ) );

// #487 — columns-specific responsive layout. `columnCount` overrides
// translate to per-breakpoint `grid-template-columns` rules. The
// generic responsive emitter in BlockSupports handles
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@php
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
use ArtisanPackUI\VisualEditorRendererBlade\Support\FlexSupport;

$tag = isset( $attributes['tagName'] ) && in_array( $attributes['tagName'], [ 'div', 'section', 'article', 'aside', 'header', 'footer', 'main', 'nav' ], true )
? $attributes['tagName']
Expand All @@ -16,7 +17,10 @@
} else {
$layoutClass = 'is-layout-flow';
}

$flexClasses = FlexSupport::wrapperForBlock( $attributes );
$wrapperBaseClasses = array_merge( [ 'wp-block-group', $layoutClass ], $flexClasses );
@endphp
<{{ $tag }}{!! BlockSupports::wrapperAttrs( $attributes, [ 'wp-block-group', $layoutClass ] ) !!}>
<{{ $tag }}{!! BlockSupports::wrapperAttrs( $attributes, $wrapperBaseClasses ) !!}>
{!! $innerBlocksHtml !!}
</{{ $tag }}>
Loading
Loading