feat(motion): event-driven cascade rollout to the menu family (ContextMenu + Menubar)

Extends the just-landed DropdownMenu event-driven item cascade to its siblings
so the framework is coherent — every menu in the family fades its items in
staggered on open, same mechanism, zero new generator code (rides the
container-driven preset branch shipped with DropdownMenu).

- ContextMenu (own recipe): Content marks the panel `[data-stagger]` +
  `--motion-stagger-each: var(--motion-stagger)`; item / checkbox-item /
  radio-item carry `data-animation-style="fade"`.
- Menubar (ONE line): soma `Menubar.Content` delegates to `DropdownMenuContent`,
  so its panel already carries `data-dropdown-menu-content` (→ the dropdown
  recipe rhythm applies) and re-uses `DropdownMenu.Item` (→ already
  `data-animation-style="fade"`). It only adds the `[data-stagger]` marker.

Verified at runtime: ContextMenu (scrubbed opacity gradient 0.96/0.84/0.00),
Menubar (panel carries `data-stagger` + `data-dropdown-menu-content`, items
`name:fade-in`, structural index from `:nth-child` 0/1/2/6/7). `check` clean for
the touched components. (Preview tab runs backgrounded → rAF paused; verified by
scrubbing `animation.currentTime` via WAAPI rather than waiting for playback.)

Deliberately NOT rolled out: select / combobox / command — live-filtered or
long-scroll lists where an open cascade is questionable UX.

Docs: ContextMenu README (Motion parity note), Menubar index doc, DropdownMenu
README (family cross-reference), RFC §D.11.4 (rollout).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 9d40ebe9c8
commit 19ecbf44e4

@ -1404,6 +1404,14 @@ each`, gradiente de opacidad en vuelo (Ruler 0.98 → Grid 0.89 → Guides 0.69,
solo cascadean los items que son **hijos directos** del panel — los envueltos en `<Group>`/`<RadioGroup>` (un
elemento DOM real) no son `:nth-child` del contenedor y aparecen instantáneos (el «intermedio transparente»).
**Rollout a la familia de menús (2026-06-20):** el mismo cableado en `<ContextMenu>` (recipe propio:
`data-context-menu-content` `[data-stagger]` + `--motion-stagger-each` + items `data-animation-style="fade"`) y
`<Menubar>` (**1 línea** — su panel ya carga `data-dropdown-menu-content` y reusa `DropdownMenu.Item`, así que
solo añade el marker `[data-stagger]`; el ritmo + el preset salen del recipe dropdown compartido). Verificados en
runtime: context-menu (gradiente scrubbed 0.96/0.84/0.00), menubar (panel con `data-stagger`+`data-dropdown-menu-content`,
items `name:fade-in`, índice `:nth-child` 0/1/2/6/7). **Fuera del rollout a propósito:** select/combobox/command —
listas con filtrado-en-vivo o scroll largo donde una cascada al abrir es UX cuestionable.
#### D.11.5 — El servicio `motion` es el motor (par/superior a Framer)
`EngineMotion` (`arts/motion` · `uix.motion`): drivers `spring` (integrador de Euler semi-implícito:

@ -11,6 +11,13 @@ DropdownMenu:
| Anchored to | Trigger's bounding box | Cursor position |
| `CheckboxGroup` | ✓ | — (Bits parity gap; can be added later) |
**Motion parity too.** Items fade in staggered on open — the *event-driven*
cascade. The Content marks the panel `[data-stagger]` + sets the rhythm
`--motion-stagger-each: var(--motion-stagger)`; each item carries
`data-animation-style="fade"`; the per-item index comes from `:nth-child`. Same
mechanism + caveats (direct-child items only; enter-only) as DropdownMenu — see
its README → **Motion** for the full explanation.
## Usage
```svelte

@ -15,7 +15,7 @@
}: ContextMenuCheckboxItemProps = $props();
</script>
<ContextMenu.CheckboxItem {...rest} {closeOnSelect} bind:checked>
<ContextMenu.CheckboxItem data-animation-style="fade" {...rest} {closeOnSelect} bind:checked>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}

@ -1,11 +1,17 @@
<script lang="ts">
/**
* `data-stagger` makes the panel the event-driven cascade container — items
* (carrying `data-animation-style`) fade in staggered on open. Same mechanism
* as DropdownMenu; see its README → Motion. The rhythm `--motion-stagger-each`
* comes from the recipe; the per-item index from `:nth-child` (structure).
*/
import * as ContextMenu from '$soma/components/context-menu';
import type { ContextMenuContentProps } from './types';
let { children: bodyContent, ...rest }: ContextMenuContentProps = $props();
</script>
<ContextMenu.Content {...rest}>
<ContextMenu.Content data-stagger="" {...rest}>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}

@ -1,7 +1,12 @@
<script lang="ts">
/**
* `data-animation-style="fade"` enrolls the item in the Content's event-driven
* cascade (staggered fade-in on open). Override the preset, or set the
* Content's `--motion-stagger-each: 0` for a parallel fade.
*/
import * as ContextMenu from '$soma/components/context-menu';
import type { ContextMenuItemProps } from './types';
let { children, ...rest }: ContextMenuItemProps = $props();
</script>
<ContextMenu.Item {...rest}>{@render children?.()}</ContextMenu.Item>
<ContextMenu.Item data-animation-style="fade" {...rest}>{@render children?.()}</ContextMenu.Item>

@ -14,7 +14,7 @@
}: ContextMenuRadioItemProps = $props();
</script>
<ContextMenu.RadioItem {...rest} {closeOnSelect}>
<ContextMenu.RadioItem data-animation-style="fade" {...rest} {closeOnSelect}>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}

@ -40,6 +40,15 @@
outline: none;
}
/* Event-driven item cascade rhythm — the Content wrapper marks itself
`[data-stagger]`; each direct-child item carries `data-animation-style` and
gets its structural index from `:nth-child`. `var(--motion-stagger)` is the
canonical 20ms (0 = parallel). Same mechanism as DropdownMenu; the appearance
flows from the `open` (emerge) event — no JS writes the index. */
[data-context-menu-content] {
--motion-stagger-each: var(--motion-stagger);
}
/* ── Item rows ─────────────────────────────────────────────────────── */
[data-context-menu-item],

@ -113,6 +113,11 @@ Two honest limits (both documented framework-wide):
lifecycle), still deferred — so a closing container does **not** drive a child
exit that would be cut off on unmount.
`<ContextMenu>` and `<Menubar>` share this verbatim (same soma menu machinery):
ContextMenu has its own recipe; Menubar's per-menu panel already carries
`data-dropdown-menu-content` and re-uses `DropdownMenu.Item`, so it inherits the
rhythm + the item preset and only adds the `[data-stagger]` marker.
## Comparativa
| Lib | Sub-menus | CheckboxItem / RadioItem | CheckboxGroup | Arrow | Modal toggle |

@ -26,7 +26,11 @@
// 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.
// chrome) since soma re-exports those DropdownMenu parts — so per-menu
// items also inherit the event-driven open cascade (items fade in
// staggered; see DropdownMenu README -> Motion). The eidos Content just
// adds the `[data-stagger]` marker; the rhythm + the item preset come
// from the shared dropdown recipe + the re-used `DropdownMenu.Item`.
//
// Drops `Arrow` part — the menubar idiom anchors panels to the bar
// edge; an arrow chrome doesn't fit. If you need one, render

@ -23,7 +23,14 @@
const passthrough = $derived(rest as Record<string, unknown>);
</script>
<Menubar.Content {...passthrough} data-size={resolvedSize}>
<!--
`data-stagger`: per-menu items cascade in on open (event-driven). The panel
also carries `data-dropdown-menu-content` (soma's MenubarContent specializes
DropdownMenuContent), so the dropdown recipe's `--motion-stagger-each` + the
`DropdownMenu.Item` `data-animation-style="fade"` (re-used as Menubar.Item)
already apply — this marker is all that's needed. See dropdown-menu README → Motion.
-->
<Menubar.Content {...passthrough} data-stagger="" data-size={resolvedSize}>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}

Loading…
Cancel
Save

Powered by TurnKey Linux.