feat(dropdown-menu): port from soma to eidos (Tier 1 sprint, 3/5)
Full 15-part compound — pass-through wrappers over soma with the
canonical visual recipe.
Parts: Provider · Trigger · Content · Arrow · Item · Group ·
GroupHeading · Separator · CheckboxItem · CheckboxGroup · RadioGroup
· RadioItem · Sub · SubTrigger · SubContent.
API parity with Bits UI (richest reference): CheckboxGroup +
RadioGroup binding arrays / scalars, nested Sub-menus, Arrow pointer.
Radix Primitives + Ark UI lack CheckboxGroup; Chakra v3 + Radix
Themes lack Arrow.
Recipe (dropdown-menu.css):
- Uniform chrome across Content + SubContent (panel shadow,
border, padding, max-height)
- Items share one set of tokens (hover/focus highlight, disabled
opacity, indicator slot via `::before` for Checkbox/Radio
data-state='checked')
- SubTrigger gets a chevron via `::after`
- GroupHeading: muted uppercase, micro-tracked
- Separator: 1px subtle divider
- Enter animation on `data-state='open'`
No `variant` / `color` / `size` knobs at the eidos layer — menus are
themselves the visual variant of "list of actions" within an app.
Per-item destructive coloring deferred to next pass (cascade via
item's `data-color` attr).
Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sound comes from
the family bases (`emerge` for open/close, `commit` for select) — no
per-component cascade needed.
Demo: 6-tab template, items hooked to `lastSelected` tracker, all
three groupings demoed (Items / CheckboxGroup / RadioGroup / Sub).
Checks: svelte-check 0 errors, component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
/*
|
|
|
|
|
* DropdownMenu recipe.
|
|
|
|
|
*
|
|
|
|
|
* Soma anchors the Content via the floating layer (Side / Align /
|
|
|
|
|
* collision avoidance). Eidos paints the surface chrome: panel with
|
|
|
|
|
* shadow, item hover/focus highlight, indicator slots for
|
|
|
|
|
* Checkbox/Radio items, muted Separator and GroupHeading.
|
|
|
|
|
*
|
|
|
|
|
* [data-dropdown-menu-trigger] → toggle button
|
|
|
|
|
* [data-dropdown-menu-content] → floating menu panel
|
|
|
|
|
* [data-dropdown-menu-item] → menuitem row
|
|
|
|
|
* [data-dropdown-menu-checkbox-item] → menuitemcheckbox row
|
|
|
|
|
* [data-dropdown-menu-radio-item] → menuitemradio row
|
|
|
|
|
* [data-dropdown-menu-sub-trigger] → menuitem that opens a submenu
|
|
|
|
|
* [data-dropdown-menu-sub-content] → nested menu panel
|
|
|
|
|
* [data-dropdown-menu-group] → semantic grouping wrapper
|
|
|
|
|
* [data-dropdown-menu-group-heading] → muted group label
|
|
|
|
|
* [data-dropdown-menu-separator] → 1px divider
|
|
|
|
|
* [data-dropdown-menu-arrow] → optional anchor pointer
|
|
|
|
|
*
|
|
|
|
|
* All highlighted-row visuals share one set of tokens so a fly-out
|
|
|
|
|
* SubContent reads identically to the root Content.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* ── Panel (Content / SubContent) ──────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-content],
|
|
|
|
|
[data-dropdown-menu-sub-content] {
|
|
|
|
|
min-inline-size: var(--dropdown-menu-content-min-width, 12rem);
|
|
|
|
|
max-block-size: var(--dropdown-menu-content-max-height, min(60vh, 22rem));
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: var(--dropdown-menu-content-padding, var(--space-1));
|
|
|
|
|
border: var(--dropdown-menu-content-border-width, 1px) solid
|
|
|
|
|
var(--dropdown-menu-content-border-color, var(--color-border-default));
|
|
|
|
|
border-radius: var(--dropdown-menu-content-radius, var(--radius-md));
|
|
|
|
|
background: var(--dropdown-menu-content-bg, var(--color-surface-overlay));
|
|
|
|
|
box-shadow: var(--dropdown-menu-content-shadow, var(--shadow-floating));
|
|
|
|
|
font-family: var(--style-label-font-family, var(--font-ui));
|
|
|
|
|
font-size: var(--dropdown-menu-content-font-size, var(--font-size-sm));
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-content]:focus-visible,
|
|
|
|
|
[data-dropdown-menu-sub-content]:focus-visible {
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Item rows ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-item],
|
|
|
|
|
[data-dropdown-menu-checkbox-item],
|
|
|
|
|
[data-dropdown-menu-radio-item],
|
|
|
|
|
[data-dropdown-menu-sub-trigger] {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--dropdown-menu-item-gap, var(--space-2));
|
|
|
|
|
min-block-size: var(--dropdown-menu-item-height, 2rem);
|
|
|
|
|
padding-block: var(--dropdown-menu-item-py, var(--space-1));
|
|
|
|
|
padding-inline: var(--dropdown-menu-item-px, var(--space-2));
|
|
|
|
|
border-radius: var(--dropdown-menu-item-radius, var(--radius-sm));
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
line-height: var(--leading-ui);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
transition:
|
|
|
|
|
background var(--duration-fast) var(--ease-default),
|
|
|
|
|
color var(--duration-fast) var(--ease-default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-item]:hover:not([data-disabled]),
|
|
|
|
|
[data-dropdown-menu-checkbox-item]:hover:not([data-disabled]),
|
|
|
|
|
[data-dropdown-menu-radio-item]:hover:not([data-disabled]),
|
|
|
|
|
[data-dropdown-menu-sub-trigger]:hover:not([data-disabled]),
|
|
|
|
|
[data-dropdown-menu-item]:focus-visible,
|
|
|
|
|
[data-dropdown-menu-checkbox-item]:focus-visible,
|
|
|
|
|
[data-dropdown-menu-radio-item]:focus-visible,
|
|
|
|
|
[data-dropdown-menu-sub-trigger]:focus-visible,
|
|
|
|
|
[data-dropdown-menu-sub-trigger][data-state='open'] {
|
|
|
|
|
outline: none;
|
|
|
|
|
background: var(--dropdown-menu-item-bg-hover, var(--color-primary-element));
|
|
|
|
|
color: var(--dropdown-menu-item-fg-hover, var(--color-content-primary));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-item][data-disabled],
|
|
|
|
|
[data-dropdown-menu-checkbox-item][data-disabled],
|
|
|
|
|
[data-dropdown-menu-radio-item][data-disabled],
|
|
|
|
|
[data-dropdown-menu-sub-trigger][data-disabled] {
|
|
|
|
|
cursor: default;
|
|
|
|
|
opacity: var(--dropdown-menu-item-disabled-opacity, 0.55);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Indicator slot + ::before painting for checkbox-item / radio-item shared
|
|
|
|
|
with context-menu via `lib/menu-indicator.css` (imported by eidos
|
|
|
|
|
entrypoint before this recipe). Override the private vars there if a
|
|
|
|
|
per-component tweak is needed (e.g. `--_menu-indicator-mask` for a
|
|
|
|
|
custom check glyph). */
|
feat(dropdown-menu): port from soma to eidos (Tier 1 sprint, 3/5)
Full 15-part compound — pass-through wrappers over soma with the
canonical visual recipe.
Parts: Provider · Trigger · Content · Arrow · Item · Group ·
GroupHeading · Separator · CheckboxItem · CheckboxGroup · RadioGroup
· RadioItem · Sub · SubTrigger · SubContent.
API parity with Bits UI (richest reference): CheckboxGroup +
RadioGroup binding arrays / scalars, nested Sub-menus, Arrow pointer.
Radix Primitives + Ark UI lack CheckboxGroup; Chakra v3 + Radix
Themes lack Arrow.
Recipe (dropdown-menu.css):
- Uniform chrome across Content + SubContent (panel shadow,
border, padding, max-height)
- Items share one set of tokens (hover/focus highlight, disabled
opacity, indicator slot via `::before` for Checkbox/Radio
data-state='checked')
- SubTrigger gets a chevron via `::after`
- GroupHeading: muted uppercase, micro-tracked
- Separator: 1px subtle divider
- Enter animation on `data-state='open'`
No `variant` / `color` / `size` knobs at the eidos layer — menus are
themselves the visual variant of "list of actions" within an app.
Per-item destructive coloring deferred to next pass (cascade via
item's `data-color` attr).
Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sound comes from
the family bases (`emerge` for open/close, `commit` for select) — no
per-component cascade needed.
Demo: 6-tab template, items hooked to `lastSelected` tracker, all
three groupings demoed (Items / CheckboxGroup / RadioGroup / Sub).
Checks: svelte-check 0 errors, component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
|
|
|
|
|
/* SubTrigger gets a chevron affordance on the trailing edge. */
|
|
|
|
|
[data-dropdown-menu-sub-trigger]::after {
|
|
|
|
|
content: '';
|
|
|
|
|
margin-inline-start: auto;
|
|
|
|
|
inline-size: 0.625rem;
|
|
|
|
|
block-size: 0.625rem;
|
|
|
|
|
border-inline-end: 2px solid currentColor;
|
|
|
|
|
border-block-start: 2px solid currentColor;
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
|
opacity: 0.65;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Group / GroupHeading / Separator ─────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-group] {
|
|
|
|
|
display: contents;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-group-heading] {
|
|
|
|
|
padding-block: var(--dropdown-menu-heading-py, var(--space-1));
|
|
|
|
|
padding-inline: var(--dropdown-menu-heading-px, var(--space-2));
|
|
|
|
|
color: var(--color-content-muted);
|
|
|
|
|
font-size: var(--dropdown-menu-heading-font-size, var(--font-size-xs));
|
|
|
|
|
font-weight: var(--style-label-font-weight, 500);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.04em; /* literal: micro-tracking for caps */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-separator] {
|
|
|
|
|
margin-block: var(--dropdown-menu-separator-my, var(--space-1));
|
|
|
|
|
block-size: 1px;
|
|
|
|
|
background: var(--dropdown-menu-separator-bg, var(--color-border-subtle));
|
|
|
|
|
}
|
|
|
|
|
|
refactor(dropdown-menu): remove unused modal; Trigger→Button; canon vocab fixes
Three related fixes touching the menu surface:
1. **Remove `modal` from DropdownMenu** (#127). The prop was declared in
soma but never wired to anything — no overlay, no pointer-events
blocker, no scrim. ark-ui / Headless-UI / Chakra all treat menu as
non-modal by convention; Dialog/Drawer/AlertDialog cover blocking
semantics. Drop it from soma (types, provider opts, component, test
fixture), the eidos doc comment, the menubar consumer, and the
dropdown-menu demo (state, control, snippet, API table, parity table).
2. **DropdownMenu.Trigger now renders `<Button>` internally** (#120
first consumer). Default variant='surface', size='md', intent='neutral'.
`variant` is narrowed to ControlVariant (`'surface' | 'outline' | 'ghost'`)
per DEMO_AUTHORING_GUIDE §12.7 so the trigger stays consistent with
the form-control surface (Field/Select/Combobox) instead of inheriting
Button's wider 6-variant set. The eidos demo exposes variant/size/intent
chips that match the canonical narrowed vocab (Intent: neutral · affirm
· fulfill · risk · threat · loss). Trigger's per-recipe CSS dropped —
chrome comes from button.css.
3. **Menu-item unchecked indicators** (follow-up to #118). CheckboxItem
and RadioItem now paint an empty outlined box / circle even when
unchecked, so the user can tell at a glance which rows are checkable.
Checked state fills with the accent color via the same `::before`. The
reserved padding slot (26px) keeps row text from shifting when state
flips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
/* ── Trigger ──────────────────────────────────────────────────────── */
|
|
|
|
|
/*
|
|
|
|
|
* The Trigger is rendered as an eidos `<Button>` (variant='surface' by
|
|
|
|
|
* default) — chrome, cursor, focus ring, padding all come from button.css.
|
|
|
|
|
* Consumers override via the standard Button visual props.
|
|
|
|
|
*/
|
feat(dropdown-menu): port from soma to eidos (Tier 1 sprint, 3/5)
Full 15-part compound — pass-through wrappers over soma with the
canonical visual recipe.
Parts: Provider · Trigger · Content · Arrow · Item · Group ·
GroupHeading · Separator · CheckboxItem · CheckboxGroup · RadioGroup
· RadioItem · Sub · SubTrigger · SubContent.
API parity with Bits UI (richest reference): CheckboxGroup +
RadioGroup binding arrays / scalars, nested Sub-menus, Arrow pointer.
Radix Primitives + Ark UI lack CheckboxGroup; Chakra v3 + Radix
Themes lack Arrow.
Recipe (dropdown-menu.css):
- Uniform chrome across Content + SubContent (panel shadow,
border, padding, max-height)
- Items share one set of tokens (hover/focus highlight, disabled
opacity, indicator slot via `::before` for Checkbox/Radio
data-state='checked')
- SubTrigger gets a chevron via `::after`
- GroupHeading: muted uppercase, micro-tracked
- Separator: 1px subtle divider
- Enter animation on `data-state='open'`
No `variant` / `color` / `size` knobs at the eidos layer — menus are
themselves the visual variant of "list of actions" within an app.
Per-item destructive coloring deferred to next pass (cascade via
item's `data-color` attr).
Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sound comes from
the family bases (`emerge` for open/close, `commit` for select) — no
per-component cascade needed.
Demo: 6-tab template, items hooked to `lastSelected` tracker, all
three groupings demoed (Items / CheckboxGroup / RadioGroup / Sub).
Checks: svelte-check 0 errors, component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
|
|
|
|
|
/* ── Arrow ────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-arrow] {
|
|
|
|
|
fill: var(--dropdown-menu-content-bg, var(--color-surface-overlay));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── State transitions ────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-content][data-state='closed'],
|
|
|
|
|
[data-dropdown-menu-sub-content][data-state='closed'] {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-dropdown-menu-content][data-state='open'],
|
|
|
|
|
[data-dropdown-menu-sub-content][data-state='open'] {
|
|
|
|
|
animation: dropdown-menu-enter var(--duration-fast) var(--ease-out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes dropdown-menu-enter {
|
|
|
|
|
from {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(-4px) scale(0.97);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: none;
|
|
|
|
|
}
|
|
|
|
|
}
|