mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
refactor(theming): replace transition-all with specific transitions
Sweep 98 `transition-all` occurrences across 62 files and replace with targeted Tailwind transition utilities. Motivation: 1. `transition-all` animates every property, including CSS custom- property-backed colours. On first paint the vars may not have resolved yet, producing the P5 "white-on-white until first interaction" rendering bug. The same bug hit food/moodlit ListViews in the earlier theme migration. 2. Specific transitions also perform better — no layout-property interpolation overhead. Codemod scripts/migrate-transition-all.mjs classifies each class attribute by its sibling classes and picks one of: - `transition-opacity` — icon fade on group-hover - `transition-[width]` — progress-bar width anim - `transition-[transform,colors,box-shadow]` — scaled buttons/cards - `transition-[border-color,box-shadow]` — card hover:border+shadow - `transition-colors` — default (card/row hover) 91 / 98 auto-classified, 7 hand-migrated: - EntryItem → transition-[box-shadow] (ring fade) - NutritionProgressWidget → transition-[stroke-dashoffset,stroke] - OnboardingModal → transition-[width,background-color] - times/reports (3×) → transition-[width] / -[height] (bar anims) - presi/present → transition-[width,background-color] (dots) svelte-check clean with 0 errors; validate:all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
46c03e6a5b
commit
ea71d3c215
63 changed files with 285 additions and 102 deletions
175
scripts/migrate-transition-all.mjs
Normal file
175
scripts/migrate-transition-all.mjs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* One-shot codemod: replace `transition-all` with specific transitions
|
||||
* based on what the element actually animates (derived from sibling
|
||||
* hover:/focus:/group-hover:/active: classes and the element's layout
|
||||
* role).
|
||||
*
|
||||
* Why: `transition-all` animates *every* property, including custom-
|
||||
* property-backed colours. On first paint, some CSS custom properties
|
||||
* haven't resolved yet, producing the P5 "white-on-white until first
|
||||
* interaction" rendering bug. Specific transitions also perf better
|
||||
* (no layout-property interpolation).
|
||||
*
|
||||
* Strategy: this script parses each `class="..."` attribute that
|
||||
* contains `transition-all` and picks one of:
|
||||
*
|
||||
* - `transition-opacity`
|
||||
* When the element only changes opacity (icon fade on group-hover).
|
||||
*
|
||||
* - `transition-[width]`
|
||||
* Progress bars — the element has `h-full rounded-full` pattern.
|
||||
*
|
||||
* - `transition-[transform,colors,box-shadow]`
|
||||
* Scaled buttons / cards (`hover:scale-*` or `active:scale-*`).
|
||||
*
|
||||
* - `transition-[border-color,box-shadow]`
|
||||
* Cards with hover:border + hover:shadow (no colour/bg change).
|
||||
*
|
||||
* - `transition-colors`
|
||||
* Default for everything else (most card/row hover states).
|
||||
*
|
||||
* Ambiguous cases stay as `transition-all` — review the remaining list
|
||||
* with `rg transition-all` and convert by hand.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/migrate-transition-all.mjs [--dry-run]
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, '..');
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
const SCAN_GLOBS = ['apps/mana/apps/web/src/**/*.svelte'];
|
||||
|
||||
function listFiles() {
|
||||
const args = SCAN_GLOBS.map((g) => `"${g}"`).join(' ');
|
||||
const out = execSync(`git ls-files ${args}`, {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return out
|
||||
.split('\n')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Decide the replacement for a transition-all occurrence based on sibling classes. */
|
||||
function pickReplacement(classes) {
|
||||
const has = (p) => classes.some((c) => p.test(c));
|
||||
|
||||
const hasScale = has(/:scale-/) || has(/^scale-/);
|
||||
const hasOpacity = has(/opacity-\d/) || has(/:opacity-\d/);
|
||||
const hasHoverBg = has(/(?:hover|focus|active|group-hover):bg-/);
|
||||
const hasHoverBorder = has(/(?:hover|focus|active|group-hover):border-/);
|
||||
const hasHoverShadow = has(/(?:hover|focus|active|group-hover):shadow-/);
|
||||
const hasHoverText = has(/(?:hover|focus|active|group-hover):text-/);
|
||||
// Progress bars: `h-full rounded-full` without any interactive variant.
|
||||
const isProgressBar =
|
||||
classes.includes('h-full') &&
|
||||
classes.includes('rounded-full') &&
|
||||
!hasScale &&
|
||||
!hasHoverBg &&
|
||||
!hasHoverBorder;
|
||||
|
||||
if (isProgressBar) return 'transition-[width]';
|
||||
if (hasScale) return 'transition-[transform,colors,box-shadow]';
|
||||
// Pure opacity fade (icon reveal on hover).
|
||||
if (hasOpacity && !hasHoverBg && !hasHoverBorder && !hasHoverText && !hasHoverShadow) {
|
||||
return 'transition-opacity';
|
||||
}
|
||||
// Card with border + shadow dance, no colour change.
|
||||
if (hasHoverBorder && hasHoverShadow && !hasHoverBg && !hasHoverText) {
|
||||
return 'transition-[border-color,box-shadow]';
|
||||
}
|
||||
// Any colour-ish interactive change.
|
||||
if (hasHoverBg || hasHoverBorder || hasHoverText || hasHoverShadow) {
|
||||
return 'transition-colors';
|
||||
}
|
||||
// No signal — leave as-is so the human can decide.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk each class="..." attribute (including class={...} template strings)
|
||||
* containing `transition-all` and rewrite it in place. Skips cases where
|
||||
* no deterministic replacement is found.
|
||||
*/
|
||||
function migrateSource(src) {
|
||||
let changes = 0;
|
||||
let unresolved = 0;
|
||||
|
||||
// Match `class="..."` and `class={"..."}` constructs. Keep simple —
|
||||
// we'll bail out if the value looks too complex to tokenise.
|
||||
const classAttrRe = /class\s*=\s*(["'`])([\s\S]*?)\1/g;
|
||||
|
||||
const out = src.replace(classAttrRe, (full, quote, value) => {
|
||||
if (!value.includes('transition-all')) return full;
|
||||
|
||||
// Tokenise on whitespace — good enough for Svelte class attributes
|
||||
// that embed `{expr}` fragments; those stay opaque and we just
|
||||
// skip them as a single token, which is fine because we only read
|
||||
// known static classes.
|
||||
const classes = value
|
||||
.split(/\s+/)
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!classes.some((c) => c === 'transition-all')) {
|
||||
// `transition-all duration-300` etc. — remove the duration
|
||||
// handling and just match the token itself.
|
||||
return full;
|
||||
}
|
||||
|
||||
const replacement = pickReplacement(classes);
|
||||
if (!replacement) {
|
||||
unresolved++;
|
||||
return full;
|
||||
}
|
||||
|
||||
const newClasses = classes.map((c) => (c === 'transition-all' ? replacement : c));
|
||||
changes++;
|
||||
return `class=${quote}${newClasses.join(' ')}${quote}`;
|
||||
});
|
||||
|
||||
return { out, changes, unresolved };
|
||||
}
|
||||
|
||||
function migrate() {
|
||||
const paths = listFiles();
|
||||
let totalChanges = 0;
|
||||
let totalUnresolved = 0;
|
||||
let changedFiles = 0;
|
||||
|
||||
for (const rel of paths) {
|
||||
const abs = join(REPO_ROOT, rel);
|
||||
const src = readFileSync(abs, 'utf8');
|
||||
if (!src.includes('transition-all')) continue;
|
||||
|
||||
const { out, changes, unresolved } = migrateSource(src);
|
||||
totalChanges += changes;
|
||||
totalUnresolved += unresolved;
|
||||
|
||||
if (changes > 0) {
|
||||
changedFiles++;
|
||||
if (!DRY_RUN) writeFileSync(abs, out, 'utf8');
|
||||
console.log(` ${String(changes).padStart(3)} → (${unresolved} left) ${rel}`);
|
||||
} else if (unresolved > 0) {
|
||||
console.log(` ${'·'.padStart(3)} (${unresolved} left) ${rel}`);
|
||||
}
|
||||
}
|
||||
|
||||
const verb = DRY_RUN ? 'Would migrate' : 'Migrated';
|
||||
console.log(
|
||||
`\n${verb} ${totalChanges} transition-all → specific, ` +
|
||||
`${totalUnresolved} left ambiguous across ${changedFiles} file(s).`
|
||||
);
|
||||
if (DRY_RUN) console.log('Run without --dry-run to apply.');
|
||||
}
|
||||
|
||||
migrate();
|
||||
Loading…
Add table
Add a link
Reference in a new issue