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]>
This commit is contained in:
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);
|
||||
}
|
||||
Reference in new issue
Block a user