Меню

Меню — это <details> с оформленной панелью. Это сознательный выбор в пользу него, а не popover API: popover живёт в верхнем слое, и без anchor positioning его нельзя расположить относительно своего триггера, а этой возможности пока есть не везде. <details> располагается правильно уже сегодня, и загружать для него ничего не надо.

Триггер — это тот самый <summary>, оформленный под кнопку, поэтому подпись и пропсы кнопки вы передаёте в menu(), а не подсовываете готовый button(). Summary уже интерактивен, и кнопка внутри него вкладывает два элемента управления туда, где действие всего одно: невалидная разметка и две остановки табуляции на одну вещь.

Закрытие по клику снаружи и по Escape приходит из обработчика ontoggle, который подгружает их при первом открытии меню — и только тогда. Если этот модуль так и не придёт, меню всё равно открывается и закрывается собственным summary.

Простое меню

menu({ trigger: 'Действия' },
  menuItem({ href: '#edit' }, 'Изменить'),
  menuItem({ href: '#duplicate' }, 'Дублировать'),
  menuSeparator(),
  menuItem({ href: '#delete' }, 'Удалить'),
)

Выравнивание

Меню раскрывается от начального края триггера. align: 'end' переворачивает это — как раз то, что нужно меню у правого края панели.

По началу
По концу
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
  menu({ trigger: 'По началу', variant: 'soft' },
    menuItem({ href: '#a' }, 'Первый'),
    menuItem({ href: '#b' }, 'Второй'),
  ),
  menu({ trigger: 'По концу', variant: 'soft', align: 'end' },
    menuItem({ href: '#c' }, 'Первый'),
    menuItem({ href: '#d' }, 'Второй'),
  ),
)

Триггеры-иконки

Иконке без текста в trigger нужен label: он становится доступным именем, которого иконка дать не может.

stack({ direction: 'row', gap: 'md' },
  menu({
    align: 'end',
    label: 'Ещё действия',
    variant: 'ghost',
    icon: icon('more-horizontal'),
  },
    menuItem({ href: '#rename' }, 'Переименовать'),
    menuItem({ href: '#move' }, 'Переместить'),
    menuSeparator(),
    menuItem({ href: '#archive' }, 'В архив'),
  ),
)

Пункты с иконками

menu({ trigger: 'Файл' },
  menuItem({
    href: '#new',
    icon: icon('plus'),
  }, 'Новая страница'),
  menuItem({
    href: '#open',
    icon: icon('folder'),
  }, 'Открыть…'),
  menuSeparator(),
  menuItem({
    href: '#build',
    icon: icon('zap'),
  }, 'Собрать сайт'),
)

Кнопки вместо ссылок

Пункт без href рисует <button> — для действия, которое происходит на странице, а не для перехода.

Экспорт
menu({ trigger: 'Экспорт', variant: 'soft', color: 'primary' },
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Экспортировано в JSON.',{color:'success'}))" }, 'В JSON'),
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Экспортировано в CSV.',{color:'success'}))" }, 'В CSV'),
)

В панели приложения

appBar({ brand: 'sitelo' },
  appBarSpacer(),
  appBarActions(
    themeToggle(),
    menu({
      align: 'end',
      label: 'Ещё',
      variant: 'ghost',
      icon: icon('more-horizontal'),
    },
      menuItem({ href: '/ru/docs' }, 'Документация'),
      menuItem({ href: '/ru/examples' }, 'Примеры'),
      menuSeparator(),
      menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
    ),
  ),
)

Доступность

Панель имеет role="menu", её пункты — role="menuitem", а summary несёт aria-haspopup. <details> не является нативным меню-виджетом, так что это разумное приближение, а не идеал: для обычного списка ссылок nav внутри details ничуть не хуже и обещает меньше.

Пропсы

menu() — пропсы триггера те же, что у кнопки:

ПропТипПо умолчаниюОписание
triggerChild—Видимая подпись. Передавайте текст, а не готовый button().
iconChild—Разметка перед подписью или сама по себе — для триггера из одной иконки.
labelstring—Доступное имя. Обязателен, когда есть иконка и нет текста trigger.
variant'solid' | 'soft' | 'outline' | 'ghost' | 'link''outline'Оформление триггера.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''neutral'Из какой палитры берёт цвета триггер.
size'sm' | 'md' | 'lg''md'Размер триггера.
align'start' | 'end''start'По какому краю триггера выравнивается панель.
triggerClassstring—Дополнительные классы для триггера, а не для обёртки details.

menuItem():

ПропТипПо умолчаниюОписание
hrefstring—Рисует ссылку; без него — кнопку.
iconChild—Разметка перед подписью.
asstring'button'Какой элемент рендерить, когда href нет.

menuSeparator() пропсов не принимает — это тонкая линия между группами пунктов.