@ -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 & 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 m enubar-specific parts (Provider, Menu, Trigger, Content)
M enubar-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) => 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 >