Show and document a dialog's scroll dividers

Plan step 23 (containment). The containment showcase gets a dialog with a
long body, whose rules follow its scroll, and one with `separator`, whose
rules always show. The <x-modal> entry in SKILL.md says what `separator`
means now and that every dialog divides a scrolling body by itself.

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 11:03:05 +02:00
co-authored by Claude Opus 5
parent cbc0fa76d2
commit 50b78b4c23
2 changed files with 35 additions and 1 deletions
@@ -515,7 +515,7 @@ An M3 dialog on native `<dialog>`. Bind with `wire:model` to a flag or an id; cl
</x-modal> </x-modal>
``` ```
Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (a divider under the headline and above the actions), `persistent` (no Escape or scrim), `fullscreen` (whole screen on a compact window, below `medium`, for forms — M3 allows a full-screen dialog only there), `alert` (`role="alertdialog"` for a dialog that interrupts to say something important — not for forms), `box-class`. The headline and the action row are pinned and only the body between them scrolls, as M3 requires, so do not put `overflow` on `box-class`. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead. Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (draw the dividers under the headline and above the actions always, not only while the body scrolls), `persistent` (no Escape or scrim), `fullscreen` (whole screen on a compact window, below `medium`, for forms — M3 allows a full-screen dialog only there), `alert` (`role="alertdialog"` for a dialog that interrupts to say something important — not for forms), `box-class`. The headline and the action row are pinned and only the body between them scrolls, as M3 requires, so do not put `overflow` on `box-class`. Every dialog divides a scrolling body from them by itself: a 1px outline-variant rule under the header once the body is scrolled away from its top, and one over the actions while more is below (under the close-and-title bar on a phone for `fullscreen`); a body that fits shows neither, and nothing moves when one appears. So do not add an `<x-divider>` at the top or bottom of the body, and do not wrap the body's content in a scroll container of its own — the rules follow the body's scroll, and content a Livewire render adds updates them. Never remove its `wire:ignore.self` behaviour by re-rendering it conditionally with `@if`; toggle the bound property instead.
### `<x-drawer>` ### `<x-drawer>`
@@ -153,6 +153,40 @@
</x-modal> </x-modal>
</div> </div>
BLADE, BLADE,
'A dialog whose body scrolls: dividers only while content is hidden' => <<<'BLADE'
<div x-data="{ open: false }">
<x-button label="Long body" variant="tonal" x-on:click="open = true" />
<x-modal title="Terms of the share link" subtitle="Scroll them: a rule appears under the headline once the text leaves its top, and the one over the buttons goes at its end.">
<div class="space-y-4">
<p>A share link opens the files you chose, and only those. Adding a file to the folder later does not add it to a link you have already sent; make a new link for it.</p>
<p>Anyone who has the link can open it until it expires. Forwarding the link forwards that access, so send it to the people who need it and to nobody else.</p>
<p>A password, when you set one, is asked for every time the link is opened. Send it separately from the link, by another channel, so one intercepted message is not enough.</p>
<p>The link expires at the time you picked. After that it shows a page saying so, and downloads that were still running stop at once.</p>
<p>You can end a link early from the list of shares. Ending it takes effect immediately for everyone, including people who opened it a moment ago.</p>
<p>Downloads are counted per file. The count is shown to you and never to the people you shared with.</p>
<p>Files larger than 2 GB are offered as a single archive only when the whole share fits under the archive limit; otherwise each file downloads on its own.</p>
<p>Previews are generated for images, PDFs and plain text. Other types show their name, size and an icon.</p>
<p>Deleting a file you shared removes it from every link that holds it. People who open such a link see the files that are left.</p>
<p>These terms apply to every link you create from now on. Links you created before keep the terms they were made under.</p>
</div>
<x-slot:actions>
<x-button label="Decline" x-on:click="close()" />
<x-button label="Accept" x-on:click="close()" />
</x-slot:actions>
</x-modal>
</div>
<div x-data="{ open: false }">
<x-button label="Always divided (separator)" variant="tonal" x-on:click="open = true" />
<x-modal title="Who can open the link" separator>
<x-radio label="Audience" name="showcase-dialog-audience" value="password" :options="[['id' => 'anyone', 'name' => 'Anyone with the link'], ['id' => 'password', 'name' => 'Anyone with the password'], ['id' => 'team', 'name' => 'My team only']]" />
<x-slot:actions>
<x-button label="Cancel" x-on:click="close()" />
<x-button label="Save" x-on:click="close()" />
</x-slot:actions>
</x-modal>
</div>
BLADE,
'A standard side sheet: co-planar from expanded, modal below' => <<<'BLADE' 'A standard side sheet: co-planar from expanded, modal below' => <<<'BLADE'
<div x-data="{ open: true }" class="w-full"> <div x-data="{ open: true }" class="w-full">
<div class="expanded:flex expanded:items-start expanded:gap-6"> <div class="expanded:flex expanded:items-start expanded:gap-6">