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:
Till JS 2026-04-22 15:57:49 +02:00
parent 46c03e6a5b
commit ea71d3c215
63 changed files with 285 additions and 102 deletions

View file

@ -64,7 +64,7 @@
{#each buttons as row}
{#each row as btn}
<button
class="h-14 rounded-xl border border-border transition-all active:scale-95 text-lg {getButtonClass(
class="h-14 rounded-xl border border-border transition-[transform,colors,box-shadow] active:scale-95 text-lg {getButtonClass(
btn
)}"
onclick={() => handleButton(btn)}
@ -76,7 +76,7 @@
</div>
<button
class="mt-2 w-full h-10 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted transition-all text-sm"
class="mt-2 w-full h-10 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted transition-colors text-sm"
onclick={onBackspace}
>
← Löschen

View file

@ -137,7 +137,7 @@
stroke-linecap="round"
stroke-dasharray={CIRCUMFERENCE}
stroke-dashoffset={strokeOffset}
class="transition-all duration-500"
class="transition-[stroke-dashoffset,stroke] duration-500"
/>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">

View file

@ -55,7 +55,7 @@
<button
type="button"
class="mood-card group relative w-full overflow-hidden rounded-2xl transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
class="mood-card group relative w-full overflow-hidden rounded-2xl transition-[transform,colors,box-shadow] duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
class:ring-2={isActive}
class:ring-primary={isActive}
onclick={handleClick}

View file

@ -235,7 +235,7 @@
<div class="flex-1 flex items-center justify-center pointer-events-auto">
<button
type="button"
class="p-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-all hover:scale-110"
class="p-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-[transform,colors,box-shadow] hover:scale-110"
onclick={(e) => {
e.stopPropagation();
togglePlay();

View file

@ -45,7 +45,7 @@
</script>
<div
class="flex cursor-pointer flex-col items-center rounded-xl border-2 border-dashed p-12 text-center transition-all {dragActive
class="flex cursor-pointer flex-col items-center rounded-xl border-2 border-dashed p-12 text-center transition-colors {dragActive
? 'border-primary bg-primary/5 border-solid'
: 'border-border bg-background-card hover:border-primary'}"
ondragover={handleDragOver}

View file

@ -18,7 +18,7 @@
</script>
<div
class="relative rounded-xl border p-4 transition-all duration-200 {achievement.unlocked
class="relative rounded-xl border p-4 transition-colors duration-200 {achievement.unlocked
? `${rarity.bgColor} ${rarity.borderColor}`
: 'border-border/50 bg-card/30'} {achievement.unlocked
? 'hover:-translate-y-0.5 hover:shadow-lg'
@ -69,7 +69,7 @@
</div>
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-gradient-to-r from-gray-500 to-gray-400 transition-all duration-300"
class="h-full rounded-full bg-gradient-to-r from-gray-500 to-gray-400 transition-[width] duration-300"
style="width: {progressPercent}%"
></div>
</div>

View file

@ -100,14 +100,14 @@
</button>
<button
onclick={onEdit}
class="rounded-lg bg-muted/20 p-2 text-muted-foreground opacity-0 transition-all hover:bg-muted/30 hover:text-white group-hover:opacity-100"
class="rounded-lg bg-muted/20 p-2 text-muted-foreground opacity-0 transition-colors hover:bg-muted/30 hover:text-white group-hover:opacity-100"
title={$_('common.edit')}
>
<PencilSimple class="h-4 w-4" />
</button>
<button
onclick={onDelete}
class="rounded-lg bg-red-600/20 p-2 text-red-400 opacity-0 transition-all hover:bg-red-600/30 group-hover:opacity-100"
class="rounded-lg bg-red-600/20 p-2 text-red-400 opacity-0 transition-colors hover:bg-red-600/30 group-hover:opacity-100"
title={$_('common.delete')}
>
<Trash class="h-4 w-4" />

View file

@ -107,7 +107,7 @@
</script>
<div
class="entry-item rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] transition-all {isExpanded
class="entry-item rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] transition-[box-shadow] {isExpanded
? 'ring-1 ring-[hsl(var(--color-primary)/0.3)]'
: ''}"
>

View file

@ -156,7 +156,7 @@
<!-- Start/Stop Button -->
<button
onclick={handleStartStop}
class="w-full rounded-xl py-3 text-lg font-medium transition-all {timerStore.isRunning
class="w-full rounded-xl py-3 text-lg font-medium transition-colors {timerStore.isRunning
? 'bg-red-500 text-white hover:bg-red-600'
: 'bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))] hover:opacity-90'}"
>

View file

@ -61,7 +61,7 @@
cy={y}
r={isSelected ? 8 : 5}
fill={isSelected ? 'hsl(var(--color-primary))' : 'hsl(var(--color-muted-foreground))'}
class="cursor-pointer hover:opacity-80 transition-all"
class="cursor-pointer hover:opacity-80 transition-opacity"
/>
{#if isSelected}
<text

View file

@ -75,7 +75,7 @@
<div class="mb-6 flex gap-1.5">
{#each steps as _, i}
<div
class="h-1.5 rounded-full transition-all {i === step
class="h-1.5 rounded-full transition-[width,background-color] {i === step
? 'w-6 bg-primary'
: i < step
? 'w-1.5 bg-primary/40'

View file

@ -37,7 +37,7 @@
onkeydown={(e) => e.key === 'Enter' && onOpenTask(task)}
role="button"
tabindex="0"
class="group flex cursor-pointer items-center gap-4 rounded-xl border border-border bg-card p-4 transition-all hover:shadow-md"
class="group flex cursor-pointer items-center gap-4 rounded-xl border border-border bg-card p-4 transition-colors hover:shadow-md"
style="border-left: 4px solid {getPriorityColor(task.priority)}"
>
<button

View file

@ -98,7 +98,7 @@
{#each quickLinks as link}
<a
href={link.href}
class="rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-lg group"
class="rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-lg group"
>
<div class="flex flex-col items-center gap-3 text-center">
<div

View file

@ -157,8 +157,7 @@
<!-- Skin picker toggle -->
<div class="mb-3 flex items-center justify-between">
<button
class="text-xs rounded-full border px-3 py-1.5 transition-all
{showSkinPicker
class="text-xs rounded-full border px-3 py-1.5 transition-colors {showSkinPicker
? 'border-pink-500 bg-pink-500 text-white'
: 'border-border bg-card text-muted-foreground hover:bg-muted'}"
onclick={() => (showSkinPicker = !showSkinPicker)}
@ -177,8 +176,9 @@
<div class="grid grid-cols-5 gap-2">
{#each CALCULATOR_SKINS as skin}
<button
class="rounded-lg border p-2 text-center transition-all
{activeSkin === skin.id ? 'border-pink-500 bg-pink-500/10' : 'border-transparent hover:bg-muted'}"
class="rounded-lg border p-2 text-center transition-colors {activeSkin === skin.id
? 'border-pink-500 bg-pink-500/10'
: 'border-transparent hover:bg-muted'}"
onclick={() => setSkin(skin.id)}
>
<div class="text-sm font-medium text-foreground">{skin.label}</div>

View file

@ -56,7 +56,7 @@
{#each quickLinks as link}
<a
href={link.href}
class="rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-lg group"
class="rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-lg group"
>
<div class="flex flex-col items-center gap-3 text-center">
<div

View file

@ -133,7 +133,7 @@
tabindex="0"
onclick={() => handleConversationClick(conv.id)}
onkeydown={(e) => e.key === 'Enter' && handleConversationClick(conv.id)}
class="group flex items-center gap-3 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group flex items-center gap-3 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<ChatCircle size={20} class="shrink-0 text-[hsl(var(--color-primary))]" />
<div class="min-w-0 flex-1">
@ -185,7 +185,7 @@
tabindex="0"
onclick={() => handleConversationClick(conv.id)}
onkeydown={(e) => e.key === 'Enter' && handleConversationClick(conv.id)}
class="group flex items-center gap-3 rounded-lg border border-transparent p-3 transition-all hover:border-[hsl(var(--color-border))] hover:bg-[hsl(var(--color-card))]"
class="group flex items-center gap-3 rounded-lg border border-transparent p-3 transition-colors hover:border-[hsl(var(--color-border))] hover:bg-[hsl(var(--color-card))]"
>
<ChatCircle size={20} class="shrink-0 text-[hsl(var(--color-muted-foreground))]" />
<div class="min-w-0 flex-1">

View file

@ -66,7 +66,7 @@
tabindex="0"
onclick={() => handleClick(conv.id)}
onkeydown={(e) => e.key === 'Enter' && handleClick(conv.id)}
class="group flex items-center gap-3 rounded-lg border border-transparent p-3 transition-all hover:border-[hsl(var(--color-border))] hover:bg-[hsl(var(--color-card))]"
class="group flex items-center gap-3 rounded-lg border border-transparent p-3 transition-colors hover:border-[hsl(var(--color-border))] hover:bg-[hsl(var(--color-card))]"
>
<ChatCircle size={20} class="shrink-0 text-[hsl(var(--color-muted-foreground))]" />
<div class="min-w-0 flex-1">

View file

@ -153,7 +153,7 @@
<div class="grid gap-4 sm:grid-cols-2">
{#each templates as template (template.id)}
<div
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-5 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-5 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">

View file

@ -35,7 +35,7 @@
{#if authStore.isAuthenticated}
<a
href="/citycorners/add-city"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-all hover:bg-primary/90 hover:shadow-lg"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-colors hover:bg-primary/90 hover:shadow-lg"
title={$_('cities.add')}
>
<Plus size={20} weight="bold" />

View file

@ -76,7 +76,7 @@
</div>
<a
href="/citycorners/cities/{citySlug}/add"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-all hover:bg-primary/90 hover:shadow-lg"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-colors hover:bg-primary/90 hover:shadow-lg"
title={$_('add.title')}
>
<Plus size={20} weight="bold" />
@ -217,7 +217,7 @@
{#if authStore.isAuthenticated}
<button
class="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-all hover:bg-black/50"
class="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-colors hover:bg-black/50"
onclick={(e) => handleFavoriteToggle(e, location.id)}
title={favoriteIds.has(location.id) ? $_('favorites.remove') : $_('favorites.add')}
>

View file

@ -226,7 +226,7 @@
<!-- Share + Favorite buttons overlay -->
<div class="absolute right-4 top-4 flex gap-2">
<button
class="flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition-all hover:bg-black/50"
class="flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition-colors hover:bg-black/50"
onclick={handleShare}
title={$_('detail.share')}
>
@ -239,7 +239,7 @@
{#if authStore.isAuthenticated}
<button
class="flex h-10 w-10 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-all hover:bg-black/50"
class="flex h-10 w-10 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-colors hover:bg-black/50"
onclick={() => favoritesStore.toggle(location!.id)}
title={favoriteIds.has(location.id) ? $_('favorites.remove') : $_('favorites.add')}
>

View file

@ -54,7 +54,7 @@
href="https://www.openstreetmap.org/#map=14/{lat}/{lng}"
target="_blank"
rel="noopener noreferrer"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-background-card border border-border text-foreground-secondary shadow-sm transition-all hover:text-primary hover:border-primary"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-background-card border border-border text-foreground-secondary shadow-sm transition-colors hover:text-primary hover:border-primary"
title={$_('map.openInMaps')}
>
<GlobeSimple size={20} />

View file

@ -94,7 +94,7 @@
{#each pinnedSpaces as space}
<a
href="/context/spaces/{space.id}"
class="rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
class="rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-center gap-3">
<span
@ -128,7 +128,7 @@
{#each recentDocs as doc}
<a
href="/context/documents/{doc.id}"
class="group rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">

View file

@ -155,7 +155,7 @@
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{#each filteredDocuments as doc (doc.id)}
<div
class="group rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-start justify-between">
<a href="/context/documents/{doc.id}" class="min-w-0 flex-1">

View file

@ -150,7 +150,7 @@
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{#each filteredSpaces as space (space.id)}
<div
class="group rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-start justify-between">
<a href="/context/spaces/{space.id}" class="min-w-0 flex-1">

View file

@ -212,7 +212,7 @@
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{#each filteredDocuments as doc (doc.id)}
<div
class="group rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-start justify-between">
<a href="/context/documents/{doc.id}" class="min-w-0 flex-1">

View file

@ -89,7 +89,7 @@
</p>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {getProgressColor(
class="h-full rounded-full transition-[width] {getProgressColor(
progress.calories.percentage
)}"
style="width: {Math.min(progress.calories.percentage, 100)}%"
@ -116,7 +116,9 @@
</p>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {getProgressColor(progress.protein.percentage)}"
class="h-full rounded-full transition-[width] {getProgressColor(
progress.protein.percentage
)}"
style="width: {Math.min(progress.protein.percentage, 100)}%"
></div>
</div>
@ -143,7 +145,9 @@
</p>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {getProgressColor(progress.carbs.percentage)}"
class="h-full rounded-full transition-[width] {getProgressColor(
progress.carbs.percentage
)}"
style="width: {Math.min(progress.carbs.percentage, 100)}%"
></div>
</div>
@ -165,7 +169,7 @@
</p>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {getProgressColor(progress.fat.percentage)}"
class="h-full rounded-full transition-[width] {getProgressColor(progress.fat.percentage)}"
style="width: {Math.min(progress.fat.percentage, 100)}%"
></div>
</div>
@ -204,7 +208,7 @@
{#each todaysMeals as meal (meal.id)}
<a
href="/food/{meal.id}"
class="block rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="block rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start gap-3">
{#if meal.photoThumbnailUrl || meal.photoUrl}
@ -264,14 +268,14 @@
<div class="flex gap-3">
<a
href="/food/goals"
class="flex-1 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-center transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="flex-1 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-center transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<span class="text-2xl">🎯</span>
<p class="mt-1 text-sm font-medium text-[hsl(var(--color-foreground))]">Ziele</p>
</a>
<a
href="/food/history"
class="flex-1 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-center transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="flex-1 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-center transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<span class="text-2xl">📊</span>
<p class="mt-1 text-sm font-medium text-[hsl(var(--color-foreground))]">Verlauf</p>

View file

@ -337,8 +337,8 @@
<button
type="button"
onclick={() => (editMealType = type)}
class="rounded-lg border-2 px-3 py-2 text-sm transition-all
{editMealType === type
class="rounded-lg border-2 px-3 py-2 text-sm transition-colors {editMealType ===
type
? 'border-[hsl(var(--color-primary))] bg-[hsl(var(--color-primary)/0.05)] font-medium'
: 'border-[hsl(var(--color-border))] hover:border-[hsl(var(--color-primary)/0.3)]'}"
>

View file

@ -424,8 +424,7 @@
<button
type="button"
onclick={() => (mealType = type)}
class="rounded-lg border-2 px-3 py-2 text-sm transition-all
{mealType === type
class="rounded-lg border-2 px-3 py-2 text-sm transition-colors {mealType === type
? 'border-[hsl(var(--color-primary))] bg-[hsl(var(--color-primary)/0.05)] font-medium'
: 'border-[hsl(var(--color-border))] hover:border-[hsl(var(--color-primary)/0.3)]'}"
>

View file

@ -72,7 +72,7 @@
{#each filteredGuides as guide}
{@const cat = GUIDE_CATEGORIES[guide.category]}
<div
class="group flex items-center gap-4 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex items-center gap-4 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
>
<div
class="{cat.color} flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-white"

View file

@ -85,7 +85,7 @@
tabindex="0"
onclick={() => handleCollectionClick(collection)}
onkeydown={(e) => e.key === 'Enter' && handleCollectionClick(collection)}
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-5 text-left transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-5 text-left transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">

View file

@ -66,7 +66,7 @@
{#each DEFAULT_TEMPLATES as template}
<button
onclick={() => selectTemplate(template)}
class="rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-left transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 text-left transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-center gap-3">
<span class="text-2xl">{template.icon}</span>

View file

@ -620,7 +620,7 @@
{#if progress !== null}
<div class="mt-3 h-2 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-all duration-300"
class="h-full rounded-full bg-primary transition-[width] duration-300"
style="width: {Math.round(progress * 100)}%"
></div>
</div>
@ -1110,7 +1110,7 @@
</div>
<div class="h-2 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-all duration-300"
class="h-full rounded-full bg-primary transition-[width] duration-300"
style="width: {Math.round((benchmarkCurrentRun / benchmarkIterations) * 100)}%"
></div>
</div>

View file

@ -191,7 +191,7 @@
tabindex="0"
onclick={() => handleMemoClick(memo.id)}
onkeydown={(e) => e.key === 'Enter' && handleMemoClick(memo.id)}
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">

View file

@ -66,7 +66,7 @@
tabindex="0"
onclick={() => handleClick(memo.id)}
onkeydown={(e) => e.key === 'Enter' && handleClick(memo.id)}
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">

View file

@ -48,7 +48,7 @@
{#each quickLinks as link}
<a
href={link.href}
class="rounded-xl border border-border bg-card p-6 transition-all hover:border-primary/50 hover:shadow-lg group"
class="rounded-xl border border-border bg-card p-6 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-lg group"
>
<div class="flex flex-col items-center gap-3 text-center">
<div

View file

@ -165,7 +165,7 @@
: `linear-gradient(135deg, ${mood.colors.join(', ')})`}
<button
onclick={() => (fullscreenMood = mood)}
class="mood-card group relative aspect-[16/10] w-full overflow-hidden rounded-2xl border-[3px] border-transparent transition-all duration-200 hover:border-border/40 focus:outline-none focus:ring-2 focus:ring-primary/50"
class="mood-card group relative aspect-[16/10] w-full overflow-hidden rounded-2xl border-[3px] border-transparent transition-colors duration-200 hover:border-border/40 focus:outline-none focus:ring-2 focus:ring-primary/50"
style="--mood-color: {mood.colors[0]}"
>
<div
@ -197,7 +197,7 @@
deleteMood(mood);
}
}}
class="absolute right-2 top-2 rounded-full bg-black/20 p-1.5 text-foreground opacity-0 backdrop-blur-sm transition-all hover:bg-black/40 hover:text-white group-hover:opacity-100 cursor-pointer"
class="absolute right-2 top-2 rounded-full bg-black/20 p-1.5 text-foreground opacity-0 backdrop-blur-sm transition-colors hover:bg-black/40 hover:text-white group-hover:opacity-100 cursor-pointer"
>
<X size={14} />
</div>

View file

@ -116,7 +116,7 @@
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{#each projectsCtx.value.slice(0, 6) as project (project.id)}
<div
class="rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<h3 class="font-medium text-[hsl(var(--color-foreground))]">{project.title}</h3>
{#if project.description}

View file

@ -84,7 +84,7 @@
{#each playlistsCtx.value as playlist (playlist.id)}
<a
href="/music/playlists/{playlist.id}"
class="group relative rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group relative rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div
class="mb-3 flex aspect-square items-center justify-center overflow-hidden rounded-lg bg-[hsl(var(--color-muted))]"

View file

@ -87,7 +87,7 @@
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{#each projectsCtx.value as project (project.id)}
<div
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="group rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start justify-between">
<h3 class="font-medium text-[hsl(var(--color-foreground))]">{project.title}</h3>

View file

@ -193,7 +193,7 @@
{#each filteredImages as img (img.id)}
<button
onclick={() => (selectedImage = img)}
class="group relative overflow-hidden rounded-lg border border-border bg-card transition-all hover:shadow-lg hover:border-primary/50"
class="group relative overflow-hidden rounded-lg border border-border bg-card transition-[border-color,box-shadow] hover:shadow-lg hover:border-primary/50"
>
{#if img.publicUrl}
<img

View file

@ -93,7 +93,7 @@
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{#each allBoards.value as board (board.id)}
<div
class="group relative overflow-hidden rounded-lg border border-border bg-card transition-all hover:shadow-lg hover:border-primary/50"
class="group relative overflow-hidden rounded-lg border border-border bg-card transition-[border-color,box-shadow] hover:shadow-lg hover:border-primary/50"
>
<!-- Thumbnail -->
<button

View file

@ -194,7 +194,7 @@
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
onclick={() => goToSlide(index)}
class="w-2 h-2 rounded-full transition-all"
class="w-2 h-2 rounded-full transition-[width,background-color]"
class:bg-primary-500={index === currentSlideIndex}
class:w-4={index === currentSlideIndex}
class:bg-muted={index !== currentSlideIndex}

View file

@ -155,7 +155,7 @@
<a
href="/questions/{question.id}"
class="block rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="block rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<div class="flex items-start gap-4">
<div class="mt-1">

View file

@ -141,7 +141,7 @@
<div class="space-y-3">
{#each collections as collection (collection.id)}
<div
class="flex items-center gap-4 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-all hover:border-[hsl(var(--color-primary)/0.3)]"
class="flex items-center gap-4 rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4 transition-colors hover:border-[hsl(var(--color-primary)/0.3)]"
>
<!-- Icon & Color -->
<div

View file

@ -215,7 +215,8 @@
<button
type="button"
onclick={() => (researchDepth = option.value)}
class="rounded-lg border-2 p-4 text-left transition-all {researchDepth === option.value
class="rounded-lg border-2 p-4 text-left transition-colors {researchDepth ===
option.value
? 'border-[hsl(var(--color-primary))] bg-[hsl(var(--color-primary)/0.05)]'
: 'border-[hsl(var(--color-border))] hover:border-[hsl(var(--color-primary)/0.3)]'}"
>

View file

@ -31,7 +31,11 @@
<!-- Daily Quote Card -->
{#if quotesStore.currentQuote}
<div class="mb-8 transition-all duration-300 {isRefreshing ? 'opacity-50 scale-95' : ''}">
<div
class="mb-8 transition-[transform,colors,box-shadow] duration-300 {isRefreshing
? 'opacity-50 scale-95'
: ''}"
>
<QuoteCard
quote={quotesStore.currentQuote}
size="large"

View file

@ -106,7 +106,7 @@
{#each allLists.value as list (list.id)}
<a
href="/quotes/lists/{list.id}"
class="block p-6 bg-surface-elevated rounded-2xl hover:shadow-lg transition-all group"
class="block p-6 bg-surface-elevated rounded-2xl hover:shadow-lg transition-colors group"
>
<div class="flex items-start justify-between">
<div>

View file

@ -78,7 +78,7 @@
</div>
<div class="h-3 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-gradient-to-r from-yellow-500 to-yellow-400 transition-all duration-500"
class="h-full rounded-full bg-gradient-to-r from-yellow-500 to-yellow-400 transition-[width] duration-500"
style="width: {completion}%"
></div>
</div>

View file

@ -68,7 +68,7 @@
{#each quickLinks as link}
<a
href={link.href}
class="rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-lg group"
class="rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-lg group"
>
<div class="flex flex-col items-center gap-3 text-center">
<div

View file

@ -81,7 +81,7 @@
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each favoriteFolders as folder (folder.id)}
<button
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
onclick={() => handleFolderClick(folder)}
>
<div
@ -98,7 +98,7 @@
{/each}
{#each favoriteFiles as file (file.id)}
<div
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-2xl">
{#if file.mimeType.startsWith('image/')}📷

View file

@ -159,7 +159,7 @@
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each folders as folder (folder.id)}
<button
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
onclick={() => handleFolderClick(folder)}
>
<div
@ -178,7 +178,7 @@
{/each}
{#each files as file (file.id)}
<div
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-2xl">
{#if file.mimeType.startsWith('image/')}📷

View file

@ -170,7 +170,7 @@
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each folders as folder (folder.id)}
<button
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
onclick={() => handleFolderClick(folder)}
>
<div
@ -186,7 +186,7 @@
{/each}
{#each files as file (file.id)}
<div
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-2xl">
{#if file.mimeType.startsWith('image/')}📷

View file

@ -122,7 +122,7 @@
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each results.folders as folder (folder.id)}
<button
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
onclick={() => handleFolderClick(folder)}
>
<div
@ -137,7 +137,7 @@
{/each}
{#each results.files as file (file.id)}
<div
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
class="group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-md"
>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-2xl">
{#if file.mimeType.startsWith('image/')}📷

View file

@ -74,7 +74,7 @@
{#each quickLinks as link}
<a
href={link.href}
class="rounded-xl border border-border bg-card p-4 transition-all hover:border-primary/50 hover:shadow-lg group"
class="rounded-xl border border-border bg-card p-4 transition-[border-color,box-shadow] hover:border-primary/50 hover:shadow-lg group"
>
<div class="flex flex-col items-center gap-3 text-center">
<div

View file

@ -311,7 +311,7 @@
</div>
<div class="mt-1 h-1.5 rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {budgetPct > 90
class="h-full rounded-full transition-[width] {budgetPct > 90
? 'bg-red-500'
: budgetPct > 75
? 'bg-amber-500'

View file

@ -315,7 +315,7 @@
</div>
<div class="mt-2 h-2.5 rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all {budgetPercent()! > 90
class="h-full rounded-full transition-[width] {budgetPercent()! > 90
? 'bg-red-500'
: budgetPercent()! > 75
? 'bg-amber-500'

View file

@ -152,11 +152,11 @@
</h3>
<div class="flex h-4 overflow-hidden rounded-full">
<div
class="bg-[hsl(var(--color-primary))] transition-all"
class="bg-[hsl(var(--color-primary))] transition-[width]"
style="width: {(billableDuration / totalDuration) * 100}%"
></div>
<div
class="bg-[hsl(var(--color-muted))] transition-all"
class="bg-[hsl(var(--color-muted))] transition-[width]"
style="width: {(nonBillableDuration / totalDuration) * 100}%"
></div>
</div>
@ -189,7 +189,7 @@
</div>
<div class="mt-1 h-2 rounded-full bg-[hsl(var(--color-muted))]">
<div
class="h-full rounded-full transition-all"
class="h-full rounded-full transition-[width]"
style="width: {(item.duration / maxProjectDuration) *
100}%; background-color: {item.color}"
></div>
@ -213,7 +213,7 @@
<div class="flex flex-1 flex-col items-center gap-1">
<div class="w-full flex flex-col justify-end" style="height: 100px;">
<div
class="w-full rounded-t bg-[hsl(var(--color-primary))] transition-all"
class="w-full rounded-t bg-[hsl(var(--color-primary))] transition-[height]"
style="height: {maxDailyDuration > 0
? (day.duration / maxDailyDuration) * 100
: 0}%"

View file

@ -279,7 +279,7 @@
</a>
<button
onclick={() => (showCreateForm = !showCreateForm)}
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white shadow-lg transition-all hover:scale-105 hover:bg-indigo-700"
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white shadow-lg transition-[transform,colors,box-shadow] hover:scale-105 hover:bg-indigo-700"
>
{showCreateForm ? '- Ausblenden' : '+ Neuer Link'}
</button>
@ -490,7 +490,7 @@
<div class="space-y-3">
{#each filteredLinks as link (link.id)}
<div
class="group rounded-xl border border-border-strong bg-white p-4 shadow-sm transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 shadow-sm transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1">
@ -545,7 +545,7 @@
<div class="ml-4 flex items-center gap-1">
<a
href="/uload/analytics/{link.id}"
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-all hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
title="Analytics"
>
<ChartBar size={16} />
@ -553,28 +553,28 @@
</a>
<button
onclick={() => copyShortUrl(link.shortCode)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title="Link kopieren"
>
<Copy size={16} />
</button>
<button
onclick={() => (qrLink = link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title="QR-Code"
>
<QrCode size={16} />
</button>
<button
onclick={() => openEdit(link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title={$_('common.edit')}
>
<PencilSimple size={16} />
</button>
<button
onclick={() => toggleActive(link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
>
<Lightning
@ -584,7 +584,7 @@
</button>
<button
onclick={() => deleteLink(link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
title="Loeschen"
>
<Trash size={16} />

View file

@ -241,7 +241,7 @@
<div class="space-y-3">
{#each filteredLinks as link (link.id)}
<div
class="group rounded-xl border border-border-strong bg-white p-4 shadow-sm transition-all hover:shadow-md dark:border-border dark:bg-card"
class="group rounded-xl border border-border-strong bg-white p-4 shadow-sm transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-center justify-between">
{#if selectMode}
@ -291,7 +291,7 @@
<div class="ml-4 flex items-center gap-1">
<a
href="/uload/analytics/{link.id}"
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-all hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
title="Analytics"
>
<ChartBar size={16} />
@ -299,14 +299,14 @@
</a>
<button
onclick={() => copyShortUrl(link.shortCode)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title="Link kopieren"
>
<Copy size={16} />
</button>
<button
onclick={() => toggleActive(link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
>
<Lightning
@ -316,7 +316,7 @@
</button>
<button
onclick={() => deleteLink(link)}
class="rounded-lg p-2 opacity-0 transition-all hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
title="Loeschen"
>
<Trash size={16} />

View file

@ -116,7 +116,7 @@
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#each tags.value as tag (tag.id)}
<div
class="group rounded-xl border border-border/10 bg-muted/5 p-4 transition-all hover:bg-muted/8"
class="group rounded-xl border border-border/10 bg-muted/5 p-4 transition-colors hover:bg-muted/8"
>
{#if editingTag?.id === tag.id}
<div class="space-y-3">
@ -152,13 +152,13 @@
<span class="text-sm text-muted-foreground">{getUsageCount(tag.id)} Links</span>
<button
onclick={() => (editingTag = { id: tag.id, name: tag.name, color: tag.color })}
class="rounded p-1 text-muted-foreground opacity-0 transition-all hover:bg-muted/10 hover:text-white group-hover:opacity-100"
class="rounded p-1 text-muted-foreground opacity-0 transition-colors hover:bg-muted/10 hover:text-white group-hover:opacity-100"
>
<PencilSimple size={16} />
</button>
<button
onclick={() => deleteTag(tag)}
class="rounded p-1 text-muted-foreground opacity-0 transition-all hover:bg-red-900/20 hover:text-red-400 group-hover:opacity-100"
class="rounded p-1 text-muted-foreground opacity-0 transition-colors hover:bg-red-900/20 hover:text-red-400 group-hover:opacity-100"
>
<Trash size={16} />
</button>

View file

@ -77,7 +77,7 @@
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each appConfig.features as feature}
<div
class="group cursor-default rounded-2xl border-2 border-gray-200 bg-white p-6 shadow-md transition-all duration-300 hover:scale-105 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800"
class="group cursor-default rounded-2xl border-2 border-gray-200 bg-white p-6 shadow-md transition-[transform,colors,box-shadow] duration-300 hover:scale-105 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800"
style="border-left: 4px solid {feature.color}"
>
<div class="mb-4 flex items-center gap-3">

View 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();