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,68 @@
// Bundle-Budget fuer `motion` im DOM (Spec T5 §17.6: hoechstens 30 kB gzip).
// Laeuft nach `next build` wie check-3d-budget.mjs; Exit 1, wenn die motion-Chunks
// zusammen das Budget ueberschreiten oder gar kein motion-Chunk gefunden wird.
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { gzipSync } from "node:zlib";
// Spec §17.6: hoechstens 30 kB gzip (1 kB = 1000 Byte) fuer motion insgesamt:
// den Teil in der App-Huelle und die asynchron geladenen Features.
export const BUDGET_BYTES = 30_000;
// String-Literale, die motion auch minifiziert behaelt und die kein App-Code schreibt:
// der Stil-Schluessel der Render-Basis, das Attribut von AnimatePresence (popLayout)
// und die Gesten-Props der Features.
export const MOTION_MARKERS = ["transformPerspective", "data-motion-pop-id", "whileHover"];
const THREE_MARKERS = ["WebGLRenderer", "__THREE__"];
export function isMotionChunk(source) {
if (THREE_MARKERS.every((marker) => source.includes(marker))) return false;
return MOTION_MARKERS.some((marker) => source.includes(marker));
}
/**
* Misst alle motion-Chunks unter chunksDir (gzip level 9) und zaehlt sie zusammen.
* @returns {{ ok: boolean, files: { file: string, gzipBytes: number }[], gzipBytes: number, budget: number, message: string }}
*/
export function checkMotionBudget(chunksDir, budget = BUDGET_BYTES) {
const files = [];
for (const name of readdirSync(chunksDir)) {
if (!name.endsWith(".js")) continue;
const source = readFileSync(path.join(chunksDir, name));
if (!isMotionChunk(source.toString("latin1"))) continue;
files.push({ file: name, gzipBytes: gzipSync(source, { level: 9 }).length });
}
const gzipBytes = files.reduce((sum, entry) => sum + entry.gzipBytes, 0);
if (files.length === 0) {
return {
ok: false,
files,
gzipBytes,
budget,
message: `Kein motion-Chunk in ${chunksDir} gefunden (Marker: ${MOTION_MARKERS.join(", ")}). Check kaputt oder Bundle umgebaut?`,
};
}
const ok = gzipBytes <= budget;
const list = files.map((entry) => `${entry.file} ${entry.gzipBytes}`).join(", ");
return {
ok,
files,
gzipBytes,
budget,
message: `motion: ${gzipBytes} Byte gzip in ${files.length} Chunks (${list}), Budget ${budget} Byte${ok ? "" : " UEBERSCHRITTEN"}`,
};
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const chunksDir = path.resolve(process.cwd(), ".next/static/chunks");
let result;
try {
result = checkMotionBudget(chunksDir);
} catch (error) {
console.error(`check-motion-budget: ${chunksDir} nicht lesbar (zuerst next build?): ${error.message}`);
process.exit(1);
}
(result.ok ? console.log : console.error)(result.message);
process.exit(result.ok ? 0 : 1);
}