|
|
<script lang="ts">
|
|
|
/**
|
|
|
* Sema panel — the perceptual layer as a first-class citizen, not a flat
|
|
|
* event table. Per event it shows the declarative shape (family / verb /
|
|
|
* sequence / intent / hold) AND the RESOLVED firma via `resolveSignature`:
|
|
|
* the sound primitives (pitch / gain / contour / envelope) + the haptic
|
|
|
* (kind / intensity / pattern) + the active channels. ▶ play emits the real
|
|
|
* signal on the live stage target, so sound is audible (Semantics → Sound
|
|
|
* must be on in the topbar).
|
|
|
*/
|
|
|
import { signatureFor } from './harness.svelte';
|
|
|
|
|
|
let {
|
|
|
actions,
|
|
|
boundIntent = 'neutral',
|
|
|
uix,
|
|
|
getTarget
|
|
|
}: {
|
|
|
actions: readonly { name: string; semantic: Record<string, unknown> }[];
|
|
|
boundIntent?: string;
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
uix: any;
|
|
|
getTarget: () => HTMLElement | null;
|
|
|
} = $props();
|
|
|
|
|
|
function effectiveIntent(semantic: Record<string, unknown>): string | undefined {
|
|
|
const decl = 'intent' in semantic ? (semantic.intent as unknown) : undefined;
|
|
|
if (typeof decl === 'string') return decl;
|
|
|
if (decl) return boundIntent; // fromProp binding → demo's current value
|
|
|
return undefined;
|
|
|
}
|
|
|
|
|
|
const CONTOUR_GLYPH: Record<string, string> = {
|
|
|
flat: '─',
|
|
|
arc: '⌒',
|
|
|
ascending: '╱',
|
|
|
descending: '╲'
|
|
|
};
|
|
|
|
|
|
function play(action: { name: string; semantic: Record<string, unknown> }) {
|
|
|
const target = getTarget();
|
|
|
if (!target) return;
|
|
|
const intent = effectiveIntent(action.semantic);
|
|
|
void uix.events?.emit({
|
|
|
name: action.name,
|
|
|
family: action.semantic.family,
|
|
|
target,
|
|
|
...(intent ? { intent } : {})
|
|
|
});
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
{#if actions.length === 0}
|
|
|
<p data-uix-section-desc>
|
|
|
This component's morfo declares <strong>no semantic events</strong>. That is only valid once
|
|
|
the component README justifies why it is perceptually passive (see the Morfo/Sema audit).
|
|
|
</p>
|
|
|
{:else}
|
|
|
<div data-uix-firma-grid>
|
|
|
{#each actions as action}
|
|
|
{@const sem = action.semantic}
|
|
|
{@const intent = effectiveIntent(sem)}
|
|
|
{@const sig = signatureFor(sem.family as string, intent)}
|
|
|
<div data-uix-firma-card>
|
|
|
<div data-uix-firma-head>
|
|
|
<span data-uix-firma-name>{action.name}</span>
|
|
|
<span data-uix-firma-tags>
|
|
|
<span data-uix-firma-tag>{sem.family}</span>
|
|
|
{#if sem.verb}<span data-uix-firma-tag>{sem.verb}</span>{/if}
|
|
|
<span data-uix-firma-tag>{(sem.sequence as string) ?? 'pre'}</span>
|
|
|
{#if intent}<span data-uix-firma-tag data-kind="intent">{intent}</span>{/if}
|
|
|
</span>
|
|
|
<button data-uix-play onclick={() => play(action)}>▶ play</button>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-firma-channels>
|
|
|
<span data-uix-firma-meta>
|
|
|
channels: {sig.activeChannels.length ? sig.activeChannels.join(' · ') : 'none'}
|
|
|
</span>
|
|
|
{#if sig.hold !== undefined}<span data-uix-firma-meta>hold: {sig.hold}ms</span>{/if}
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-firma-signals>
|
|
|
<!-- Sound -->
|
|
|
<div data-uix-firma-signal data-muted={sig.sound ? undefined : 'true'}>
|
|
|
<span data-uix-firma-signal-label>sound</span>
|
|
|
{#if sig.sound}
|
|
|
<div data-uix-firma-rows>
|
|
|
<span>pitch <b>{Math.round(sig.sound.pitch)}Hz</b></span>
|
|
|
<span>
|
|
|
gain
|
|
|
<span data-uix-firma-bar><span style="inline-size: {Math.min(1, sig.sound.gain) * 100}%"></span></span>
|
|
|
</span>
|
|
|
<span>contour <b data-uix-firma-contour>{CONTOUR_GLYPH[sig.sound.contour] ?? sig.sound.contour}</b> {sig.sound.contour}</span>
|
|
|
<span>centroid <b>{Math.round(sig.sound.centroid)}</b></span>
|
|
|
<span>roughness <b>{sig.sound.roughness}</b></span>
|
|
|
<span>env <b>{sig.sound.attack}/{sig.sound.decay}/{sig.sound.duration}ms</b></span>
|
|
|
</div>
|
|
|
{:else}
|
|
|
<span data-uix-firma-none>silent</span>
|
|
|
{/if}
|
|
|
</div>
|
|
|
|
|
|
<!-- Haptic -->
|
|
|
<div data-uix-firma-signal data-muted={sig.haptic ? undefined : 'true'}>
|
|
|
<span data-uix-firma-signal-label>haptic</span>
|
|
|
{#if sig.haptic}
|
|
|
<div data-uix-firma-rows>
|
|
|
<span>kind <b>{sig.haptic.kind}</b></span>
|
|
|
<span>
|
|
|
intensity
|
|
|
<span data-uix-firma-bar><span style="inline-size: {Math.min(1, sig.haptic.intensity ?? 0) * 100}%"></span></span>
|
|
|
</span>
|
|
|
{#if sig.haptic.pattern}
|
|
|
<span data-uix-firma-pattern>
|
|
|
{#each sig.haptic.pattern as ms, i}
|
|
|
<span data-on={i % 2 === 0 ? 'true' : undefined} style="inline-size: {Math.max(4, ms / 6)}px"></span>
|
|
|
{/each}
|
|
|
</span>
|
|
|
{:else if sig.haptic.duration}
|
|
|
<span>duration <b>{sig.haptic.duration}ms</b></span>
|
|
|
{/if}
|
|
|
</div>
|
|
|
{:else}
|
|
|
<span data-uix-firma-none>none</span>
|
|
|
{/if}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
{/each}
|
|
|
</div>
|
|
|
{/if}
|