Cluster menu items by a gap, as Expressive does

Plan step 22, actions.md § Missing (Grouped menu layout by gap): M3
Expressive's "Grouped" layout separates clusters with a gap rather than a
divider, and `<x-menu-group>` only had the labelled form.

`<x-menu-group gap>` holds its items in a box 2px apart
(SegmentedMenuTokens.SegmentedGap) whose ends round like a list's, and
stands 8px off its neighbours — the same 8px the separator keeps around
its line. `label` is now optional, so a cluster can be a gap alone. The
header says when to reach for which: the divider first, and always in a
menu long enough to scroll, where M3 says gaps are unsupported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:34:19 +02:00
co-authored by Claude Opus 5
parent 25091ebf21
commit 3f651c5c08
4 changed files with 96 additions and 5 deletions
+19
View File
@@ -103,6 +103,25 @@ it('separates and labels groups', function () {
->assertSee('role="group" aria-label="Sort by"', false);
});
it('separates clusters by a gap instead of a divider when asked', function () {
$html = (string) $this->blade('<x-menu-group gap><x-menu-item label="Cut" /></x-menu-group>');
// M3 Expressive's grouped layout: 2px between the items of a cluster, 8px between clusters —
// the same 8px the divider keeps above and below its line.
expect($html)
->toContain('role="group"')
->toContain('not-first:mt-2')
->toContain('space-y-0.5')
->not->toContain('aria-label')
->not->toContain('py-1 first:pt-0')
->and((string) $this->blade('<x-menu-group label="Then" gap><x-menu-item label="Cut" /></x-menu-group>'))
->toContain('aria-label="Then"')
->toContain('space-y-0.5')
->and((string) $this->blade('<x-menu-group label="Sort by"><x-menu-item label="Newest" /></x-menu-group>'))
->toContain('py-1 first:pt-0 last:pb-0')
->not->toContain('space-y-0.5');
});
it('adds icon-class to the leading icon, over its own colour but not over disabled', function () {
$leading = fn (string $html): string => preg_match('/<svg[^>]*class="([^"]*)"/', $html, $icon) ? $icon[1] : '';