From 9def877dbd256345e9367560307c43db026d710c Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 14:43:13 +0200 Subject: [PATCH] feat(eidos): port Menubar (desktop File/Edit/View bar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #129. Eidos wrapper + types + recipe + canonical-depth demo. Soma owns active-menu state, cross-menu ArrowLeft/Right navigation, hover-switch when active, focus return on Escape, typeahead, RTL. Eidos paints the bar surface and trigger states; per-menu Content reuses the dropdown-menu recipe (Item / Sub / Checkbox / Radio chrome + the shared menu-indicator partial from `a2709476`) because soma already re-exports the DropdownMenu parts for those slots. Reference parity: | Feature | Radix | bits-ui | ark-ui | this port | |---|---|---|---|---| | Root provider w/ active-menu binding | ✓ | ✓ | ✗ (Menu only) | ✓ | | Menu / Trigger / Content | ✓ | ✓ | ✓ | ✓ | | Item / Group / GroupHeading / Separator | ✓ | ✓ | ✓ | ✓ | | CheckboxItem / CheckboxGroup / RadioGroup / RadioItem | ✓ | ✓ | ✓ | ✓ | | Sub / SubTrigger / SubContent | ✓ | ✓ | ✓ | ✓ | | Cross-menu ArrowLeft/Right (with loop) | ✓ | ✓ | n/a | ✓ | | Auto-open on hover when active | ✓ | ✓ | n/a | ✓ | | Typeahead | ✓ | ✓ | ✓ | ✓ | | Arrow part | ✓ | ✓ | ✓ | ⚠ dropped — menubar idiom anchors to bar edge | Parts shipped (15): Provider, Menu, Trigger, Portal, Content, Item, Group, GroupHeading, Separator, Arrow (re-exported), CheckboxItem, CheckboxGroup, RadioGroup, RadioItem, Sub, SubTrigger, SubContent. The inner 12 (Item onwards) are re-exports of the eidos DropdownMenu wrappers so the menu-indicator partial applies automatically — no recipe duplication. Variant set: only `size: 'sm' | 'md' | 'lg'` (xs would crowd triggers, xl unnecessary for a chrome bar). Size cascades from Menubar root to per-menu Content via a Svelte context (`menubar-context.ts`, `getMenubarSizeContext` / `setMenubarSizeContext`). Cross-layer: eidos Menubar reuses eidos DropdownMenu internals — the recipe doesn't redefine Item/Sub/Checkbox/Radio. Menubar.Content stamps `data-size` AND inherits `data-dropdown-menu-content` (soma's MenubarContent specializes DropdownMenuContent), so dropdown-menu.css paints the panel chrome and our menubar.css only declares the bar surface + trigger states. Workaround note: menubar-content.svelte casts rest props through `Record` to bypass a TS "union too complex" depth limit triggered by soma's inline-augmented MenuContentProps + HTML attribute union. Runtime types are correct. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/uix/eidos/components/menubar/index.ts | 106 +++ .../components/menubar/menubar-content.svelte | 30 + .../components/menubar/menubar-context.ts | 21 + .../components/menubar/menubar-menu.svelte | 15 + .../components/menubar/menubar-trigger.svelte | 17 + src/uix/eidos/components/menubar/menubar.css | 121 ++++ .../eidos/components/menubar/menubar.svelte | 48 ++ src/uix/eidos/components/menubar/types.ts | 78 +++ src/uix/eidos/index.css | 1 + .../uix/components/menubar/+page.svelte | 647 ++++++++++++++++++ 10 files changed, 1084 insertions(+) create mode 100644 src/uix/eidos/components/menubar/index.ts create mode 100644 src/uix/eidos/components/menubar/menubar-content.svelte create mode 100644 src/uix/eidos/components/menubar/menubar-context.ts create mode 100644 src/uix/eidos/components/menubar/menubar-menu.svelte create mode 100644 src/uix/eidos/components/menubar/menubar-trigger.svelte create mode 100644 src/uix/eidos/components/menubar/menubar.css create mode 100644 src/uix/eidos/components/menubar/menubar.svelte create mode 100644 src/uix/eidos/components/menubar/types.ts create mode 100644 web/routes/uix/components/menubar/+page.svelte diff --git a/src/uix/eidos/components/menubar/index.ts b/src/uix/eidos/components/menubar/index.ts new file mode 100644 index 000000000..a500021c6 --- /dev/null +++ b/src/uix/eidos/components/menubar/index.ts @@ -0,0 +1,106 @@ +// Menubar — eidos compound API. +// +// import { Menubar } from '$uix/eidos/components/menubar'; +// +// +// +// File +// +// +// New +// Open… +// +// +// Open recent +// +// project-1.svelte +// +// +// +// +// +// … +// +// +// Soma owns the active-menu state, cross-menu arrow navigation, hover- +// switch when active, focus return, typeahead, RTL. Eidos paints the +// bar surface + trigger states; per-menu Content reuses the +// dropdown-menu recipe (Item / Sub / Checkbox / Radio / Separator +// chrome) since soma re-exports those DropdownMenu parts. +// +// Drops `Arrow` part — the menubar idiom anchors panels to the bar +// edge; an arrow chrome doesn't fit. If you need one, render +// `` inside Content (it'll work, just visually +// awkward against the bar). +import MenubarComponent from './menubar.svelte'; +import Menu from './menubar-menu.svelte'; +import Trigger from './menubar-trigger.svelte'; +import Content from './menubar-content.svelte'; + +// Per-menu inner parts: re-use the eidos DropdownMenu wrappers so the +// menu-indicator shared partial (commit a2709476) applies and we don't +// duplicate Item/Sub/Checkbox/Radio recipes. +import { + DropdownMenu as _DropdownMenu +} from '$uix/eidos/components/dropdown-menu'; +import { Portal } from '$soma/components/internal'; + +type MenubarNamespace = typeof MenubarComponent & { + Menu: typeof Menu; + Trigger: typeof Trigger; + Portal: typeof Portal; + Content: typeof Content; + Item: typeof _DropdownMenu.Item; + Group: typeof _DropdownMenu.Group; + GroupHeading: typeof _DropdownMenu.GroupHeading; + Separator: typeof _DropdownMenu.Separator; + Arrow: typeof _DropdownMenu.Arrow; + CheckboxItem: typeof _DropdownMenu.CheckboxItem; + CheckboxGroup: typeof _DropdownMenu.CheckboxGroup; + RadioGroup: typeof _DropdownMenu.RadioGroup; + RadioItem: typeof _DropdownMenu.RadioItem; + Sub: typeof _DropdownMenu.Sub; + SubTrigger: typeof _DropdownMenu.SubTrigger; + SubContent: typeof _DropdownMenu.SubContent; +}; + +const Menubar = MenubarComponent as MenubarNamespace; +Menubar.Menu = Menu; +Menubar.Trigger = Trigger; +Menubar.Portal = Portal; +Menubar.Content = Content; +Menubar.Item = _DropdownMenu.Item; +Menubar.Group = _DropdownMenu.Group; +Menubar.GroupHeading = _DropdownMenu.GroupHeading; +Menubar.Separator = _DropdownMenu.Separator; +Menubar.Arrow = _DropdownMenu.Arrow; +Menubar.CheckboxItem = _DropdownMenu.CheckboxItem; +Menubar.CheckboxGroup = _DropdownMenu.CheckboxGroup; +Menubar.RadioGroup = _DropdownMenu.RadioGroup; +Menubar.RadioItem = _DropdownMenu.RadioItem; +Menubar.Sub = _DropdownMenu.Sub; +Menubar.SubTrigger = _DropdownMenu.SubTrigger; +Menubar.SubContent = _DropdownMenu.SubContent; + +export { Menubar }; +export default Menubar; + +export type { + MenubarProps, + MenubarMenuProps as MenuProps, + MenubarTriggerProps as TriggerProps, + MenubarContentProps as ContentProps, + MenubarArrowProps as ArrowProps, + MenubarItemProps as ItemProps, + MenubarGroupProps as GroupProps, + MenubarGroupHeadingProps as GroupHeadingProps, + MenubarSeparatorProps as SeparatorProps, + MenubarCheckboxItemProps as CheckboxItemProps, + MenubarCheckboxGroupProps as CheckboxGroupProps, + MenubarRadioGroupProps as RadioGroupProps, + MenubarRadioItemProps as RadioItemProps, + MenubarSubProps as SubProps, + MenubarSubTriggerProps as SubTriggerProps, + MenubarSubContentProps as SubContentProps, + MenubarSize +} from './types'; diff --git a/src/uix/eidos/components/menubar/menubar-content.svelte b/src/uix/eidos/components/menubar/menubar-content.svelte new file mode 100644 index 000000000..e70fdf0d6 --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar-content.svelte @@ -0,0 +1,30 @@ + + + + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/menubar/menubar-context.ts b/src/uix/eidos/components/menubar/menubar-context.ts new file mode 100644 index 000000000..77fef045c --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar-context.ts @@ -0,0 +1,21 @@ +import { getContext, setContext } from 'svelte'; +import type { MenubarSize } from './types'; + +/** + * Menubar size context — set by `` (Provider), read by + * `` so the size variant cascades to every per-menu + * panel without consumers needing to repeat the prop. Air did the same + * thing via `AIR_MENUBAR_CTX.getSize`; we just use Svelte's native + * `getContext`. + */ +const MENUBAR_SIZE_KEY = Symbol('eidos.menubar.size'); + +export type MenubarSizeContext = { getSize: () => MenubarSize }; + +export function setMenubarSizeContext(getSize: () => MenubarSize): void { + setContext(MENUBAR_SIZE_KEY, { getSize }); +} + +export function getMenubarSizeContext(): MenubarSizeContext | undefined { + return getContext(MENUBAR_SIZE_KEY); +} diff --git a/src/uix/eidos/components/menubar/menubar-menu.svelte b/src/uix/eidos/components/menubar/menubar-menu.svelte new file mode 100644 index 000000000..cd30994ed --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar-menu.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/menubar/menubar-trigger.svelte b/src/uix/eidos/components/menubar/menubar-trigger.svelte new file mode 100644 index 000000000..3325c7213 --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar-trigger.svelte @@ -0,0 +1,17 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/menubar/menubar.css b/src/uix/eidos/components/menubar/menubar.css new file mode 100644 index 000000000..8a9ebd682 --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar.css @@ -0,0 +1,121 @@ +/* + * Menubar recipe — desktop File / Edit / View bar. + * + * Scope: provider bar surface + per-menu trigger states. Per-menu + * Content / Item / Sub-tree / Checkbox / Radio chrome is reused from + * dropdown-menu.css since soma's Menubar literally re-exports the + * DropdownMenu parts for those slots (parts emit the same + * `data-dropdown-menu-*` attrs and the shared menu-indicator partial + * picks them up automatically). + * + * What's menubar-specific: + * + * [data-menubar] → bar surface + * [data-menubar-trigger] → in-bar trigger ("File", "Edit", …) + * [data-menubar-content] → per-menu panel (same chrome as + * [data-dropdown-menu-content] via + * the dropdown-menu recipe; we only + * add size variants here) + * + * Sizes — sm | md | lg (xs and xl out of band for a desktop chrome bar). + */ + +/* ── Provider — the bar itself ──────────────────────────────────────── */ + +[data-menubar] { + --_menubar-height: var(--control-height-md); + --_menubar-padding: var(--space-1); + --_menubar-gap: var(--space-1); + --_menubar-radius: var(--radius-md); + --_menubar-trigger-px: var(--space-3); + --_menubar-font-size: var(--font-size-sm); + + display: inline-flex; + align-items: center; + gap: var(--_menubar-gap); + padding: var(--_menubar-padding); + border: 1px solid var(--color-border-default); + border-radius: var(--_menubar-radius); + background: var(--color-surface-raised); +} + +[data-menubar][data-size='sm'] { + --_menubar-height: var(--control-height-sm); + --_menubar-padding: calc(var(--space-1) * 0.5); + --_menubar-gap: calc(var(--space-1) * 0.5); + --_menubar-radius: var(--radius-sm); + --_menubar-trigger-px: var(--space-2); + --_menubar-font-size: var(--font-size-xs); +} + +[data-menubar][data-size='lg'] { + --_menubar-height: var(--control-height-lg); + --_menubar-padding: var(--space-2); + --_menubar-gap: var(--space-2); + --_menubar-radius: var(--radius-lg); + --_menubar-trigger-px: var(--space-4); + --_menubar-font-size: var(--font-size-md); +} + +/* ── Trigger — in-bar menu opener ───────────────────────────────────── */ + +[data-menubar-trigger] { + display: inline-flex; + align-items: center; + justify-content: center; + min-block-size: calc(var(--_menubar-height) - var(--_menubar-padding) * 2 - 2px); + padding-inline: var(--_menubar-trigger-px); + border: none; + border-radius: calc(var(--_menubar-radius) - var(--_menubar-padding)); + background: transparent; + color: var(--color-content-primary); + font-family: var(--font-ui); + font-size: var(--_menubar-font-size); + font-weight: 500; + line-height: var(--leading-ui); + white-space: nowrap; + cursor: pointer; + outline: none; + transition: + background var(--duration-fast) var(--ease-default), + color var(--duration-fast) var(--ease-default); +} + +[data-menubar-trigger]:hover:not([data-disabled]), +[data-menubar-trigger][data-highlighted]:not([data-disabled]) { + background: var(--color-surface-overlay); +} + +[data-menubar-trigger][data-state='open'], +[data-menubar-trigger][data-state='open']:hover, +[data-menubar-trigger][data-state='open'][data-highlighted] { + background: var(--color-primary-element); + color: var(--color-content-primary); +} + +[data-menubar-trigger]:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: 1px; + position: relative; + z-index: 1; +} + +[data-menubar-trigger][data-disabled] { + color: var(--color-content-muted); + pointer-events: none; + cursor: default; +} + +/* ── Content — per-menu panel ─────────────────────────────────────────── + * Chrome (border, bg, shadow, padding, item rows, separator, sub-trigger + * chevron, group heading) comes from `dropdown-menu.css` via the + * `[data-dropdown-menu-*]` selectors that the Menu/Item/Sub parts emit + * (soma re-exports the DropdownMenu parts for the per-menu inner slots). + * + * We add only the menubar-specific marker selector here. The + * `[data-menubar-content]` selector matches the panel that wraps the + * Item rows — it's the same element that ALSO carries + * `[data-dropdown-menu-content]` because soma's MenubarContent + * specializes DropdownMenuContent. So the dropdown-menu panel rules + * already paint it; we don't repeat them. + */ diff --git a/src/uix/eidos/components/menubar/menubar.svelte b/src/uix/eidos/components/menubar/menubar.svelte new file mode 100644 index 000000000..c58e4f6be --- /dev/null +++ b/src/uix/eidos/components/menubar/menubar.svelte @@ -0,0 +1,48 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/menubar/types.ts b/src/uix/eidos/components/menubar/types.ts new file mode 100644 index 000000000..c1b1997a8 --- /dev/null +++ b/src/uix/eidos/components/menubar/types.ts @@ -0,0 +1,78 @@ +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; +import type { + ProviderProps, + MenuProps, + TriggerProps, + ContentProps, + ArrowProps, + ItemProps, + GroupProps, + GroupHeadingProps, + SeparatorProps, + CheckboxItemProps, + CheckboxGroupProps, + RadioGroupProps, + RadioItemProps, + SubProps, + SubTriggerProps, + SubContentProps +} from '$soma/components/menubar'; + +/** + * Menubar visual size — three-step subset of the canonical `Size` + * scale, matching the air baseline. A menubar is a desktop affordance + * (File / Edit / View / …); the comfortable-density range is sm..lg, + * xs would crowd the triggers and xl is unnecessary for a chrome bar. + */ +export type MenubarSize = Extract; + +export type MenubarProps = ProviderProps & { + /** + * Visual size preset — `'sm' | 'md' | 'lg'`. Resolved reactively via + * `Eidos.resolve(size)`. Propagated to Content via context so the + * size variant cascades to per-menu panels. + * + * @default 'md' + */ + size?: ResponsiveProp; +}; + +export type MenubarMenuProps = MenuProps; +export type MenubarTriggerProps = TriggerProps; +export type MenubarContentProps = ContentProps; + +// Re-exported from DropdownMenu — Menubar's per-menu inner parts +// (Item, Group, Separator, Checkbox/RadioItem, Sub-tree) are literally +// DropdownMenu parts. Eidos re-exports the dropdown-menu wrappers so +// the menu-indicator shared partial (commit a2709476) applies +// automatically. +export type { + DropdownMenuArrowProps as MenubarArrowProps, + DropdownMenuItemProps as MenubarItemProps, + DropdownMenuGroupProps as MenubarGroupProps, + DropdownMenuGroupHeadingProps as MenubarGroupHeadingProps, + DropdownMenuSeparatorProps as MenubarSeparatorProps, + DropdownMenuCheckboxItemProps as MenubarCheckboxItemProps, + DropdownMenuCheckboxGroupProps as MenubarCheckboxGroupProps, + DropdownMenuRadioGroupProps as MenubarRadioGroupProps, + DropdownMenuRadioItemProps as MenubarRadioItemProps, + DropdownMenuSubProps as MenubarSubProps, + DropdownMenuSubTriggerProps as MenubarSubTriggerProps, + DropdownMenuSubContentProps as MenubarSubContentProps +} from '$uix/eidos/components/dropdown-menu/types'; + +// Re-export for callers that don't want to dive into soma types +export type { + ArrowProps, + ItemProps, + GroupProps, + GroupHeadingProps, + SeparatorProps, + CheckboxItemProps, + CheckboxGroupProps, + RadioGroupProps, + RadioItemProps, + SubProps, + SubTriggerProps, + SubContentProps +}; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 03c8d8587..4321b2112 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -94,6 +94,7 @@ @import './components/alert-dialog/alert-dialog.css'; @import './components/dropdown-menu/dropdown-menu.css'; @import './components/context-menu/context-menu.css'; +@import './components/menubar/menubar.css'; /* Shared menu-item indicator partial — must load AFTER the menu recipes so its `padding-inline-start` calc wins over the row rule's `padding-inline` shorthand at equal specificity. */ diff --git a/web/routes/uix/components/menubar/+page.svelte b/web/routes/uix/components/menubar/+page.svelte new file mode 100644 index 000000000..1a3205c66 --- /dev/null +++ b/web/routes/uix/components/menubar/+page.svelte @@ -0,0 +1,647 @@ + + +
+
+
Navigation · Menubar
+

Menubar

+

+ Desktop File / Edit / View bar. Exactly one menu open at a time; + hover a sibling trigger while a menu is open to switch without an + intermediate click. Soma owns active-menu state + cross-menu + ArrowLeft/Right + typeahead + focus return on Escape. Per-menu + Item / Sub / Checkbox / Radio chrome is shared with + DropdownMenu — same parts, same recipe, same + menu-indicator (checkbox/radio glyphs match the standalone + Checkbox / RadioGroup). +

+
+ + parts{compiled.parts.order.length} + + + events{events.length} + + + sizes3 + + + apg + menubar ↗ + + {#if lastSelected} + + last{lastSelected} + + {/if} +
+
+ +
+
+ + + File + + + New tab + New window + New incognito (offline) + + + Open recent + + project.svelte + readme.md + package.json + + Clear recent + + + + Print… + Quit + + + + + + Edit + + + Undo + Redo + + Cut + Copy + Paste + + Find… + Replace… + + + + + + View + + + + Show status bar + + + + Layers + + Ruler + Grid + Guides + + + + + Theme + + Light + Dark + System + + + + + + + + Help + + + Documentation + Keyboard shortcuts + + About UIX + + + + +
+
+ trace + {#if trace.length === 0} + click a trigger or arrow-nav between menus + {:else} + {#each trace.slice(0, 3) as entry (entry.at)} + {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + active {activeMenu || '—'} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Soma owns active-menu state + keyboard nav across menus. Eidos + adds size on the root and propagates it to per-menu Content via context. +

+ +
+ soma Provider props +
+
+ + + +
+ +
+ eidos visual size + + · bar height + trigger padding + font-size + per-menu density + +
+
+ +
+ +
+ Bound state + + · mirrors of CheckboxItem / CheckboxGroup / RadioGroup + +
+
+ + + +
+ +
+
+ soma + headless · active-menu state + cross-menu nav + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · bar surface + trigger states + size context + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ Three menubar-specific parts (Provider, Menu, Trigger, Content) + plus DropdownMenu's full inner surface for Item / Sub / + Checkbox / Radio. The inner parts share recipes with + DropdownMenu — the + menu-indicator shared partial (checkbox/radio glyphs) applies + automatically. +

+ +
Provider
+
+ + + + + + + + + +
PropTypeDefaultDescription
value bindablestring''Identifier of the currently open menu. Empty = none open.
onValueChange(value: string) => void—Fires when the active menu changes.
dir'ltr' | 'rtl''ltr'Direction for cross-menu arrow nav.
loopbooleantrueWrap cross-menu arrow at edges.
size eidosResponsiveProp<'sm' | 'md' | 'lg'>'md'Visual size — bar height + trigger padding + per-menu density (via context).
+
+ +
Menu
+
+ + + + + + +
PropTypeDefaultDescription
valuestring—Unique identifier for this menu inside the menubar.
onOpenChange / onOpenChangeComplete(open: boolean) => void—Per-menu open/close + animation-settle callbacks.
+
+ +
Trigger
+
+ + + + + + +
PropTypeDefaultDescription
disabledbooleanfalseDisable opening this menu via this trigger.
…native button attrsPrimitiveButtonAttributes—Renders a native <button>; all HTML button attrs flow through.
+
+ +
Content
+
+ + + + + + +
PropTypeDefaultDescription
side · sideOffset · align · alignOffset · avoidCollisions · …FloatingLayerProps—Same surface as DropdownMenu.Content.
data-size eidosautofrom Menubar rootStamped via context — no need to pass per Content.
+
+ +
Inner parts (shared with DropdownMenu)
+

+ Item, Group, GroupHeading, + Separator, Arrow, CheckboxItem, + CheckboxGroup, RadioGroup, RadioItem, + Sub, SubTrigger, SubContent — + identical API to DropdownMenu. + Eidos re-uses those wrappers so the recipe (and the + menu-indicator shared partial) is one source of truth. +

+
+ {/if} + + {#if tab === 'morfo'} +
+

+ morfo + · declarative contract +

+

+ Source: src/uix/morfo/components/menubar.ts. +

+ +
+ + + + + + + + + + +
FieldValue
name"{menubarMorfo.name}"
kebab"{menubarMorfo.kebab}"
scope[{menubarMorfo.scope.map((s) => `"${s}"`).join(', ')}]
apg{menubarMorfo.apg}
parts.length{menubarMorfo.parts.length}
events.length{events.length}
+
+ +
Parts
+
+ + + + {#each partsList as part (part.kebab)} + + + + + + + + + + {/each} + +
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.archetype ?? '—'}{part.states.length ? part.states.join(' | ') : '—'}{part.optional ? 'yes' : 'no'}
+
+ +
Events declaration
+
+ + + + {#each events as action (action.name)} + {@const sem = action.semantic} + {@const intentDecl = 'intent' in sem ? sem.intent : undefined} + {@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'} + + + + + + + + + {/each} + +
namefamilyverbsequenceintenttarget
{action.name}{sem.family}{sem.verb ?? '—'}{sem.sequence ?? 'pre'}{intentStr}{action.target}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema + · events + perceptual signature +

+

+ One event — commit-select — fires when an Item is + activated. Open/close of individual menus rides on + DropdownMenu's events via the re-exported parts. +

+ +
+ + + + {#each events as action (action.name)} + {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined} + {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined} + + + + + + + + + {/each} + +
NameFamilyVerbSequenceIntentPlay
{action.name}{action.semantic.family}{action.semantic.verb ?? '—'}{action.semantic.sequence ?? 'pre'}{effectiveIntent ?? '—'} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors at src/uix/eidos/components/menubar/menubar.css + (bar + trigger only). Per-menu Content / Item / Sub / Checkbox / Radio + chrome is reused from dropdown-menu.css + the shared + menu-indicator.css partial because soma's Menubar re-exports + the DropdownMenu parts for those slots. +

+
+ + + + + + + + + + +
SelectorSourceWhat it paints
[data-menubar]eidosThe bar surface — flex row, border, bg.
[data-menubar][data-size]eidosSize variant (sm/md/lg) — height + padding + gap + font.
[data-menubar-trigger]morfoIn-bar trigger: transparent bg, hover/open tint, focus ring.
[data-menubar-trigger][data-state='open']morfoOpen menu's trigger lights up with primary tint.
[data-dropdown-menu-content], [data-dropdown-menu-item], …eidosInner panel + items — shared with DropdownMenu.
[data-dropdown-menu-checkbox-item]::before, ::aftereidosCheckbox indicator — outlined box / filled + ✓ via shared menu-indicator partial.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+

+ Follows the APG menubar pattern. + Bar exposes role="menubar"; each per-menu Content is + role="menu". Trigger has aria-haspopup="menu", + aria-expanded and aria-controls. +

+ +
Keyboard
+
+ + + + + + + + + + + +
KeyAction
ArrowRight / ArrowLeft on TriggerMove focus to next/previous trigger (wraps if loop=true; RTL inverts).
Enter / Space / ArrowDown on TriggerOpen the menu, focus first item.
ArrowUp / ArrowDown on ItemMove within the menu.
ArrowLeft / ArrowRight on ItemClose current menu, focus + open prev/next menu.
EscapeClose the menu, return focus to its Trigger.
Home / EndJump to first / last item.
LetterTypeahead — focus next item starting with that letter.
+
+ +
Pointer
+
+ + + + + + + +
GestureBehavior
Click triggerOpens its menu (or closes if already open).
Hover sibling trigger while a menu is openSwitches to the hovered menu without an intermediate click.
Click outsideCloses the active menu.
+
+ +
ARIA contract
+
+ + + + + + + + + + + +
PartAttributeValue
menubarrole"menubar"
triggeraria-haspopup"menu"
triggeraria-expanded{`{open}`}
triggeraria-controls{``}
triggerdata-state"open" | "closed"
contentrole"menu"
contentaria-labelledby{``}
+
+
+ {/if} +