feat(t5): Bewegungs-Sprache mit Tokens, Stufen und Budget (T5a A11)
Backend Image / build-and-push (push) Successful in 11m29s
Frontend Image / build-and-push (push) Successful in 23m35s

- 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]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 21:15:42 +02:00
1 parent 7025a6b589
commit cda3e02a31
66 files changed
+2574 -773

No files matched your search

@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* T5 motion codemod (spec 17.2, 17.4, 17.7, Task A11). Pure and idempotent:
*
* - `motion-safe:` only asks the system and skips the level chosen in the app. Movement behind it
* (translate, scale, rotate, slide-, zoom-, animate-, transition-transform, also behind further
* variants) moves to `motion-full:`.
* - Colour and opacity transitions need no variant ("Aus" sets every duration to zero):
* `motion-safe:transition-colors`/`-opacity` lose the variant.
* - Pulsing and spinning are full motion: a bare `animate-pulse`/`animate-spin` gets
* `motion-full:`, and `motion-reduce:animate-none` (the old opt-out) goes.
*
* Hover lifts, `transition-all` and overlay timings are changed by hand (they need a judgement).
*
* CLI: node scripts/codemod-t5-motion.mjs [--check] <file or directory…>
* --check lists the files that would change and exits 1 if there are any.
*/
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Characters that belong to a class token (so a match never starts inside one). */
const TOKEN_CHAR = String.raw`[\w:/\[\]().%-]`;
/** Further variants between the motion variant and the utility (`data-open:`, `group-hover:`). */
const VARIANTS = String.raw`(?:[^\s"'\`:]+:)*`;
const MOVEMENT = String.raw`-?(?:translate|scale|rotate|slide-|zoom-|animate-|transition-transform)`;
const SAFE_MOVEMENT = new RegExp(String.raw`(?<!${TOKEN_CHAR})motion-safe:(?=${VARIANTS}${MOVEMENT})`, "g");
const SAFE_FADE = new RegExp(String.raw`(?<!${TOKEN_CHAR})motion-safe:(?=transition-(?:colors|opacity)(?!${TOKEN_CHAR}))`, "g");
const REDUCE_NONE = new RegExp(String.raw` ?(?<!${TOKEN_CHAR})motion-reduce:animate-none(?!${TOKEN_CHAR})`, "g");
const BARE_PULSE_SPIN = new RegExp(String.raw`(?<!${TOKEN_CHAR})animate-(pulse|spin)(?![\w-])`, "g");
/** Rewrites the motion classes of one source text. */
export function transform(text) {
return text
.replace(SAFE_MOVEMENT, "motion-full:")
.replace(SAFE_FADE, "")
.replace(REDUCE_NONE, "")
.replace(BARE_PULSE_SPIN, "motion-full:animate-$1");
}
function filesOf(path) {
if (statSync(path).isDirectory()) {
return readdirSync(path).flatMap((name) => filesOf(join(path, name)));
}
return /\.tsx?$/.test(path) && !/\.spec\./.test(path) ? [path] : [];
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2);
const check = args.includes("--check");
const changed = args
.filter((arg) => arg !== "--check")
.flatMap((arg) => filesOf(resolve(arg)))
.filter((file) => {
const before = readFileSync(file, "utf8");
const after = transform(before);
if (after === before) return false;
if (!check) writeFileSync(file, after);
return true;
});
for (const file of changed) console.log(file);
if (check && changed.length > 0) process.exit(1);
}