Drag the border between two columns to set their widths in 1.16.0
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

Umut, with a card of an icon next to a text as the example: "bei Rows die
Breite ändern, am besten per Drag and Drop im Editor". Rows only ever had
equal columns - both the classic `columns` token and the free `gridColumns`
produce `repeat(n, 1fr)`.

A row now carries the free style `columnWidths`, a list of shares such as
`2 1` or `66.7 33.3`, per device and without a new document schema.
`StyleCompiler::columnTracks()` turns it into `grid-template-columns`,
pads or cuts the shares to the number of columns, and gives a device whose
layout stacks the columns a single track, because the desktop rule carries
an id and would otherwise outrank the class that stacks a phone.

In the editor the border between two side-by-side columns of the selected
row is a handle: dragging restyles the row inline while the pointer moves
and writes one undo step on release; the arrow keys nudge by five points.
The design panel shows the same value as the text field "Spaltenbreiten".
The Playwright editor check drags the handle with the real mouse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-15 10:39:06 +02:00
1 parent f6350de636
commit 32ee28888e
14 files changed
+346 -3

No files matched your search

+57
View File
@@ -92,6 +92,11 @@ final class StyleCompiler
sprintf('"%s" is outside its range.', $key),
),
'font' => $this->require(1 === preg_match('/^[a-z][a-z0-9-]{0,39}$/D', $text), sprintf('"%s" is not a font key.', $key)),
// A share of nothing is a column that cannot be seen, so zero is refused with the rest.
'fractions' => $this->require(
1 === preg_match(StyleSchema::FRACTIONS, $text) && !in_array(0.0, array_map(floatval(...), explode(' ', $text)), true),
sprintf('"%s" is not a list of column shares.', $key),
),
'media' => $this->require('none' === $text || 1 === preg_match('/^[a-f0-9]{32}$/D', $text), sprintf('"%s" is not a media ID.', $key)),
default => $this->require(false, 'Unknown style type.'),
};
@@ -112,6 +117,9 @@ final class StyleCompiler
if ('image' === ($block['type'] ?? null)) {
$css .= $this->imageAlignment((string) $block['id'], $block['style'], $parent);
}
if ('row' === ($block['type'] ?? null)) {
$css .= $this->columnTracks((string) $block['id'], $block['style'], is_array($block['layout'] ?? null) ? $block['layout'] : [], count($block['children'] ?? []));
}
}
$walk($block['children'] ?? [], $block);
}
@@ -461,6 +469,55 @@ final class StyleCompiler
return $chosen;
}
/**
* The column shares of a row: "2 1" becomes two tracks of 2fr and 1fr. Shares inherit from the
* wider device like every other style. A device whose layout stacks the columns - "one" from
* the classic tokens or a single grid column - gets one track instead: the desktop rule carries
* an id and would otherwise outrank the class that stacks the columns on a phone.
*/
private function columnTracks(string $id, array $style, array $layout, int $count): string
{
$css = $previous = '';
$widths = null;
$stacked = false;
foreach (['desktop', 'tablet', 'mobile'] as $device) {
$values = is_array($style[$device] ?? null) ? $style[$device] : [];
$widths = $values['columnWidths'] ?? $widths;
if (isset($values['gridColumns'])) {
$stacked = 1 === (int) $values['gridColumns'];
} elseif (isset($layout['design'][$device]['columns'])) {
$stacked = 'one' === $layout['design'][$device]['columns'];
}
if (null === $widths || $count < 2) {
continue;
}
$rule = sprintf('#%s{grid-template-columns:%s}', $id, $stacked ? 'minmax(0,1fr)' : $this->tracks((string) $widths, $count));
if ($rule === $previous) {
continue;
}
$css .= isset(self::BREAKPOINTS[$device])
? sprintf('@media (max-width:%dpx){%s}', self::BREAKPOINTS[$device], $rule)
: $rule;
$previous = $rule;
}
return $css;
}
/** One track per column: a column added later gets the average share, surplus shares are dropped. */
private function tracks(string $widths, int $count): string
{
$shares = array_values(array_filter(array_map(floatval(...), explode(' ', $widths)), static fn (float $share): bool => $share > 0));
$shares = array_slice($shares, 0, $count);
$average = [] === $shares ? 1.0 : array_sum($shares) / count($shares);
while (count($shares) < $count) {
$shares[] = $average;
}
$format = static fn (float $share): string => rtrim(rtrim(number_format($share, 1, '.', ''), '0'), '.');
return implode(' ', array_map(static fn (float $share): string => sprintf('minmax(0,%sfr)', $format($share)), $shares));
}
private function grid(array $values): string
{
return isset($values['gridColumns'])
+5
View File
@@ -27,6 +27,8 @@ final class StyleSchema
*/
public const LENGTH = '/^(0|-?\d{1,5}(\.\d{1,3})?(px|rem|em|%|vw|vh|ch|dvh|svh|lvh|dvw))$/D';
public const LENGTH_AUTO = '/^(auto|none|0|-?\d{1,5}(\.\d{1,3})?(px|rem|em|%|vw|vh|ch|dvh|svh|lvh|dvw))$/D';
/** Column shares of a row, up to twelve numbers separated by blanks: "2 1" or "66.7 33.3". */
public const FRACTIONS = '/^\d{1,3}(\.\d)?( \d{1,3}(\.\d)?){0,11}$/D';
/**
* group – where the property appears in the editor
@@ -152,6 +154,9 @@ final class StyleSchema
'stretch' => 'Dehnen', 'flex-start' => 'Anfang', 'center' => 'Mitte', 'flex-end' => 'Ende', 'baseline' => 'Grundlinie',
]],
'gridColumns' => ['group' => 'layout', 'type' => 'number', 'css' => null, 'label' => 'Spalten', 'min' => 1, 'max' => 12, 'step' => 1],
// Die Anteile der Spalten einer Zeile: "2 1" ist zwei Drittel zu einem Drittel. Gesetzt wird
// das vor allem durch Ziehen der Spaltengrenze in der Vorschau des Editors.
'columnWidths' => ['group' => 'layout', 'type' => 'fractions', 'css' => null, 'label' => 'Spaltenbreiten'],
'gap' => ['group' => 'layout', 'type' => 'length', 'css' => 'gap', 'label' => 'Abstand zwischen Elementen'],
'order' => ['group' => 'layout', 'type' => 'number', 'css' => 'order', 'label' => 'Reihenfolge', 'min' => -20, 'max' => 20, 'step' => 1],
'rowGap' => ['group' => 'layout', 'type' => 'length', 'css' => 'row-gap', 'label' => 'Abstand zwischen Zeilen'],