feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
|
|
|
|
|
|
|
|
import { validateEidosConfig } from './lib/config';
|
|
|
|
|
|
import { createEngineMotion } from '$motion';
|
|
|
|
|
|
import { BUILTIN_CSS_PRESETS, BUILTIN_KEYFRAMES } from './lib/motion/presets/css';
|
feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
import { renderStaticCss } from './lib/render-css';
|
|
|
|
|
|
import { createThemeBaseEidosConfig } from './lib/themes/base';
|
|
|
|
|
|
import type { EidosMotionPresets } from './lib/motion/registry';
|
|
|
|
|
|
|
|
|
|
|
|
describe('eidos motion — CSS generation', () => {
|
|
|
|
|
|
const css = renderStaticCss(createThemeBaseEidosConfig());
|
|
|
|
|
|
|
|
|
|
|
|
it('emits an @keyframes for every built-in keyframe', () => {
|
|
|
|
|
|
for (const name of Object.keys(BUILTIN_KEYFRAMES)) {
|
|
|
|
|
|
expect(css).toContain(`@keyframes ${name} {`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits enter (open) + exit (closed) rules per css preset, composing keyframes', () => {
|
|
|
|
|
|
expect(css).toContain(`[data-animation-style='scale-fade'][data-state='open']`);
|
|
|
|
|
|
expect(css).toContain(`[data-animation-style='scale-fade'][data-state='closed']`);
|
|
|
|
|
|
// composed keyframes under one animation; the duration is per-component
|
|
|
|
|
|
// overridable (`--motion-duration-{enter,exit}`) with the preset token as
|
|
|
|
|
|
// the fallback. Enter holds `backwards`, exit `forwards`.
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
'scale-in var(--motion-duration-enter, var(--duration-moderate)) var(--motion-ease-enter, var(--ease-out)) backwards'
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
'fade-in var(--motion-duration-enter, var(--duration-moderate)) var(--motion-ease-enter, var(--ease-out)) backwards'
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
'scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards'
|
|
|
|
|
|
);
|
|
|
|
|
|
// the duration + easing override custom props are registered (inherits:false)
|
|
|
|
|
|
expect(css).toContain('@property --motion-duration-enter');
|
|
|
|
|
|
expect(css).toContain('@property --motion-ease-exit');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits the stagger choreography hook on enter rules (Material list)', () => {
|
|
|
|
|
|
// per-item index registered as a non-inheriting integer
|
|
|
|
|
|
expect(css).toContain('@property --motion-stagger-index');
|
|
|
|
|
|
expect(css).toContain("syntax: '<integer>'");
|
|
|
|
|
|
// enter rules carry the index × each delay (default 0 → no stagger)
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
'animation-delay: calc(var(--motion-stagger-index, 0) * var(--motion-stagger-each, 0ms))'
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits the Material transition presets (shared-axis + fade-through)', () => {
|
|
|
|
|
|
expect(css).toContain(`[data-animation-style='shared-axis-x'][data-state='open']`);
|
|
|
|
|
|
expect(css).toContain(`[data-animation-style='shared-axis-y'][data-state='open']`);
|
|
|
|
|
|
expect(css).toContain(`[data-animation-style='fade-through'][data-state='open']`);
|
|
|
|
|
|
expect(css).toContain('@keyframes scale-through');
|
|
|
|
|
|
// shared-axis composes a directional slide with a fade
|
|
|
|
|
|
expect(css).toMatch(/shared-axis-x'\]\[data-state='open'\] \{\s*animation: slide-axis-x-in/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits side-aware variants for slide-fade (data-side)', () => {
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
`[data-animation-style='slide-fade'][data-side='top'][data-state='open']`
|
|
|
|
|
|
);
|
feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
`[data-animation-style='slide-fade'][data-side='bottom'][data-state='open']`
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
`[data-animation-style='slide-fade'][data-side='left'][data-state='open']`
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('honors reduced motion per policy (projected attr + media query)', () => {
|
|
|
|
|
|
// opacity-only preset drops transforms, keeps a fade
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
`[data-motion='reduce'] [data-animation-style='scale-fade'][data-state='open']`
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(css).toContain('@media (prefers-reduced-motion: reduce)');
|
|
|
|
|
|
// instant preset kills the animation
|
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
|
`[data-motion='reduce'] [data-animation-style='collapse'][data-state='open']`
|
|
|
|
|
|
);
|
feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
expect(css).toMatch(
|
|
|
|
|
|
/\[data-motion='reduce'\] \[data-animation-style='collapse'\]\[data-state='open'\] \{\s*animation: none !important;/
|
|
|
|
|
|
);
|
|
|
|
|
|
// 'none' presets (fade) get NO per-preset reduced override
|
|
|
|
|
|
expect(css).not.toContain(`[data-motion='reduce'] [data-animation-style='fade'][data-state`);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits event-moment signatures (data-event-*) from motion.signatures', () => {
|
|
|
|
|
|
const withSig = renderStaticCss({
|
|
|
|
|
|
...createThemeBaseEidosConfig(),
|
|
|
|
|
|
motion: {
|
|
|
|
|
|
keyframes: { 'fade-in': { from: { opacity: '0' }, to: { opacity: '1' } } },
|
|
|
|
|
|
signatures: {
|
|
|
|
|
|
'announce-threat': {
|
|
|
|
|
|
family: 'signal',
|
|
|
|
|
|
intent: 'threat',
|
|
|
|
|
|
keyframes: 'fade-in',
|
|
|
|
|
|
duration: 'slow'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(withSig).toContain(
|
|
|
|
|
|
`[data-event-family='signal'][data-event-intent='threat'][data-event-phase='active']`
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(withSig).toContain('fade-in var(--duration-slow)');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('eidos motion — runtime registry', () => {
|
|
|
|
|
|
const runtime = createEngineMotion(createThemeBaseEidosConfig().motion);
|
|
|
|
|
|
|
|
|
|
|
|
it('registers the built-in presets', () => {
|
|
|
|
|
|
for (const name of Object.keys(BUILTIN_CSS_PRESETS)) {
|
|
|
|
|
|
expect(runtime.has(name)).toBe(true);
|
|
|
|
|
|
expect(runtime.resolve(name)?.driver).toBe('css');
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(runtime.list()).toEqual(expect.arrayContaining(Object.keys(BUILTIN_CSS_PRESETS)));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('returns undefined for an unknown preset', () => {
|
|
|
|
|
|
expect(runtime.resolve('does-not-exist')).toBeUndefined();
|
|
|
|
|
|
expect(runtime.has('does-not-exist')).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('css presets resolve enter/exit to a settled handle (declarative path)', async () => {
|
|
|
|
|
|
// CSS presets never touch the element — soma's Presence drives them.
|
|
|
|
|
|
const el = {} as HTMLElement;
|
|
|
|
|
|
await expect(runtime.enter(el, 'scale-fade').finished).resolves.toBeUndefined();
|
|
|
|
|
|
await expect(runtime.exit(el, 'fade').finished).resolves.toBeUndefined();
|
|
|
|
|
|
// an unknown name is a no-op settled handle too
|
|
|
|
|
|
await expect(runtime.enter(el, 'nope').finished).resolves.toBeUndefined();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('register adds a preset', () => {
|
|
|
|
|
|
const local = createEngineMotion();
|
|
|
|
|
|
expect(local.has('fade')).toBe(false);
|
|
|
|
|
|
local.register('fade', BUILTIN_CSS_PRESETS.fade);
|
|
|
|
|
|
expect(local.resolve('fade')?.driver).toBe('css');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('eidos motion — validation', () => {
|
|
|
|
|
|
it('passes for the base config', () => {
|
|
|
|
|
|
expect(validateEidosConfig(createThemeBaseEidosConfig()).ok).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('flags a css preset referencing an unknown keyframe', () => {
|
|
|
|
|
|
const report = validateEidosConfig({
|
|
|
|
|
|
...createThemeBaseEidosConfig(),
|
|
|
|
|
|
motion: {
|
|
|
|
|
|
keyframes: { 'fade-in': { from: { opacity: '0' }, to: { opacity: '1' } } },
|
|
|
|
|
|
presets: {
|
|
|
|
|
|
broken: { driver: 'css', enter: { keyframes: 'no-such-keyframe' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(report.ok).toBe(false);
|
|
|
|
|
|
expect(report.issues.some((issue) => issue.path.startsWith('motion.presets.broken'))).toBe(
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("flags an 'opacity-only' preset missing the fade keyframes", () => {
|
|
|
|
|
|
const report = validateEidosConfig({
|
|
|
|
|
|
...createThemeBaseEidosConfig(),
|
|
|
|
|
|
motion: {
|
|
|
|
|
|
keyframes: { 'slide-in': { from: { translate: '0 4px' }, to: { translate: '0 0' } } },
|
|
|
|
|
|
presets: {
|
|
|
|
|
|
slide: {
|
|
|
|
|
|
driver: 'css',
|
|
|
|
|
|
enter: { keyframes: 'slide-in' },
|
|
|
|
|
|
reduce: 'opacity-only'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(report.ok).toBe(false);
|
|
|
|
|
|
expect(report.issues.some((issue) => issue.message.includes('fade-in'))).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('flags a signature referencing an unknown keyframe', () => {
|
|
|
|
|
|
const report = validateEidosConfig({
|
|
|
|
|
|
...createThemeBaseEidosConfig(),
|
|
|
|
|
|
motion: {
|
|
|
|
|
|
keyframes: { 'fade-in': { from: { opacity: '0' }, to: { opacity: '1' } } },
|
|
|
|
|
|
signatures: { bad: { family: 'commit', keyframes: 'no-such-kf' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(report.ok).toBe(false);
|
|
|
|
|
|
expect(report.issues.some((issue) => issue.path.startsWith('motion.signatures.bad'))).toBe(
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('eidos motion — F6 token rigor', () => {
|
|
|
|
|
|
const css = renderStaticCss(createThemeBaseEidosConfig());
|
|
|
|
|
|
|
|
|
|
|
|
it('emits the extended duration scale (perceptual holds for the firma)', () => {
|
|
|
|
|
|
expect(css).toContain('--duration-slower: 400ms;');
|
|
|
|
|
|
expect(css).toContain('--duration-deliberate: 600ms;');
|
|
|
|
|
|
expect(css).toContain('--duration-emphatic: 800ms;');
|
|
|
|
|
|
expect(css).toContain('--duration-sustained: 1000ms;');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits the emphasized easing + shared-axis (xl) + fade-through (through) tokens', () => {
|
|
|
|
|
|
expect(css).toContain('--ease-emphasized:');
|
|
|
|
|
|
expect(css).toContain('--motion-distance-xl: 30px;');
|
|
|
|
|
|
expect(css).toContain('--motion-scale-through: 0.92;');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('tokenizes the firma durations — announce pulses escalate by intent severity', () => {
|
|
|
|
|
|
expect(css).toContain('animation: announce-pulse-neutral var(--duration-deliberate)');
|
|
|
|
|
|
expect(css).toContain('animation: announce-pulse-risk var(--duration-emphatic)');
|
|
|
|
|
|
expect(css).toContain('animation: announce-pulse-threat var(--duration-sustained)');
|
|
|
|
|
|
// press / commit snap onto the existing scale (sub-perceptual)
|
|
|
|
|
|
expect(css).toContain('animation: press-squeeze var(--duration-fast)');
|
|
|
|
|
|
expect(css).toContain('animation: commit-settle var(--duration-moderate)');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('points the Material keyframes at the new tokens (no hardcoded 30px / 0.92)', () => {
|
|
|
|
|
|
expect(css).toContain('translate: var(--motion-distance-xl) 0');
|
|
|
|
|
|
expect(css).toContain('scale: var(--motion-scale-through)');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emits the expressive motion set (Carbon productive/expressive) as a scoped override', () => {
|
|
|
|
|
|
expect(css).toContain("[data-motion-set='expressive']");
|
|
|
|
|
|
// expressive remaps the easing curves; productive (`:root`) stays the default
|
|
|
|
|
|
expect(css).toMatch(/\[data-motion-set='expressive'\] \{[^}]*--ease-out:/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('does NOT remap expressive tokens that the set leaves to productive', () => {
|
|
|
|
|
|
// the base set only overrides default/out/in — `spring` is untouched
|
|
|
|
|
|
expect(css).not.toMatch(/\[data-motion-set='expressive'\] \{[^}]*--ease-spring:/);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('eidos motion — F7 typegen registry', () => {
|
|
|
|
|
|
it('declares every built-in CSS preset as a type-safe registry name', () => {
|
|
|
|
|
|
// `registered` is the closed built-in set; the `satisfies` makes each a
|
|
|
|
|
|
// `keyof EidosMotionPresets` — a TYPO fails compile (the registry's
|
|
|
|
|
|
// `(string & {})` escape is NOT in play here). The runtime check pins it to
|
|
|
|
|
|
// the ACTUAL presets, so adding a built-in without registering it fails too.
|
|
|
|
|
|
const registered = [
|
|
|
|
|
|
'fade',
|
|
|
|
|
|
'scale-fade',
|
|
|
|
|
|
'slide-fade',
|
|
|
|
|
|
'slide-full',
|
|
|
|
|
|
'collapse',
|
|
|
|
|
|
'shared-axis-x',
|
|
|
|
|
|
'shared-axis-y',
|
|
|
|
|
|
'fade-through'
|
|
|
|
|
|
] as const satisfies readonly (keyof EidosMotionPresets)[];
|
|
|
|
|
|
expect([...Object.keys(BUILTIN_CSS_PRESETS)].sort()).toEqual([...registered].sort());
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|