|
|
|
|
@ -0,0 +1,584 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { MenuDial } from '$uix/eidos/components/menu-dial';
|
|
|
|
|
import type {
|
|
|
|
|
MenuDialDirection,
|
|
|
|
|
MenuDialLabels,
|
|
|
|
|
MenuDialLayout,
|
|
|
|
|
MenuDialOpenOn,
|
|
|
|
|
MenuDialPlacement
|
|
|
|
|
} from '$uix/eidos/components/menu-dial';
|
|
|
|
|
import type { FabSize } from '$uix/eidos/components/fab';
|
|
|
|
|
import { SvgPlus, SvgCheck, SvgDot, SvgMinus, SvgChevron } from '$uix/eidos/components/svg';
|
|
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
|
|
import { menuDialMorfo } from '@/uix/morfo/components/menu-dial';
|
|
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
|
|
|
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
|
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
let trace = $state<TraceEntry[]>([]);
|
|
|
|
|
let stageRef = $state<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
|
|
// ── Dial state ───────────────────────────────────────────────────────
|
|
|
|
|
let open = $state(true);
|
|
|
|
|
let last = $state('—');
|
|
|
|
|
|
|
|
|
|
// ── Live props — every public knob is a control ────────────────────────
|
|
|
|
|
let layout = $state<MenuDialLayout>('linear');
|
|
|
|
|
let direction = $state<MenuDialDirection>('up');
|
|
|
|
|
let openOn = $state<MenuDialOpenOn>('click');
|
|
|
|
|
let size = $state<FabSize>('md');
|
|
|
|
|
let placement = $state<MenuDialPlacement>('center');
|
|
|
|
|
let labels = $state<MenuDialLabels>('tooltip');
|
|
|
|
|
let variant = $state<'solid' | 'soft' | 'surface' | 'outline' | 'ghost'>('solid');
|
|
|
|
|
let color = $state<'primary' | 'secondary' | 'neutral' | undefined>(undefined);
|
|
|
|
|
let intent = $state<'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss' | undefined>(undefined);
|
|
|
|
|
let backdrop = $state(false);
|
|
|
|
|
let stagger = $state(true);
|
|
|
|
|
|
|
|
|
|
const layouts: MenuDialLayout[] = ['linear', 'arc'];
|
|
|
|
|
const directions: MenuDialDirection[] = ['up', 'down', 'left', 'right'];
|
|
|
|
|
const openOns: MenuDialOpenOn[] = ['click', 'hover'];
|
|
|
|
|
const sizes: FabSize[] = ['sm', 'md', 'lg'];
|
|
|
|
|
// Zone grid (like OnionMenu) — the 9 zones: corners (90°), edges (180°) and the
|
|
|
|
|
// centre (360°). A separate `static` chip below opts out of fixed positioning.
|
|
|
|
|
const placementGrid: MenuDialPlacement[][] = [
|
|
|
|
|
['top-start', 'top-center', 'top-end'],
|
|
|
|
|
['left-center', 'center', 'right-center'],
|
|
|
|
|
['bottom-start', 'bottom-center', 'bottom-end']
|
|
|
|
|
];
|
|
|
|
|
const labelModes: MenuDialLabels[] = ['tooltip', 'inline', 'none'];
|
|
|
|
|
const variants = ['solid', 'soft', 'surface', 'outline', 'ghost'] as const;
|
|
|
|
|
const colors = [undefined, 'primary', 'secondary', 'neutral'] as const;
|
|
|
|
|
const intents = [undefined, 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as const;
|
|
|
|
|
|
|
|
|
|
const compiled = compileMorfo(menuDialMorfo);
|
|
|
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
|
|
|
const events = $derived([...compiled.actions.byName.values()]);
|
|
|
|
|
|
|
|
|
|
function fmtTime(at: number): string {
|
|
|
|
|
const d = new Date(at);
|
|
|
|
|
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
const el = stageRef;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const obs = new MutationObserver((mutations) => {
|
|
|
|
|
for (const m of mutations) {
|
|
|
|
|
if (m.attributeName !== 'data-event') continue;
|
|
|
|
|
const target = m.target as Element;
|
|
|
|
|
const ev = target.getAttribute('data-event');
|
|
|
|
|
if (!ev) continue;
|
|
|
|
|
trace = [
|
|
|
|
|
{
|
|
|
|
|
event: ev,
|
|
|
|
|
family: target.getAttribute('data-event-family') ?? '—',
|
|
|
|
|
intent: target.getAttribute('data-event-intent') ?? undefined,
|
|
|
|
|
at: Date.now()
|
|
|
|
|
},
|
|
|
|
|
...trace
|
|
|
|
|
].slice(0, 6);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
|
|
|
|
|
return () => obs.disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const eidosSnippet = $derived(
|
|
|
|
|
[
|
|
|
|
|
"<script lang='ts'>",
|
|
|
|
|
" import { MenuDial } from '$uix/eidos/components/menu-dial';",
|
|
|
|
|
'</' + 'script>',
|
|
|
|
|
'',
|
|
|
|
|
'<MenuDial',
|
|
|
|
|
layout !== 'linear' && ` layout="${layout}"`,
|
|
|
|
|
direction !== 'up' && ` direction="${direction}"`,
|
|
|
|
|
openOn !== 'click' && ` openOn="${openOn}"`,
|
|
|
|
|
size !== 'md' && ` size="${size}"`,
|
|
|
|
|
placement !== 'bottom-end' && ` placement="${placement}"`,
|
|
|
|
|
labels !== 'tooltip' && ` labels="${labels}"`,
|
|
|
|
|
variant !== 'solid' && ` variant="${variant}"`,
|
|
|
|
|
color && ` color="${color}"`,
|
|
|
|
|
backdrop && ' backdrop',
|
|
|
|
|
!stagger && ' stagger={false}',
|
|
|
|
|
' aria-label="Quick actions"',
|
|
|
|
|
'>',
|
|
|
|
|
' <MenuDial.Action label="Add" onSelect={add}>',
|
|
|
|
|
' {#snippet icon()}<SvgPlus />{/snippet}',
|
|
|
|
|
' </MenuDial.Action>',
|
|
|
|
|
' <MenuDial.Action label="Confirm" intent="affirm" onSelect={ok} />',
|
|
|
|
|
' <MenuDial.Action label="Delete" intent="threat" onSelect={remove} />',
|
|
|
|
|
'</MenuDial>'
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function playTarget(part: string): HTMLElement | null {
|
|
|
|
|
return (stageRef?.querySelector(`[data-menu-dial-${part}]`) ??
|
|
|
|
|
stageRef?.querySelector('[data-menu-dial-list]') ??
|
|
|
|
|
stageRef) as HTMLElement | null;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#snippet iAdd()}<SvgPlus />{/snippet}
|
|
|
|
|
{#snippet iConfirm()}<SvgCheck />{/snippet}
|
|
|
|
|
{#snippet iMore()}<SvgDot />{/snippet}
|
|
|
|
|
{#snippet iCollapse()}<SvgChevron direction="down" />{/snippet}
|
|
|
|
|
{#snippet iRemove()}<SvgMinus />{/snippet}
|
|
|
|
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
|
|
<header>
|
|
|
|
|
<div data-uix-eyebrow>Overlay · MenuDial</div>
|
|
|
|
|
<h1 data-uix-page-title>Menu dial</h1>
|
|
|
|
|
<p data-uix-page-lede>
|
|
|
|
|
A <strong>speed dial</strong>: a FAB that fans out a linear set of action mini-FABs.
|
|
|
|
|
<strong>Compositional</strong> — the trigger is a <code><Fab></code>, each
|
|
|
|
|
<code><MenuDial.Action></code> a <code><Fab></code> (the Material "small FAB"). Flat
|
|
|
|
|
quick actions — the radial / nested case is <strong>OnionMenu</strong>'s. The fan lays out by
|
|
|
|
|
<code>direction</code> in plain flex (no SVG).
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-page-meta>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>events</span>{events.length}</span>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>composes</span>Fab · Tooltip</span>
|
|
|
|
|
<span data-uix-meta-pill>
|
|
|
|
|
<span data-uix-meta-key>apg</span>
|
|
|
|
|
<a href={menuDialMorfo.apg} target="_blank" rel="noreferrer">menu-button ↗</a>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div data-uix-stage>
|
|
|
|
|
<div
|
|
|
|
|
data-uix-stage-area
|
|
|
|
|
bind:this={stageRef}
|
|
|
|
|
style="position: relative; contain: layout; min-block-size: 24rem; display: grid; place-items: center; overflow: hidden; border-radius: var(--uix-radius-lg);"
|
|
|
|
|
>
|
|
|
|
|
<MenuDial
|
|
|
|
|
bind:open
|
|
|
|
|
{layout}
|
|
|
|
|
{direction}
|
|
|
|
|
{openOn}
|
|
|
|
|
{size}
|
|
|
|
|
{placement}
|
|
|
|
|
{labels}
|
|
|
|
|
{variant}
|
|
|
|
|
{color}
|
|
|
|
|
{intent}
|
|
|
|
|
{backdrop}
|
|
|
|
|
{stagger}
|
|
|
|
|
aria-label="Quick actions"
|
|
|
|
|
>
|
|
|
|
|
<MenuDial.Action label="Add" icon={iAdd} onSelect={() => (last = 'Add')} />
|
|
|
|
|
<MenuDial.Action label="Confirm" intent="affirm" icon={iConfirm} onSelect={() => (last = 'Confirm')} />
|
|
|
|
|
<MenuDial.Action label="More" icon={iMore} onSelect={() => (last = 'More')} />
|
|
|
|
|
<MenuDial.Action label="Collapse" icon={iCollapse} onSelect={() => (last = 'Collapse')} />
|
|
|
|
|
<MenuDial.Action label="Remove" intent="threat" disabled icon={iRemove} onSelect={() => (last = 'Remove')} />
|
|
|
|
|
</MenuDial>
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-stage-trace>
|
|
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
|
|
{#if trace.length === 0}
|
|
|
|
|
<span>open the dial, choose an action, or fire a Sema event</span>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each trace.slice(0, 3) as entry}
|
|
|
|
|
<span
|
|
|
|
|
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
|
|
|
|
|
? ' · ' + entry.intent
|
|
|
|
|
: ''}</span
|
|
|
|
|
>
|
|
|
|
|
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
<span style="margin-inline-start: auto;">
|
|
|
|
|
<span data-uix-stage-trace-key>state</span>
|
|
|
|
|
{open ? 'open' : 'closed'} · <span data-uix-stage-trace-key>last</span>
|
|
|
|
|
{last}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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>2</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
|
|
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
|
|
|
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
|
|
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
|
|
|
<span data-uix-tab-count>{events.length}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if tab === 'live'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Controls</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
Every public prop is a live control. The dial is <code>static</code> here; the floating
|
|
|
|
|
<code>placement</code> values fix it to the viewport. Events live in the
|
|
|
|
|
<button data-uix-inline-link onclick={() => (tab = 'sema')}>Sema</button> tab.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span> props · MenuDial (root) · layout
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>layout <span data-uix-control-hint>straight line vs placement-driven arc</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each layouts as l}
|
|
|
|
|
<button data-uix-chip data-active={layout === l} onclick={() => (layout = l)}>{l}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>direction <span data-uix-control-hint>linear axis · arc opening on static</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each directions as d}
|
|
|
|
|
<button data-uix-chip data-active={direction === d} onclick={() => (direction = d)}>{d}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>placement <span data-uix-control-hint>9 zones · arc 90° corner / 180° edge / 360° centre</span></span>
|
|
|
|
|
<div style="display:flex; align-items:center; gap:10px;">
|
|
|
|
|
<div style="display:grid; grid-template-columns:repeat(3,auto); gap:6px; width:max-content;">
|
|
|
|
|
{#each placementGrid as row}
|
|
|
|
|
{#each row as p}
|
|
|
|
|
<button
|
|
|
|
|
data-uix-chip
|
|
|
|
|
data-active={placement === p}
|
|
|
|
|
title={p}
|
|
|
|
|
style="min-width:30px;"
|
|
|
|
|
onclick={() => (placement = p)}>{p === 'center' ? '○' : '·'}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
<button data-uix-chip data-active={placement === 'static'} title="static (place it yourself)" onclick={() => (placement = 'static')}>static</button>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>openOn</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each openOns as o}
|
|
|
|
|
<button data-uix-chip data-active={openOn === o} onclick={() => (openOn = o)}>{o}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>open</span>
|
|
|
|
|
<span data-uix-switch><input type="checkbox" bind:checked={open} /><span data-uix-switch-label>{open ? 'on' : 'off'}</span></span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span> props · appearance
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>size <span data-uix-control-hint>actions one step smaller</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each sizes as s}
|
|
|
|
|
<button data-uix-chip data-active={size === s} onclick={() => (size = s)}>{s}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>labels</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each labelModes as l}
|
|
|
|
|
<button data-uix-chip data-active={labels === l} onclick={() => (labels = l)}>{l}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>variant <span data-uix-control-hint>trigger</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each variants as v}
|
|
|
|
|
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v)}>{v}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>color <span data-uix-control-hint>role · applies when intent=neutral</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each colors as c}
|
|
|
|
|
<button data-uix-chip data-active={color === c} onclick={() => (color = c)}>{c ?? 'default'}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>intent <span data-uix-control-hint>evaluative · wins over color</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each intents as i}
|
|
|
|
|
<button data-uix-chip data-active={intent === i} onclick={() => (intent = i)}>{i ?? 'neutral'}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>backdrop <span data-uix-control-hint>full-screen scrim</span></span>
|
|
|
|
|
<span data-uix-switch><input type="checkbox" bind:checked={backdrop} /><span data-uix-switch-label>{backdrop ? 'on' : 'off'}</span></span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>stagger</span>
|
|
|
|
|
<span data-uix-switch><input type="checkbox" bind:checked={stagger} /><span data-uix-switch-label>{stagger ? 'on' : 'off'}</span></span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-code>
|
|
|
|
|
<div data-uix-code-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
|
|
|
<span>compositional · MenuDial › MenuDial.Action</span>
|
|
|
|
|
<span data-uix-code-lang>svelte</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre><code>{eidosSnippet}</code></pre>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'api'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>API reference</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
Two components. <code><MenuDial></code> holds the trigger + the actions; each
|
|
|
|
|
<code><MenuDial.Action></code> is a mini-FAB.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-subsection-head>MenuDial</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">layout</td><td class="type">'linear' | 'arc'</td><td class="default">'linear'</td><td>Straight line, or a placement-driven arc (90° corner / 180° static).</td></tr>
|
|
|
|
|
<tr><td class="name">direction</td><td class="type">'up' | 'down' | 'left' | 'right'</td><td class="default">'up'</td><td>Linear axis; orients the arc on <code>static</code>.</td></tr>
|
|
|
|
|
<tr><td class="name">radius</td><td class="type">string</td><td class="default empty">— (size-derived)</td><td>Arc radius override (CSS length).</td></tr>
|
|
|
|
|
<tr><td class="name">openOn</td><td class="type">'click' | 'hover'</td><td class="default">'click'</td><td>Open on click (toggle) or hover.</td></tr>
|
|
|
|
|
<tr><td class="name">placement</td><td class="type">MenuDialPlacement</td><td class="default">'bottom-end'</td><td>One of 9 zones (corner / edge / centre) or <code>static</code>; sets the arc span (90/180/360°).</td></tr>
|
|
|
|
|
<tr><td class="name">size</td><td class="type">'sm' | 'md' | 'lg'</td><td class="default">'md'</td><td>Trigger FAB size; actions are one step smaller.</td></tr>
|
|
|
|
|
<tr><td class="name">labels</td><td class="type">'tooltip' | 'inline' | 'none'</td><td class="default">'tooltip'</td><td>How action names are shown.</td></tr>
|
|
|
|
|
<tr><td class="name">backdrop</td><td class="type">boolean</td><td class="default">false</td><td>Scrim behind the open fan.</td></tr>
|
|
|
|
|
<tr><td class="name">stagger</td><td class="type">boolean</td><td class="default">true</td><td>Stagger the actions in/out.</td></tr>
|
|
|
|
|
<tr><td class="name">open</td><td class="type">boolean</td><td class="default">false</td><td>Bindable open state.</td></tr>
|
|
|
|
|
<tr><td class="name">variant / color / intent</td><td class="type">Button knobs</td><td class="default empty">—</td><td>Trigger chrome.</td></tr>
|
|
|
|
|
<tr><td class="name">icon / openIcon</td><td class="type">Snippet</td><td class="default empty">— (+→×)</td><td>Trigger glyph; omit for the default morph.</td></tr>
|
|
|
|
|
<tr><td class="name">onOpenChange</td><td class="type">(open) => void</td><td class="default empty">—</td><td>Open-state change.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-subsection-head>MenuDial.Action</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">label</td><td class="type">string</td><td class="default empty">—</td><td>Name + tooltip / inline pill.</td></tr>
|
|
|
|
|
<tr><td class="name">icon</td><td class="type">Snippet</td><td class="default empty">—</td><td>The glyph.</td></tr>
|
|
|
|
|
<tr><td class="name">onSelect</td><td class="type">() => void</td><td class="default empty">—</td><td>Chosen (also closes the dial).</td></tr>
|
|
|
|
|
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable + skip in nav.</td></tr>
|
|
|
|
|
<tr><td class="name">variant / color / intent</td><td class="type">Button knobs</td><td class="default empty">—</td><td>Per-action chrome.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'morfo'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>
|
|
|
|
|
<span data-uix-layer-badge="morfo">morfo</span> · declarative contract
|
|
|
|
|
</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
The morfo declares the menu-button contract: the trigger, the <code>role="menu"</code> list,
|
|
|
|
|
the <code>menuitem</code> actions, and the open / close / commit-select events.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">name</td><td class="type">"{menuDialMorfo.name}"</td></tr>
|
|
|
|
|
<tr><td class="name">kebab</td><td class="type">"{menuDialMorfo.kebab}"</td></tr>
|
|
|
|
|
<tr><td class="name">scope</td><td class="type">[{menuDialMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
|
|
|
|
|
<tr><td class="name">apg</td><td><a href={menuDialMorfo.apg} target="_blank" rel="noreferrer">menu-button ↗</a></td></tr>
|
|
|
|
|
<tr><td class="name">parts.length</td><td class="default">{menuDialMorfo.parts.length}</td></tr>
|
|
|
|
|
<tr><td class="name">events.length</td><td class="default">{events.length}</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Parts</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each partsList as part}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">{part.kebab}</td>
|
|
|
|
|
<td><code data-uix-part-marker>[{part.marker}]</code></td>
|
|
|
|
|
<td class="type"><{part.defaultElement}></td>
|
|
|
|
|
<td class="type">{part.role ?? '—'}</td>
|
|
|
|
|
<td class="default">{part.archetype ?? '—'}</td>
|
|
|
|
|
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#each menuDialMorfo.parts as rawPart}
|
|
|
|
|
{@const partAny = rawPart as unknown as {
|
|
|
|
|
kebab: string;
|
|
|
|
|
aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string }>;
|
|
|
|
|
keyboard?: ReadonlyArray<{ key: string; action: string }>;
|
|
|
|
|
}}
|
|
|
|
|
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
|
|
|
|
|
{@const ariaAttrs = partAny.aria ?? []}
|
|
|
|
|
{@const keys = partAny.keyboard ?? []}
|
|
|
|
|
{#if dataAttrs.length || ariaAttrs.length || keys.length}
|
|
|
|
|
<div data-uix-subsection-head>{partAny.kebab}</div>
|
|
|
|
|
{#if ariaAttrs.length}
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>aria-attr</th><th>Source</th><th>Condition</th><th>Severity</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each ariaAttrs as a}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">{a.attr}</td>
|
|
|
|
|
<td class="type">{a.value.kind}</td>
|
|
|
|
|
<td class="default">{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}` : 'always'}</td>
|
|
|
|
|
<td class="default">{a.severity ?? 'required'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if keys.length}
|
|
|
|
|
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>key</th><th>action</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each keys as k}
|
|
|
|
|
<tr><td><span data-uix-kbd>{k.key === ' ' ? 'Space' : k.key}</span></td><td class="default">{k.action}</td></tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Events</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each menuDialMorfo.events as ev}
|
|
|
|
|
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: string; target?: { kind: string; ref?: string } }}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">{ev.name}</td>
|
|
|
|
|
<td class="type">{sem.family}</td>
|
|
|
|
|
<td class="default">{sem.verb ?? '—'}</td>
|
|
|
|
|
<td class="default">{sem.sequence ?? '—'}</td>
|
|
|
|
|
<td class="default">{sem.intent ?? '—'}</td>
|
|
|
|
|
<td class="default">{sem.target?.ref ?? sem.target?.kind ?? '—'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'sema'}
|
|
|
|
|
<section data-uix-section id="sema">
|
|
|
|
|
<h2 data-uix-section-title>
|
|
|
|
|
<span data-uix-layer-badge="sema">sema</span> · events
|
|
|
|
|
</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
<code>open</code> / <code>close</code> (emerge · list) and <code>commit-select</code>
|
|
|
|
|
(commit · affirm · action). Expression <code>family-default</code>. The soma layer fires
|
|
|
|
|
these on real interaction; here ▶ play emits onto the live target.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>play</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each menuDialMorfo.events as ev}
|
|
|
|
|
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: string; target?: { kind: string; ref?: string } }}
|
|
|
|
|
{@const part = sem.target?.ref ?? 'list'}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">{ev.name}</td>
|
|
|
|
|
<td class="type">{sem.family}</td>
|
|
|
|
|
<td class="default">{sem.verb ?? '—'}</td>
|
|
|
|
|
<td class="default">{sem.sequence ?? '—'}</td>
|
|
|
|
|
<td class="default">{sem.intent ?? '—'}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<button
|
|
|
|
|
data-uix-play
|
|
|
|
|
onclick={() => {
|
|
|
|
|
const target = playTarget(part);
|
|
|
|
|
if (!target) return;
|
|
|
|
|
void uix.events?.emit({
|
|
|
|
|
name: ev.name,
|
|
|
|
|
family: sem.family as never,
|
|
|
|
|
target,
|
|
|
|
|
...(sem.intent ? { intent: sem.intent as never } : {})
|
|
|
|
|
});
|
|
|
|
|
}}>▶ play</button
|
|
|
|
|
>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'recipe'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
The trigger + actions are composed FABs; <code>menu-dial.css</code> owns only the placement,
|
|
|
|
|
the fan layout per <code>data-direction</code>, the open/close + stagger and the glyph.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Selector</th><th>Source</th><th>Purpose</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name"><code>[data-menu-dial][data-direction]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Fan flex axis per direction.</td></tr>
|
|
|
|
|
<tr><td class="name"><code>[data-menu-dial][data-placement]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Viewport-fixed corner.</td></tr>
|
|
|
|
|
<tr><td class="name"><code>[data-menu-dial-list][data-state]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Open/close + stagger of the actions.</td></tr>
|
|
|
|
|
<tr><td class="name"><code>.menu-dial-action-label</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Tooltip / inline pill.</td></tr>
|
|
|
|
|
<tr><td class="name"><code>[data-menu-dial][data-state='open'] .menu-dial-glyph</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Rotate the + → × on open.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'a11y'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">Trigger</td><td><code><Fab></code> with <code>aria-haspopup="menu"</code> + <code>aria-expanded</code>.</td></tr>
|
|
|
|
|
<tr><td class="name">Fan</td><td><code>role="menu"</code>; actions are <code>menuitem</code>s; disabled actions expose <code>aria-disabled</code>.</td></tr>
|
|
|
|
|
<tr><td class="name">Keyboard <span data-uix-tag data-kind="soma">soma</span></td><td><span data-uix-kbd>Enter</span>/<span data-uix-kbd>Space</span>/<span data-uix-kbd>↑↓←→</span> open · <span data-uix-kbd>↑↓</span>/<span data-uix-kbd>←→</span> move (wrap) · <span data-uix-kbd>Home</span>/<span data-uix-kbd>End</span> jump · <span data-uix-kbd>Enter</span>/<span data-uix-kbd>Space</span> activate · <span data-uix-kbd>Esc</span> close.</td></tr>
|
|
|
|
|
<tr><td class="name">Dismiss</td><td>Outside-click + Tab-out close (non-modal); focus returns to the trigger.</td></tr>
|
|
|
|
|
<tr><td class="name">Motion</td><td>The fan stagger, the glyph rotation and the backdrop fade are suppressed under <span data-uix-kbd>prefers-reduced-motion</span>.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|