- 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]>
65 lines
2.9 KiB
JavaScript
65 lines
2.9 KiB
JavaScript
#!/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);
|
|
}
|