feat(motion): pilot the universal `motion` prop on a real component (Badge) — (b) step 2

First real consumer of `motionAttrs`: `<Badge>` gains a `motion` prop so a
designer can apply any registered preset as the badge's entrance — `<Badge
motion="scale-fade">` plays the preset's enter on mount, zero per-component
machinery (the component just spreads `{...motionAttrs(motion)}`).

Badge is the clean content-domain pilot: pure eidos primitive, no morfo events,
no own `data-state` (so the helper's presentation `data-state="open"` doesn't
clash — unlike Card, which has selected/idle + a built-in emerge). Additive +
non-breaking: `motion` defaults to undefined (no animation).

- badge/types.ts: `motion?: MotionPresetName`.
- badge/badge.svelte: spread `motionAttrs(motion)` on the span.
- badge demo: a "Motion" showcase (fade / scale-fade / slide-fade + Replay) —
  the prop demonstrated per the testbed doctrine.

Verified at runtime: the three motion badges render `data-animation-style` +
`data-state="open"` with the right enter `animation-name` (fade-in / scale-in,
fade-in / slide-from-bottom, scale-in, fade-in). `check` clean for badge. (A
fresh badge screenshot was blocked by preview-infra flakiness this round; the
enter keyframes themselves were confirmed visually earlier via the cascade.)

Establishes the pattern: any component becomes animation-selectable by accepting
`motion` + spreading the helper. RFC §D.12 (b).

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

@ -12,7 +12,7 @@
* Optional Dot / Icon / Remove sub-parts compose inside.
*/
import './badge.css';
import { ActiveEidos } from '$uix/eidos';
import { ActiveEidos, motionAttrs } from '$uix/eidos';
import { BADGE_LANGS } from './langs';
import type { BadgeProps } from './types';
@ -27,6 +27,7 @@
onRemove,
removeLabel,
icon,
motion,
children,
...rest
}: BadgeProps = $props();
@ -49,6 +50,7 @@
data-shape={shape}
data-dot={dot ? '' : undefined}
data-removable={removable ? '' : undefined}
{...motionAttrs(motion)}
{...rest}
>
{#if dot}

@ -1,5 +1,6 @@
import type { Snippet } from 'svelte';
import type { ChipVariant, ColorRole, ResponsiveProp, ShapeFamily, Size } from '$uix/eidos/lib/types';
import type { MotionPresetName } from '$uix/eidos/lib/motion/registry';
import type { BoxProps } from '../box/types';
/**
@ -78,4 +79,11 @@ export type BadgeProps = Omit<BoxProps, 'display'> & {
icon?: Snippet;
/** Badge label. */
children?: Snippet;
/**
* Entrance animation — any registered motion preset (`fade` / `scale-fade` /
* `slide-fade` / …). The badge plays the preset's enter on mount (content
* domain — RFC §D.12; routed via `motionAttrs`). `undefined` (default) = no
* animation. A common use: a notification badge that pops in.
*/
motion?: MotionPresetName;
};

@ -30,6 +30,8 @@
let withIcon = $state(false);
let removable = $state(false);
let removeCount = $state(0);
// Remount key so the on-mount entrance animation can be replayed on demand.
let motionReplay = $state(0);
const variants: BadgeVariant[] = ['soft', 'solid', 'outline', 'ghost'];
const sizes: BadgeSize[] = ['xs', 'sm', 'md', 'lg'];
@ -770,6 +772,31 @@
</Badge>
<Badge color="affirm" dot removable onRemove={handleRemove}>dot + remove</Badge>
</Box>
<div data-uix-subsection-head>Motion · entrance animation (content domain · RFC §D.12)</div>
<Box
style="display: flex; flex-direction: column; gap: var(--uix-space-3); padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<p style="margin: 0; color: var(--uix-muted);">
Any component can opt into a registered motion preset via the <code>motion</code> prop —
the badge plays its enter on mount (routed through <code>motionAttrs</code>, reusing the
existing preset, no per-component machinery). <code>undefined</code> = no animation.
</p>
<button
type="button"
onclick={() => (motionReplay += 1)}
style="align-self: flex-start; padding: var(--uix-space-1) var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-sm); background: var(--uix-surface); cursor: pointer;"
>
Replay ↻
</button>
{#key motionReplay}
<Box style="display: flex; flex-wrap: wrap; gap: var(--uix-space-3);">
<Badge color="primary" motion="fade">motion=&quot;fade&quot;</Badge>
<Badge color="affirm" motion="scale-fade">motion=&quot;scale-fade&quot;</Badge>
<Badge color="risk" motion="slide-fade">motion=&quot;slide-fade&quot;</Badge>
</Box>
{/key}
</Box>
</section>
{/if}

Loading…
Cancel
Save

Powered by TurnKey Linux.