Files
Andreas Reinhold / reiniandClaude Sonnet 5 3c235ecc1e Rewrite the modal (dialog) without Tailwind
Plan step 36 (containment group): <x-modal>'s class lists move into
resources/css/components/modal.css, keyed on data-md-modal (with
data-md-fullscreen) and data-md-modal-box/-bar/-head/-title/-subtitle/
-body/-content/-actions. Sizes stay px (560/280/48px matching
DialogTokens.kt/FullScreenDialogTokens.kt and the 56px full-screen
header, C-17), spacing the measurement tokens, breakpoints a literal
600px range query.

The phone-only header bar and the pinned head/actions keep their
existing responsive behaviour (a subtitle or an icon keeps the head on
screen below 600px; only the bar's own divider-eligibility needs a
computed flag, since it alone can sit beside a head that is also on
screen) — reproduced with :has() against data-md-icon/
data-md-modal-subtitle wherever that is self-contained, and with an
explicit data-md-modal-divider attribute where it is not. The
scroll-driven dividers rename data-dialog-head/-actions ->
data-md-modal-head/-actions, data-overflow-top/-bottom ->
data-md-overflow-top/-bottom (resources/js/dialog.js); the
dialog-dividers Alpine directive keeps its name, since it isn't a
data-md-* hook.

resources/css/components/dialog.css (Tailwind-era) is replaced by
modal.css, imported from the Containment block of components.css;
its import leaves resources/css/tailwind.css. No other component
renders <x-modal> or dialog.css's rules (checked: datepicker,
timepicker and search draw their own dialog-shaped surfaces
independently).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 21:01:05 +02:00

51 lines
2.2 KiB
JavaScript

/**
* `x-dialog-dividers`, on `<x-modal>`'s body — the one part of a dialog that scrolls: marks the
* `<dialog>` with `data-md-overflow-top` while the body is scrolled away from its top, and
* `data-md-overflow-bottom` while more of it is below. resources/css/components/modal.css draws
* those as the dividers under the pinned header and over the pinned actions; a body that fits
* marks neither.
*
* The marks go on the `<dialog>` rather than the body because the dialog is `wire:ignore.self`: a
* Livewire morph hands the body back the server's attributes, which would wipe a mark until the
* next scroll, but it never touches the dialog's own.
*
* Measured on scroll, and whenever the body or what is in it changes size: the body when the window
* or the dialog does (opening, too — a closed dialog has no size, and the observer reports the
* one it opens to), and the element the component wraps around the slot when a morph, an image or
* a disclosure makes the content taller or shorter while the body stays at its cap.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.directive('dialog-dividers', (el, _, { cleanup }) => {
const dialog = el.closest('dialog')
if (!dialog) {
return
}
// A pixel of slack: at a fractional device pixel ratio the end of a scroll can stop a
// fraction short of `scrollHeight`, which is rounded.
const measure = () => {
dialog.toggleAttribute('data-md-overflow-top', el.scrollTop >= 1)
dialog.toggleAttribute('data-md-overflow-bottom', el.scrollHeight - el.clientHeight - el.scrollTop >= 1)
}
const resizes = new ResizeObserver(measure)
resizes.observe(el)
const content = el.querySelector(':scope > [data-md-modal-content]')
if (content) {
resizes.observe(content)
}
el.addEventListener('scroll', measure, { passive: true })
cleanup(() => {
resizes.disconnect()
el.removeEventListener('scroll', measure)
dialog.removeAttribute('data-md-overflow-top')
dialog.removeAttribute('data-md-overflow-bottom')
})
})
})