You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/onion-menu
dev ebc742e73b
fix(eidos): component-coherence audit FASE 2 — focus-ring, fields, segments, langs, API nits
4 months ago
..
README.md feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
color.test.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
color.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
context.svelte.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
geometry.test.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
geometry.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
index.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
onion-layer.svelte feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
onion-menu.css feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
onion-menu.svelte fix(eidos): component-coherence audit FASE 2 — focus-ring, fields, segments, langs, API nits 4 months ago
onion-option.svelte feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago
types.ts feat(eidos): action components — fab, onion-menu, button-group, split-button 4 months ago

README.md

OnionMenu

A trigger-agnostic radial ("onion") menu: any trigger opens a ring of options around a centre; an option with children drills into a new concentric ring. The active branch owns a distinct theme colour, the deeper rings derive lighter/darker tints by depth, and the already-passed inner rings mute to a trail — only the followed option stays lit.

<script>
  import { OnionMenu, OnionLayer, OnionOption } from '$uix/eidos/components/onion-menu';
  import { SvgPlus, SvgCheck } from '$uix/eidos/components/svg';
  let open = $state(false);
</script>

<OnionMenu bind:open placement="bottom-end">
  <OnionLayer textDisposition="angular">
    <OnionOption label="Create" color="teal">
      {#snippet icon()}<SvgPlus />{/snippet}
      <OnionLayer textDisposition="vertical">
        <OnionOption label="Document" onSelect={createDoc} />
        <OnionOption label="Folder" onSelect={createFolder} />
      </OnionLayer>
    </OnionOption>
    <OnionOption label="Share" onSelect={share}>
      {#snippet icon()}<SvgCheck />{/snippet}
    </OnionOption>
  </OnionLayer>
</OnionMenu>

Compositional API

The menu is declared by composition, not a tree data prop. <OnionLayer> is a ring; an option's children are a nested <OnionLayer> placed inside its <OnionOption> (like DropdownMenu.Sub). The parts don't paint — they register into a reactive node tree via context; the root <OnionMenu> reads that tree and paints the SVG rings centrally. Parts declare structure, the root draws.

Component Role
<OnionMenu> Root. Owns open / drillPath / selected, the trigger, render + behaviour.
<OnionLayer> One ring. Root layer = ring 1; a layer inside an option = that option's children.
<OnionOption> One sector. Nest an <OnionLayer> inside it to give it children.

Key <OnionMenu> props: placement (anchor → arc span 90/180/360 + orientation), open / drillPath / selected (bindable), innerRadius / ringWidth / gap / fontSize / width / height (geometry), trigger (snippet — any element). <OnionLayer>: textDisposition (angular/horizontal/vertical), color, display, ringWidth, gap. <OnionOption>: label, icon (snippet), color, display, disabled, onSelect.

Layers

  • morfo ($uix/morfo/components/onion-menu) — the contract: a Menu Button (APG), role="menu" surface, menuitem sectors, the focus policy (non-modal: arrows navigate, Tab / outside-click exit + close, focus returns to the trigger), the keyboard map, and the open / close (emerge) + commit-select (commit · affirm) events. expression: 'family-default' — no per-component sema pack.
  • soma ($soma/components/onion-menu) — the headless behaviour bridge: createOnionMenuRuntime emits the perceptual signals and dispatches the keyboard contract; onionNav* are the pure radial focus maths. The root drives focus / drill / dismiss against them.
  • eidos (here) — the SVG render + the two pure engines (geometry + colour) + the recipe. The root owns open / drill state and paints.
  • sema — picks open/close up from the emerge family base and commit-select from commit (no pack; family defaults).

Pure engines

  • geometry.ts — annular-sector maths: placement → arc, constant-pixel gaps (the angular pad scales 1/r), curved labels via <textPath> (flipped on the lower arc), icon boxes. Output is geometry only — no colour, no DOM.
  • color.ts — fully theme-driven colour via color-mix() of the theme's canonical scales (--scale-{name}-*) / roles (--primitive-{role}-*). A branch is a scale name; depth + muted recede the solid (step 9) toward the subtle step (step 3), which tracks the mode background (lightens in light mode, darkens in dark mode). Text auto-contrasts (--color-content-on-solid over solids, step 12 over tints). No invented colours — retinting the theme retints the menu.

Keyboard & a11y

Opened from the trigger (Enter / Space / ArrowDown), focus moves into the ring. Within a ring: ArrowRight / ArrowLeft move (wrap around, skip disabled), Enter / Space activate (drill a branch / choose a leaf), Backspace drills up (and closes at the root), Escape closes. Closing restores focus to the trigger. Outside-click and Tab-out dismiss (non-modal). Branch sectors expose aria-haspopup="menu" + aria-expanded; the surface is a role="menu", sectors are role="menuitem".

Recipe

The fills / text / stroke are computed per sector by the colour engine and applied inline (they depend on the live branch + drill state). The recipe (onion-menu.css) owns only structural chrome: the surface, the sector enter animation, the hover / selection rings (--color-content-on-solid at full / --opacity-65), the elevation (--shadow-raised sectors, --shadow-overlay trigger — single-layer tokens drop straight into drop-shadow()), the label / icon typography, and the +→× trigger glyph. All colours come through tokens; no raw hex in the recipe.

Decisions

  • Compositional, not data-driven — parts register a node tree; the root paints. An option's children nest inside the option (DropdownMenu.Sub shape).
  • Theme-driven colour — color-mix of --scale-*, mode-adaptive recession. The color prop is a theme scale / role name, never a raw colour.
  • Behaviour lives in the SVG root, not a parts provider — the menu is a single SVG renderer that already owns its state, so soma here is a behaviour bridge (runtime + nav maths) the root drives, rather than a compound provider tree.
  • Focus on open transition, not mount — a controlled open=true at mount does not steal focus; user/programmatic open transitions move focus into the ring.

Powered by TurnKey Linux.