feat(uix): Menubar.Panel — una entrada puede abrir un panel (role=dialog)

Nueva parte Panel en el morfo (archetype content, role dialog, opcional) y
aria-haspopup pasa a propRef: 'menu' cuando la entrada despliega un Content,
'dialog' cuando despliega un Panel. Wrapper eidos Menubar.Panel + receta, y
la demo monta el mega-menú/form-panel anclado a la barra o al trigger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 2 months ago
parent 7b52ae8b6a
commit e09170f9da

@ -40,7 +40,8 @@ las partes internas por-menú ya no son propias — son re-exports de
- Partes propias: root (`role="menubar"`, horizontal), `Menu` (scope por
menú), `Trigger` (`role="menuitem"`, `data-state`/`data-highlighted`/
`data-menubar-value`), `Content` (panel).
`data-menubar-value`), `Content` (panel de menú) y `Panel` (superficie
`role="dialog"` para controles de formulario — alternativa a Content).
- Partes re-exportadas de DropdownMenu: `Item`, `Group`, `GroupHeading`,
`Separator`, `CheckboxItem`, `CheckboxGroup`, `RadioGroup`, `RadioItem`,
`Sub`, `SubTrigger`, `SubContent`, `Arrow`.
@ -62,6 +63,13 @@ las partes internas por-menú ya no son propias — son re-exports de
| Checkbox/Radio items + submenús | ✓ | ✓ | ✓ | ✓ | — |
| `loop` + RTL | ✓ | ✓ | ✓ | ✓ | — |
| Evento perceptivo en el select | ✓ (`commit-select`) | No | No | No | — |
| **Superficie panel (role=dialog) por entrada** | ✓ (`Menubar.Panel`) | No | No | No | No |
`Menubar.Panel` es una EXTENSIÓN sobre las referencias headless (ninguna lo
modela): los precedentes son las barras reales que mezclan menús y paneles —
los menu extras de macOS (Wi-Fi / Control Center: trigger en barra, panel
no-menu, Esc cierra, las flechas operan el panel), las galerías del ribbon de
Office y los paneles de barra de Canva.
Referencias: [Radix Menubar](https://www.radix-ui.com/primitives/docs/components/menubar) ·
[Bits UI Menubar](https://bits-ui.com/docs/components/menubar) ·
@ -82,6 +90,53 @@ Referencias: [Radix Menubar](https://www.radix-ui.com/primitives/docs/components
automáticamente y un fix visual del dropdown alcanza al menubar sin drift.
- **`data-menubar-value` en el trigger** enlaza trigger↔menú para el estado
controlado (`value`/`onValueChange`).
- **`Menubar.Panel` — superficie de formulario por entrada** (2026-07-16,
encargo del editor palabras `panelMode='menubar'` v2, componente genérico).
Motivo: el Content de menú intercepta ↑↓/Home/End + typeahead (navegación
de items) y eso rompe cualquier control de formulario compuesto dentro. El
Panel es un part propio del morfo (`data-menubar-panel`, `role="dialog"`,
`aria-labelledby`→trigger; el trigger pasa a `aria-haspopup="dialog"`) que
NO añade teclado propio: las teclas dentro pertenecen a sus controles (las
flechas ←→ NO cruzan menús desde dentro — mueven carets/sliders; Esc
devuelve a la barra). La coordinación de barra (roving, hover-slide,
keyboard-slide, `commit-select`, un-abierto-a-la-vez) queda intacta porque
vive en el Trigger/Provider. Presencia = tipo (N-7): componer `<Panel>` en
vez de `<Content>` declara la superficie. Cableado: reusa el shell flotante
del DropdownMenu anidado (anchor del trigger, presence, canal
`contentId`/`setContentRef`), con FocusScope/Dismissal propios y SIN
TextSelection (los inputs necesitan selección) ni ScrollLock. Foco:
apertura por teclado → primer control; por puntero → la superficie (hover
jamás roba el foco a un input). Elevación vía `data-depth="overlay"` +
tokens públicos `--menubar-panel-*` (z · min-width · max-height · padding ·
radius).
- **Mega-panel = `customAnchor={barra}` a secas** (2026-07-17). El ancho es
automático: soma pre-mide el anchor (`--menubar-panel-anchor-width` en el
elemento) y la recipe dimensiona la superficie `data-anchored` con
`contain: inline-size` — el positioner flotante mide el WRAPPER
(`min-width: max-content`) y la intrínseca de un grid (suma de tracks) lo
inflaba, volteando el placement; el containment fija la contribución al
ancho usado (en modo anclado el ancho viene del anchor, nunca del
contenido). Regla de consumo: el `style` del Panel viste al POSITIONER
(mismo plumbing que todo Content del catálogo) — el layout del contenido
(grid/flex) va en un elemento INTERNO.
- **La apertura sigue al roving por teclado** (2026-07-16): con un menú
abierto, ArrowLeft/Right/Home/End sobre los triggers deslizan la apertura al
destino — el espejo de teclado del hover-follow (la doctrina del
`commit-select` ya lo prometía). El slide vive en la navegación explícita
del provider, no en `onfocus`: un listener de focus no distingue el roving
del focus-return del FocusScope al cerrarse el menú que se desliza. Ese
focus-return se suprime en `<Menubar.Content>` (patrón Radix: `preventDefault`
en `onCloseAutoFocus` cuando otro menú del menubar queda abierto); el cierre
simple (Escape / outside / select) conserva el retorno de foco normal.
- **Toda apertura por teclado aterriza en el PRIMER ITEM activo** (2026-07-16,
decisión de usuario tras confusión real: un panel abierto sin opción activa
no dice cómo navegar). Cubre ArrowDown/Enter/Space en el trigger, el slide
←→/Home/End entre triggers y el cross-menu desde dentro de un panel — la
señal viaja como `openReason: 'keyboard'` del menubar al DropdownMenu
interno, cuyo autofocus enfoca `getItems()[0]` en vez del panel (APG "focus
first item"; Word hace lo mismo en el slide). Las aperturas por PUNTERO
(click, hover-follow) mantienen el foco en el panel: ahí resalta el hover y
un item pre-activado sería una fila fantasma bajo el cursor.
## Gaps
@ -90,6 +145,8 @@ Referencias: [Radix Menubar](https://www.radix-ui.com/primitives/docs/components
| Tests del wrapper eidos (el soma tiene suite ✓) | **diferir** | Pasada SYS-2 conjunta. |
| API extendida sobre las referencias (p. ej. trigger-on-hover configurable) | **diferir** | Paridad Radix/Bits ya alcanzada; sin caso real. |
| Intent del `commit-select` fijo en `affirm` | **descartar** | Seleccionar una entrada de menú no carga evaluación variable; el intent por-evento es intrínseco (doctrina per-event intent). |
| Typeahead a nivel de barra (menubar cerrado salta al trigger por inicial) | **diferir** | APG lo lista; ni Radix ni Bits lo implementan (el typeahead vive dentro del menú abierto, ✓ aquí). Paridad mantenida; sin caso real. |
| `ArrowUp` en el trigger abre enfocando el último item | **diferir** | Radix lo soporta; exige que el DropdownMenu compuesto acepte "open with last item highlighted". Se aborda en el dropdown, no con un hack local. |
## Referencias

@ -40,6 +40,7 @@ import MenubarComponent from './menubar.svelte';
import Menu from './menubar-menu.svelte';
import Trigger from './menubar-trigger.svelte';
import Content from './menubar-content.svelte';
import Panel from './menubar-panel.svelte';
// Per-menu inner parts: re-use the eidos DropdownMenu wrappers so the
// menu-indicator shared partial (commit a2709476) applies and we don't
@ -54,6 +55,7 @@ type MenubarNamespace = typeof MenubarComponent & {
Trigger: typeof Trigger;
Portal: typeof Portal;
Content: typeof Content;
Panel: typeof Panel;
Item: typeof _DropdownMenu.Item;
Group: typeof _DropdownMenu.Group;
GroupHeading: typeof _DropdownMenu.GroupHeading;
@ -73,6 +75,7 @@ Menubar.Menu = Menu;
Menubar.Trigger = Trigger;
Menubar.Portal = Portal;
Menubar.Content = Content;
Menubar.Panel = Panel;
Menubar.Item = _DropdownMenu.Item;
Menubar.Group = _DropdownMenu.Group;
Menubar.GroupHeading = _DropdownMenu.GroupHeading;
@ -94,6 +97,7 @@ export type {
MenubarMenuProps as MenuProps,
MenubarTriggerProps as TriggerProps,
MenubarContentProps as ContentProps,
MenubarPanelProps as PanelProps,
MenubarArrowProps as ArrowProps,
MenubarItemProps as ItemProps,
MenubarGroupProps as GroupProps,

@ -0,0 +1,30 @@
<script lang="ts">
/**
* Eidos `Menubar.Panel` — the role=dialog FORM surface a bar entry can
* drop instead of a role=menu Content. Stamps the overlay elevation
* plane + `data-size` from the Menubar root's size context (same wiring
* as `Menubar.Content`); the recipe paints the surface via the
* `data-menubar-panel` marker the soma runtime emits.
*/
import * as Menubar from '$soma/components/menubar';
import type { MenubarPanelProps } from './types';
import { getMenubarSizeContext } from './menubar-context';
let { children: bodyContent, ...rest }: MenubarPanelProps = $props();
const ctx = getMenubarSizeContext();
const resolvedSize = $derived(ctx?.getSize() ?? 'md');
const passthrough = $derived(rest as Record<string, unknown>);
</script>
<Menubar.Panel
{...passthrough}
data-depth="overlay"
data-floating-gap={passthrough.sideOffset === undefined ? 'menu' : undefined}
data-size={resolvedSize}
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}
</Menubar.Panel>

@ -135,14 +135,86 @@
* BAR — "the space is the menu, not the option". The shared positioner reads
* `--floating-gap` off the content; these (0,2,0) beat the foundation
* `[data-floating-gap='menu']` (0,1,0). */
[data-menubar-content][data-size='md'] {
[data-menubar-content][data-size='md'],
[data-menubar-panel][data-size='md'] {
--floating-gap: calc(var(--floating-gap-menu) + var(--space-1) + 1px);
}
[data-menubar-content][data-size='sm'] {
[data-menubar-content][data-size='sm'],
[data-menubar-panel][data-size='sm'] {
--floating-gap: calc(var(--floating-gap-menu) + var(--space-1) * 0.5 + 1px);
}
[data-menubar-content][data-size='lg'] {
[data-menubar-content][data-size='lg'],
[data-menubar-panel][data-size='lg'] {
--floating-gap: calc(var(--floating-gap-menu) + var(--space-2) + 1px);
}
/* ── Panel — per-entry role=dialog form surface ──────────────────────────
* The alternative to Content for a bar entry: a free surface for form
* controls (Fields, selects, sliders). It does NOT carry
* `data-dropdown-menu-content` (it is the menubar's own part), so the
* dropdown recipe never paints it: elevation (surface · border · shadow)
* comes from the `overlay` plane via `data-depth` (stamped by the eidos
* wrapper, same as Content); everything else is defined here. Portal
* typography rule: font-family + line-height + color anchored on the
* portaled root. */
[data-menubar-panel] {
/* Portaled root: bring box-sizing along so a consumer-set
* `inline-size: var(--floating-anchor-width)` (mega-panel idiom) matches
* the anchor EDGE to edge, padding and border included. */
box-sizing: border-box;
z-index: var(--menubar-panel-z);
min-inline-size: var(--menubar-panel-min-width);
/* Cap the INTRINSIC size: the floating wrapper measures the panel's
* max-content (a grid's track sum can dwarf its used inline-size), and an
* inflated measurement makes the positioner flip/clamp the placement. */
max-inline-size: 100vw;
max-block-size: var(--menubar-panel-max-height);
overflow-y: auto;
overscroll-behavior: contain;
padding: var(--menubar-panel-padding);
border-radius: var(--menubar-panel-radius);
font-family: var(--style-label-font-family);
font-size: var(--font-size-md);
line-height: var(--leading-ui);
color: var(--color-content-primary);
}
[data-menubar-panel][data-size='sm'] {
font-size: var(--font-size-sm);
}
[data-menubar-panel][data-size='lg'] {
font-size: var(--font-size-lg);
}
/* Anchored (mega-panel): the panel matches its custom anchor's width. Soma
* pre-measures the anchor and publishes `--menubar-panel-anchor-width` on
* the element; until that lands (one frame on the very first open) the cap
* falls back to the panel's min-width so the positioner's FIRST measurement
* is already small — align start then never flips, and the panel simply
* widens in place when the var arrives. */
[data-menubar-panel][data-anchored] {
/* The per-size `--floating-gap` rules above compensate the trigger's
* inset INSIDE the bar (bar padding + 1px centering) so a trigger-anchored
* surface reads the canonical menu gap FROM THE BAR. Anchored to the bar
* itself, the positioner already measures from the bar edge — the canon
* applies raw, no compensation. */
--floating-gap: var(--floating-gap-menu);
inline-size: var(--menubar-panel-anchor-width, var(--menubar-panel-min-width));
max-inline-size: var(--menubar-panel-anchor-width, var(--menubar-panel-min-width));
/* The positioner measures the floating WRAPPER (min-width: max-content),
* and a grid child contributes its TRACK-SUM intrinsic size there — width
* caps on the grid itself don't stop it, so an inflated measurement flips
* the placement. Inline-size containment makes this panel's contribution
* its used width: in anchored mode the width comes from the anchor, never
* from the content. */
contain: inline-size;
}
/* Focus lives on the panel's controls; the surface itself (focused on
* pointer opens) shows no ring. Same treatment as the menu panels. */
[data-menubar-panel]:focus-visible {
outline: none;
}

@ -4,6 +4,7 @@ import type {
MenuProps,
TriggerProps,
ContentProps,
PanelProps,
ArrowProps,
ItemProps,
GroupProps,
@ -40,6 +41,7 @@ export type MenubarProps = ProviderProps & {
export type MenubarMenuProps = MenuProps;
export type MenubarTriggerProps = TriggerProps;
export type MenubarContentProps = ContentProps;
export type MenubarPanelProps = PanelProps;
// Re-exported from DropdownMenu — Menubar's per-menu inner parts
// (Item, Group, Separator, Checkbox/RadioItem, Sub-tree) are literally

@ -54,7 +54,10 @@ export const menubarMorfo = {
],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('menu') },
// 'menu' when the entry drops a Content (role=menu), 'dialog' when
// it drops a Panel (role=dialog) — resolved from the surface the
// consumer composed inside this entry's Menu scope.
{ attr: 'aria-haspopup', value: v.propRef('haspopup') },
{ attr: 'aria-expanded', value: v.stateRef('open') }
],
keyboard: [
@ -66,6 +69,27 @@ export const menubarMorfo = {
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Panel',
kebab: 'panel',
archetype: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: true,
states: ['open', 'closed'],
data: [
// data-side / data-align are floating-layer attrs (same as the
// dropdown's Content — the layer emits them, the morfo doesn't
// model them).
{ attr: 'data-state', values: ['open', 'closed'] },
// Present when the panel is anchored to a custom element (the
// mega-panel idiom) — the recipe caps the intrinsic size from it.
{ attr: 'data-anchored', severity: 'optional' }
],
aria: [{ attr: 'aria-labelledby', value: v.partRef('trigger') }],
keyboard: [{ key: 'Escape', action: 'close' }]
}
]
} as const satisfies Morfo;

@ -3,6 +3,9 @@
Menubar,
type MenubarSize
} from '$uix/eidos/components/menubar';
import { Field } from '$uix/eidos/components/field';
import { Slider } from '$uix/eidos/components/slider';
import { Kbd } from '$uix/eidos/components/kbd';
import { compileMorfo } from '$uix/morfo';
import { menubarMorfo } from '@/uix/morfo/components/menubar';
import { getActiveUix } from '$active-uix';
@ -23,6 +26,38 @@
let theme = $state<'light' | 'dark' | 'system'>('system');
let viewLayers = $state<string[]>(['ruler', 'grid']);
// Bound state from the Format PANEL surface (form controls, not menu items)
let docTitle = $state('Untitled document');
let docWidth = $state([60]);
// Panel anchor: the whole bar (mega-menu, default here so the showcase is
// visible on open) or the entry's trigger (compact form panel; toggle off).
let anchorBar = $state(true);
let barEl = $state<HTMLElement | null>(null);
// Mega-menu link columns: icon + label + sublabel + shortcut. Each row
// traces through `last` (the bar's commit-select fires on the trigger).
const megaColumns = [
{
heading: 'Text',
items: [
{ label: 'Bold', sub: 'Heavier weight', keys: ['⌘', 'B'], icon: 'M7 5h6a3.5 3.5 0 0 1 0 7H7z M7 12h7a3.5 3.5 0 0 1 0 7H7z' },
{ label: 'Italic', sub: 'Slanted emphasis', keys: ['⌘', 'I'], icon: 'M10 5h6 M8 19h6 M14 5l-4 14' },
{ label: 'Underline', sub: 'Underlined run', keys: ['⌘', 'U'], icon: 'M6 4v6a6 6 0 0 0 12 0V4 M5 20h14' },
{ label: 'Inline code', sub: 'Monospace span', keys: ['⌘', 'E'], icon: 'M8 6l-6 6 6 6 M16 6l6 6-6 6' }
]
},
{
heading: 'Paragraph',
items: [
{ label: 'Align left', sub: 'Ragged right', keys: ['⌘', '⇧', 'L'], icon: 'M4 6h16 M4 12h10 M4 18h13' },
{ label: 'Align center', sub: 'Centered block', keys: ['⌘', '⇧', 'E'], icon: 'M4 6h16 M7 12h10 M6 18h12' },
{ label: 'Bulleted list', sub: 'Unordered items', keys: ['⌘', '⇧', '8'], icon: 'M9 6h11 M9 12h11 M9 18h11 M4 6h.01 M4 12h.01 M4 18h.01' },
{ label: 'Blockquote', sub: 'Pull a quote', keys: ['⌘', '⇧', '9'], icon: 'M6 7h5v6a4 4 0 0 1-4 4 M13 7h5v6a4 4 0 0 1-4 4' }
]
}
];
let lastSelected = $state<string | null>(null);
function pickItem(label: string) {
return () => {
@ -34,6 +69,10 @@
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
$effect(() => {
barEl = stageRef?.querySelector<HTMLElement>('[data-menubar]') ?? null;
});
$effect(() => {
const el = stageRef;
if (!el) return;
@ -114,6 +153,25 @@
' <Menubar.Item onSelect={…}>Quit</Menubar.Item>',
' </Menubar.Content>',
' </Menubar.Menu>',
'',
' <!-- role=dialog form surface: no typeahead, no arrow theft -->',
' <Menubar.Menu value="format">',
' <Menubar.Trigger>Format</Menubar.Trigger>',
anchorBar ? ' <!-- mega-panel: anchor the BAR; width matches it automatically -->' : false,
anchorBar ? ' <Menubar.Panel customAnchor={barEl}>' : ' <Menubar.Panel>',
' <!-- content layout on an INNER element (the Panel style prop',
' dresses the floating positioner, like every Content) -->',
anchorBar ? ' <div class="mm"><!-- flex: min-width:0 columns -->' : ' <div style="display: flex; flex-direction: column; …">',
anchorBar && ' <button class="mm-featured">… illustration + CTA …</button>',
anchorBar && ' <div class="mm-col"><!-- Text -->',
anchorBar && ' <button class="mm-link">icon · label · sub · <Kbd>⌘</Kbd><Kbd>B</Kbd></button> …',
anchorBar && ' </div>',
anchorBar && ' <div class="mm-col"><!-- Paragraph --> … </div>',
!anchorBar && ' <Field name="title">…</Field>',
!anchorBar && ' <Slider bind:value={width} … />',
' </div>',
' </Menubar.Panel>',
' </Menubar.Menu>',
'</Menubar>'
]
.filter(Boolean)
@ -122,6 +180,25 @@
</script>
<div data-uix-canvas-inner>
{#snippet panelSettings()}
<Field name="doc-title">
<Field.Label>Title</Field.Label>
<Field.Control>
<Field.Input type="text" bind:value={docTitle} />
</Field.Control>
<Field.HelperText>Typeahead-proof: typing stays in the input.</Field.HelperText>
</Field>
<Field name="doc-width">
<Field.Label>Content width — {docWidth[0]}ch</Field.Label>
<Field.Control>
<Slider bind:value={docWidth} min={40} max={120} aria-label="Content width">
<Slider.Range />
<Slider.Thumb />
</Slider>
</Field.Control>
<Field.HelperText>Arrow keys drive the slider, not the bar.</Field.HelperText>
</Field>
{/snippet}
<header>
<div data-uix-eyebrow>Navigation · Menubar</div>
<h1 data-uix-page-title>Menubar</h1>
@ -133,7 +210,11 @@
Item / Sub / Checkbox / Radio chrome is shared with
<code>DropdownMenu</code> — same parts, same recipe, same
menu-indicator (checkbox/radio glyphs match the standalone
<code>Checkbox</code> / <code>RadioGroup</code>).
<code>Checkbox</code> / <code>RadioGroup</code>). An entry can drop
a <code>Menubar.Panel</code> instead of a Content: a
<code>role="dialog"</code> form surface (see “Format”) with NO
typeahead and NO arrow interception — keys inside it belong to its
controls, while the bar keeps roving, hover-slide and Escape.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
@ -159,7 +240,13 @@
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="min-block-size: 14rem; display: flex; flex-direction: column; align-items: flex-start; gap: var(--uix-space-3); padding: var(--uix-space-4);">
<Menubar bind:value={activeMenu} {size} {dir} {loop}>
<Menubar
bind:value={activeMenu}
{size}
{dir}
{loop}
style={anchorBar ? 'inline-size: 100%;' : undefined}
>
<Menubar.Menu value="file">
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Portal>
@ -231,6 +318,74 @@
</Menubar.Portal>
</Menubar.Menu>
<Menubar.Menu value="format">
<Menubar.Trigger>Format</Menubar.Trigger>
<!-- No Portal: the Panel is an IN-PLACE floating div (position: fixed
via the shared floating engine). It stays inside the app's DOM +
token context — so `--uix-*`/framework vars resolve normally, with
no cross-portal loss. Wrap in `<Menubar.Portal>` only when the bar
lives inside a clipping/transformed ancestor that would trap it. -->
<Menubar.Panel aria-label="Format panel" customAnchor={anchorBar ? barEl : null}>
<!-- The Panel's `style` prop dresses the floating positioner (same
plumbing as every Content in the catalog) — content layout
lives on an INNER element. -->
{#if anchorBar}
<!-- Typical mega-menu anatomy: a featured tile (illustration +
gradient + CTA) beside link columns (icon · label · sublabel ·
shortcut). Flex children carry min-width: 0 so nothing
overflows the bar-wide surface. -->
<div class="mm">
<button class="mm-featured" onclick={pickItem('Browse styles')}>
<span class="mm-featured-art" aria-hidden="true">
<svg viewBox="0 0 200 108" width="100%">
<rect x="24" y="12" width="152" height="84" rx="9" fill="#ffffff" opacity="0.97" />
<rect x="40" y="28" width="76" height="11" rx="3.5" fill="#6366f1" />
<rect x="40" y="48" width="120" height="6" rx="3" fill="#c9c9d4" />
<rect x="40" y="60" width="120" height="6" rx="3" fill="#c9c9d4" />
<rect x="40" y="72" width="72" height="6" rx="3" fill="#dcdce3" />
<circle cx="150" cy="76" r="13" fill="#ec4899" opacity="0.92" />
</svg>
</span>
<span class="mm-eyebrow">Featured</span>
<span class="mm-featured-title">Style presets</span>
<span class="mm-featured-text">
Apply a coordinated type &amp; spacing scale in one click.
</span>
<span class="mm-featured-cta">Browse styles →</span>
</button>
{#each megaColumns as col (col.heading)}
<div class="mm-col">
<span class="mm-col-head">{col.heading}</span>
{#each col.items as it (it.label)}
<button class="mm-link" onclick={pickItem(it.label)}>
<span class="mm-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18"><path d={it.icon} /></svg>
</span>
<span class="mm-link-body">
<span class="mm-link-title">{it.label}</span>
<span class="mm-link-sub">{it.sub}</span>
</span>
<span class="mm-keys">
{#each it.keys as k (k)}
<Kbd size="sm">{k}</Kbd>
{/each}
</span>
</button>
{/each}
</div>
{/each}
</div>
{:else}
<div
style="display: flex; flex-direction: column; gap: var(--space-3); inline-size: 18rem; padding-inline: 14px;"
>
{@render panelSettings()}
</div>
{/if}
</Menubar.Panel>
</Menubar.Menu>
<Menubar.Menu value="help">
<Menubar.Trigger>Help</Menubar.Trigger>
<Menubar.Portal>
@ -263,7 +418,7 @@
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>17</span>
API <span data-uix-tab-count>23</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
@ -308,6 +463,13 @@
<span data-uix-switch-label>{loop ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>anchorBar <span data-uix-control-hint>Panel · customAnchor = the bar (mega-panel)</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={anchorBar} />
<span data-uix-switch-label>{anchorBar ? 'bar' : 'trigger'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
@ -353,6 +515,14 @@
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>docTitle <span data-uix-control-hint>Panel · Field.Input</span></span>
<input type="text" value={docTitle} readonly />
</label>
<label data-uix-control>
<span data-uix-control-label>docWidth <span data-uix-control-hint>Panel · Slider</span></span>
<input type="text" value={`${docWidth[0]}ch`} readonly />
</label>
</div>
<div data-uix-code>
@ -379,7 +549,7 @@
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Three menubar-specific parts (Provider, Menu, Trigger, Content)
Menubar-specific parts (Provider, Menu, Trigger, Content, Panel)
plus DropdownMenu's full inner surface for Item / Sub /
Checkbox / Radio. The inner parts share recipes with
<a href="/uix/components/dropdown-menu">DropdownMenu</a> — the
@ -434,6 +604,21 @@
</table>
</div>
<div data-uix-subsection-head>Panel</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">(surface)</td><td class="type">role="dialog"</td><td class="default empty">—</td><td>Form surface alternative to Content: NO typeahead, NO arrow interception — keys belong to its controls. Trigger flips to <code>aria-haspopup="dialog"</code>. Escape closes + returns focus to the trigger.</td></tr>
<tr><td class="name">side · sideOffset · align · alignOffset · avoidCollisions · …</td><td class="type">FloatingLayerProps</td><td class="default empty">—</td><td>Same floating surface as Content (anchored to the bar trigger).</td></tr>
<tr><td class="name">customAnchor</td><td class="type">HTMLElement | null</td><td class="default">null</td><td>Anchor override. Pass the BAR element (mega-panel) and the panel spans it edge-to-edge automatically (pre-measured width + intrinsic containment via <code>data-anchored</code>). Content layout goes on an INNER element — the Panel's <code>style</code> prop dresses the floating positioner.</td></tr>
<tr><td class="name">forceMount</td><td class="type">boolean</td><td class="default">false</td><td>Keep mounted while closed (exit animations).</td></tr>
<tr><td class="name">onOpenAutoFocus / onCloseAutoFocus / onEscapeKeydown / onInteractOutside</td><td class="type">(e) =&gt; void</td><td class="default empty">—</td><td>Cancelable hooks, same shape as Popover's.</td></tr>
<tr><td class="name">data-size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">auto</td><td class="default empty">from Menubar root</td><td>Stamped via context, like Content.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inner parts (shared with DropdownMenu)</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
<code>Item</code>, <code>Group</code>, <code>GroupHeading</code>,
@ -645,3 +830,159 @@
</section>
{/if}
</div>
<style>
/* Mega-menu content for the anchored Format panel. The panel is PORTALED
outside `[data-uix-docs]`, so the docs-shell `--uix-*` tokens do NOT
resolve here (cross-portal vars don't inherit). It speaks the framework's
GLOBAL tokens (`--color-*`, `--space-*`, …, emitted at :root) instead —
which resolve everywhere, portal included, and follow the active theme. */
.mm {
display: flex;
gap: var(--space-4);
align-items: stretch;
}
/* Featured tile — illustration + gradient + CTA (Stripe/Linear idiom). */
.mm-featured {
flex: 0 0 15rem;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-4);
border: 0;
border-radius: var(--radius-md);
background:
radial-gradient(120% 80% at 15% 0%, rgba(255, 255, 255, 0.22), transparent 55%),
linear-gradient(135deg, #6366f1, #8b5cf6 55%, #ec4899);
color: #ffffff;
text-align: start;
font: inherit;
cursor: pointer;
transition:
transform var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default);
}
.mm-featured:hover {
transform: translateY(-1px);
box-shadow: 0 10px 26px rgba(99, 102, 241, 0.3);
}
.mm-featured:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
.mm-featured-art {
display: block;
margin-block-end: var(--space-3);
border-radius: var(--radius-sm);
overflow: hidden;
background: rgba(255, 255, 255, 0.14);
}
.mm-featured-art svg {
display: block;
}
.mm-eyebrow {
font-size: var(--font-size-xs);
letter-spacing: 0.11em;
text-transform: uppercase;
font-weight: var(--font-weight-semibold);
opacity: 0.85;
}
.mm-featured-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
line-height: 1.2;
}
.mm-featured-text {
font-size: var(--font-size-sm);
line-height: 1.4;
opacity: 0.92;
}
.mm-featured-cta {
margin-block-start: auto;
padding-block-start: var(--space-3);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
}
/* Link columns. */
.mm-col {
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mm-col-head {
font-size: var(--font-size-xs);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-content-secondary);
font-weight: var(--font-weight-semibold);
padding-inline: var(--space-2);
margin-block-end: var(--space-1);
}
.mm-link {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2);
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-content-primary);
text-align: start;
font: inherit;
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default);
}
.mm-link:hover,
.mm-link:focus-visible {
background: var(--state-hover);
outline: none;
}
.mm-ico {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
color: var(--color-primary-text);
background: color-mix(in oklab, var(--color-primary-element) 12%, transparent);
}
.mm-ico svg {
fill: none;
stroke: currentColor;
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
}
.mm-link-body {
display: flex;
flex-direction: column;
min-width: 0;
}
.mm-link-title {
font-size: var(--font-size-md);
font-weight: var(--font-weight-medium);
line-height: 1.3;
}
.mm-link-sub {
font-size: var(--font-size-xs);
color: var(--color-content-muted);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Shortcut = individual keycaps (the catalog Kbd primitive), macOS style. */
.mm-keys {
margin-inline-start: auto;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 3px;
}
</style>

Loading…
Cancel
Save

Powered by TurnKey Linux.