Files
ApiCore/ApiCore-Frontend/scripts/codemod-t5-motion.spec.mjs
T
Nick.RosenloecherandClaude Opus 5.5 cda3e02a31
Backend Image / build-and-push (push) Successful in 11m29s
Frontend Image / build-and-push (push) Successful in 23m35s
feat(t5): Bewegungs-Sprache mit Tokens, Stufen und Budget (T5a A11)
- src/lib/motion/tokens.ts und --motion-* in globals.css: Dauern,
  Kurven und Feder an einer Stelle; Beute-Bühne liest dieselben Werte.
- AppMotion: LazyMotion strict, Bewegungs-Code (domAnimation) als ein
  asynchroner Chunk, 28 kB gzip; check:motion-budget im Docker-Build
  neben dem 3D-Budget (Spec 17.6).
- PresenceList: abgehakte Zeilen blenden aus, die übrigen rücken nur bei
  „Voll“ nach; „Dezent“ blendet nur, „Aus“ sofort. Die Zeile geht nach
  der festen Verzögerung, auch wenn der Server schweigt (Spec 17.5);
  Fokus wandert zum nächsten Haken, auch ohne Bewegungs-Chunk.
- Overlays (Sheet, Dialog, Menü, Select) über Base-UI-Start/Ende-Stile
  mit Token-Zeiten; Verschiebung und Skalierung nur bei motion-full.
- Kein transition-all, kein Hover-Heben, keine Breiten-Animation von
  Daten; Skeleton erst nach 150 ms; Wächter-Regeln für alles davon.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 21:15:42 +02:00

48 lines
2.4 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { transform } from "./codemod-t5-motion.mjs";
describe("codemod t5 motion (spec 17.2, 17.4, 17.7)", () => {
it("puts movement behind motion-full: instead of motion-safe:", () => {
expect(transform('cn("size-4 motion-safe:transition-transform", open && "rotate-180")')).toBe(
'cn("size-4 motion-full:transition-transform", open && "rotate-180")',
);
expect(transform('"holo-stage bg-holo-bg motion-safe:animate-pulse"')).toBe('"holo-stage bg-holo-bg motion-full:animate-pulse"');
expect(transform('" motion-safe:animate-spin"')).toBe('" motion-full:animate-spin"');
expect(transform('"motion-safe:data-open:zoom-in-95 motion-safe:slide-in-from-top-2"')).toBe(
'"motion-full:data-open:zoom-in-95 motion-full:slide-in-from-top-2"',
);
expect(transform('"motion-safe:-translate-y-1 motion-safe:scale-95 motion-safe:rotate-90"')).toBe(
'"motion-full:-translate-y-1 motion-full:scale-95 motion-full:rotate-90"',
);
});
it("drops the variant from colour and opacity transitions (off sets their duration to zero)", () => {
expect(transform('"rounded-lg px-2 motion-safe:transition-colors hover:bg-muted"')).toBe(
'"rounded-lg px-2 transition-colors hover:bg-muted"',
);
expect(transform('"motion-safe:transition-opacity"')).toBe('"transition-opacity"');
});
it("gates bare pulse and spin behind full motion and drops motion-reduce:animate-none", () => {
expect(transform('"size-4 animate-spin motion-reduce:animate-none"')).toBe('"size-4 motion-full:animate-spin"');
expect(transform('"h-14 animate-pulse rounded-xl"')).toBe('"h-14 motion-full:animate-pulse rounded-xl"');
expect(transform('"size-4 animate-spin"')).toBe('"size-4 motion-full:animate-spin"');
});
it("leaves other animate classes, tokens and identifiers alone", () => {
const untouched = [
'"data-open:animate-in data-closed:animate-out"',
'"motion-full:animate-pulse"',
"const isAnimating = animate-spinner;",
'"animate-page-in"',
"// motion-safe: is forbidden for movement",
];
for (const text of untouched) expect(transform(text)).toBe(text);
});
it("is idempotent", () => {
const once = transform('"a motion-safe:transition-transform animate-spin motion-reduce:animate-none motion-safe:transition-colors"');
expect(transform(once)).toBe(once);
});
});