Draw the menu without Tailwind

<x-menu> renders data-md-menu with data-md-sheet-at-compact, and its
popover data-md-menu-popover with data-md-position and data-md-vibrant;
menu.css caps a long menu at 288px so it scrolls instead of running
off the top layer (ACT-04) and ties the open/close transition to the
trigger's corner (ACT-26), restyles a submenu to open in place inside
a sheet-at-compact sheet, and still carries the dropdown a select's
picker and a searchable choices field draw, so select.css and
choices.css finish their step-36 TODO import. actions.css sheds the
state-transition-fast and popover-transition utilities menu-item and
menu no longer use; state-transition-default and [data-fab-menu]
stay for fab-menu's own rewrite.

data-menu, data-menu-filter, data-menu-sheet, data-menu-empty,
data-sheet-at-compact, data-vibrant and data-active are renamed to
data-md-*, also in menu.js, split-button.css (which renders a
<x-menu> and now imports its stylesheet) and AppBarTest's overflow
menu (plan step 36, actions).

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 16:16:49 +02:00
co-authored by Claude Opus 5
parent c040925cd4
commit 5cdeec26e8
13 changed files with 501 additions and 390 deletions
+10 -10
View File
@@ -134,7 +134,7 @@ const menu = () => ({
}
})
if (this.$el.hasAttribute('data-sheet-at-compact')) {
if (this.$el.hasAttribute('data-md-sheet-at-compact')) {
this.compact = upTo('medium')
// A window resized across 600px while the menu is open would leave it in the
@@ -256,12 +256,12 @@ const menu = () => ({
/** The list the person is looking at: the popover, or the sheet's copy of it. */
list() {
return this.shown === 'sheet' ? this.$refs.sheetHost.querySelector('[data-menu-sheet]') : this.$refs.menu
return this.shown === 'sheet' ? this.$refs.sheetHost.querySelector('[data-md-menu-sheet]') : this.$refs.menu
},
/** The filter field at the top of a list, if the menu has one; a submenu never does. */
field(root = this.list()) {
return root?.querySelector(':scope > [data-menu-filter] input') ?? null
return root?.querySelector(':scope > [data-md-menu-filter] input') ?? null
},
open(focus = 'first') {
@@ -365,7 +365,7 @@ const menu = () => ({
return
}
const list = this.$refs.sheetHost?.querySelector('[data-menu-sheet]')
const list = this.$refs.sheetHost?.querySelector('[data-md-menu-sheet]')
if (list) {
this.clear(list)
@@ -400,7 +400,7 @@ const menu = () => ({
* submenu keeps its `popover` attribute in the sheet too, where it opens in place.
*/
items(root = this.list()) {
return [...root.querySelectorAll(ITEMS)].filter((item) => item.closest('[popover], [data-menu-sheet]') === root)
return [...root.querySelectorAll(ITEMS)].filter((item) => item.closest('[popover], [data-md-menu-sheet]') === root)
},
/** The menu scrolls when it is too long for the window, so the item taken has to be shown. */
@@ -521,7 +521,7 @@ const menu = () => ({
const left = this.visible(root)
root.querySelector('[data-menu-empty]').hidden = left.length > 0
root.querySelector('[data-md-menu-empty]').hidden = left.length > 0
this.mark(left[0] ?? null, root)
},
@@ -540,7 +540,7 @@ const menu = () => ({
for (const each of this.items(root)) {
if (each !== item) {
each.removeAttribute('data-active')
each.removeAttribute('data-md-active')
}
}
@@ -551,7 +551,7 @@ const menu = () => ({
}
item.id ||= `${root.id}-item-${this.items(root).indexOf(item)}`
item.setAttribute('data-active', '')
item.setAttribute('data-md-active', '')
field?.setAttribute('aria-activedescendant', item.id)
if (this.isOpen()) {
@@ -562,7 +562,7 @@ const menu = () => ({
/** The APG combobox keyboard, on the field: the list moves under it and Enter takes a row. */
search(event) {
const left = this.visible()
const current = left.findIndex((item) => item.hasAttribute('data-active'))
const current = left.findIndex((item) => item.hasAttribute('data-md-active'))
const move = (index) => {
event.preventDefault()
@@ -651,7 +651,7 @@ document.addEventListener('alpine:init', () => {
/** In the sheet of a `sheet-at-compact` menu the submenu opens in place, under its item. */
init() {
this.inline = this.$el.closest('[data-menu-sheet]') !== null
this.inline = this.$el.closest('[data-md-menu-sheet]') !== null
base.init.call(this)
},