mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:01:09 +02:00
refactor(theming): migrate 6 ListViews + ai-missions badges to theme tokens
Replace raw white-alpha Tailwind utilities (text-white/x, bg-white/x, border-white/x) with canonical theme tokens (text-foreground, bg-muted, border-border, etc.) in cards, context, food, moodlit, storage, music ListViews. Replace hardcoded hex badge/dot/phase colors in ai-missions with success/warning/error/primary tokens. Fix two transition-all bugs (food:160, moodlit:223) that prevented CSS custom property colors from resolving on first paint under theme switches. Add scripts/validate-theme-tokens.mjs to prevent regression; run via pnpm run validate:theme-tokens. Not yet in validate:all — 12 modules still use raw white utilities (citycorners, guides, inventory, memoro, picture, plants, playground, presi, questions, times, uload, who). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc22240483
commit
a2a43b1d5a
9 changed files with 188 additions and 75 deletions
|
|
@ -564,16 +564,16 @@
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
.dot-active {
|
.dot-active {
|
||||||
background: #22c55e;
|
background: hsl(var(--color-success));
|
||||||
}
|
}
|
||||||
.dot-paused {
|
.dot-paused {
|
||||||
background: #f59e0b;
|
background: hsl(var(--color-warning));
|
||||||
}
|
}
|
||||||
.dot-done {
|
.dot-done {
|
||||||
background: #6b7280;
|
background: hsl(var(--color-muted-foreground));
|
||||||
}
|
}
|
||||||
.dot-archived {
|
.dot-archived {
|
||||||
background: #374151;
|
background: hsl(var(--color-muted-foreground) / 0.5);
|
||||||
}
|
}
|
||||||
.m-meta {
|
.m-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -730,24 +730,24 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.badge-awaiting-review {
|
.badge-awaiting-review {
|
||||||
background: #fef0c9;
|
background: hsl(var(--color-warning) / 0.18);
|
||||||
color: #8a5a00;
|
color: hsl(var(--color-warning));
|
||||||
}
|
}
|
||||||
.badge-approved {
|
.badge-approved {
|
||||||
background: #d7f7e3;
|
background: hsl(var(--color-success) / 0.18);
|
||||||
color: #1b7a3a;
|
color: hsl(var(--color-success));
|
||||||
}
|
}
|
||||||
.badge-rejected,
|
.badge-rejected,
|
||||||
.badge-failed {
|
.badge-failed {
|
||||||
background: #f7d7d7;
|
background: hsl(var(--color-error) / 0.18);
|
||||||
color: #8a1b1b;
|
color: hsl(var(--color-error));
|
||||||
}
|
}
|
||||||
.badge-running {
|
.badge-running {
|
||||||
background: #d7ecff;
|
background: hsl(var(--color-primary) / 0.18);
|
||||||
color: #0a548b;
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
.it-running {
|
.it-running {
|
||||||
border-color: color-mix(in oklab, #0a548b 35%, hsl(var(--color-border)));
|
border-color: color-mix(in oklab, hsl(var(--color-primary)) 35%, hsl(var(--color-border)));
|
||||||
}
|
}
|
||||||
.phase-block {
|
.phase-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -755,7 +755,7 @@
|
||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
padding: 0.5rem 0.625rem;
|
padding: 0.5rem 0.625rem;
|
||||||
margin: 0.375rem 0;
|
margin: 0.375rem 0;
|
||||||
background: color-mix(in oklab, #0a548b 6%, transparent);
|
background: hsl(var(--color-primary) / 0.08);
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
.phase-line {
|
.phase-line {
|
||||||
|
|
@ -806,8 +806,8 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
.cancel-btn:hover:not(:disabled) {
|
.cancel-btn:hover:not(:disabled) {
|
||||||
color: #8a1b1b;
|
color: hsl(var(--color-error));
|
||||||
border-color: #e99;
|
border-color: hsl(var(--color-error) / 0.5);
|
||||||
}
|
}
|
||||||
.cancel-btn:disabled {
|
.cancel-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
@ -815,10 +815,10 @@
|
||||||
}
|
}
|
||||||
.err-details {
|
.err-details {
|
||||||
margin-top: 0.375rem;
|
margin-top: 0.375rem;
|
||||||
border: 1px solid #f7d7d7;
|
border: 1px solid hsl(var(--color-error) / 0.3);
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
padding: 0.375rem 0.5rem;
|
padding: 0.375rem 0.5rem;
|
||||||
background: color-mix(in oklab, #8a1b1b 4%, transparent);
|
background: hsl(var(--color-error) / 0.05);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
.err-details summary {
|
.err-details summary {
|
||||||
|
|
@ -830,7 +830,7 @@
|
||||||
.err-name {
|
.err-name {
|
||||||
font-family: var(--font-mono, ui-monospace, monospace);
|
font-family: var(--font-mono, ui-monospace, monospace);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #8a1b1b;
|
color: hsl(var(--color-error));
|
||||||
}
|
}
|
||||||
.err-phase {
|
.err-phase {
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: hsl(var(--color-muted-foreground));
|
||||||
|
|
@ -838,7 +838,7 @@
|
||||||
}
|
}
|
||||||
.err-message {
|
.err-message {
|
||||||
margin: 0.375rem 0 0;
|
margin: 0.375rem 0 0;
|
||||||
color: #6a1515;
|
color: hsl(var(--color-error));
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.err-stack {
|
.err-stack {
|
||||||
|
|
@ -931,12 +931,12 @@
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
.grant-pill-ok {
|
.grant-pill-ok {
|
||||||
background: #d7f7e3;
|
background: hsl(var(--color-success) / 0.18);
|
||||||
color: #1b7a3a;
|
color: hsl(var(--color-success));
|
||||||
}
|
}
|
||||||
.grant-pill-warn {
|
.grant-pill-warn {
|
||||||
background: #fde7c8;
|
background: hsl(var(--color-warning) / 0.18);
|
||||||
color: #8a4f00;
|
color: hsl(var(--color-warning));
|
||||||
}
|
}
|
||||||
.grant-pill-muted {
|
.grant-pill-muted {
|
||||||
background: hsl(var(--color-surface));
|
background: hsl(var(--color-surface));
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<BaseListView items={decks} getKey={(d) => d.id} emptyTitle="Keine Decks">
|
<BaseListView items={decks} getKey={(d) => d.id} emptyTitle="Keine Decks">
|
||||||
{#snippet header()}
|
{#snippet header()}
|
||||||
<span class="flex-1">{decks.length} Decks</span>
|
<span class="flex-1">{decks.length} Decks</span>
|
||||||
<span class="text-amber-400/70">{dueForReview} fällig</span>
|
<span class="text-warning/80">{dueForReview} fällig</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet item(deck)}
|
{#snippet item(deck)}
|
||||||
|
|
@ -48,15 +48,15 @@
|
||||||
_siblingIds: decks.map((d) => d.id),
|
_siblingIds: decks.map((d) => d.id),
|
||||||
_siblingKey: 'deckId',
|
_siblingKey: 'deckId',
|
||||||
})}
|
})}
|
||||||
class="mb-2 w-full rounded-md border border-white/10 px-3 py-2.5 text-left transition-colors hover:bg-white/5 min-h-[44px]"
|
class="mb-2 w-full rounded-md border border-border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 min-h-[44px]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="h-3 w-3 rounded" style="background: {deck.color}"></div>
|
<div class="h-3 w-3 rounded" style="background: {deck.color}"></div>
|
||||||
<p class="flex-1 truncate text-sm font-medium text-white/80">{deck.name}</p>
|
<p class="flex-1 truncate text-sm font-medium text-foreground">{deck.name}</p>
|
||||||
<span class="text-xs text-white/40">{cardsInDeck(deck.id)}</span>
|
<span class="text-xs text-muted-foreground">{cardsInDeck(deck.id)}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if deck.description}
|
{#if deck.description}
|
||||||
<p class="mt-1 truncate text-xs text-white/40">{deck.description}</p>
|
<p class="mt-1 truncate text-xs text-muted-foreground">{deck.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,14 @@
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<a
|
<a
|
||||||
href="/context/documents"
|
href="/context/documents"
|
||||||
class="text-xs text-white/50 transition-colors hover:text-white/80"
|
class="text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
Alle Dokumente →
|
Alle Dokumente →
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={handleCreateDocument}
|
onclick={handleCreateDocument}
|
||||||
class="flex items-center gap-1.5 rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-indigo-700"
|
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">+</span>
|
<span aria-hidden="true">+</span>
|
||||||
Neues Dokument
|
Neues Dokument
|
||||||
|
|
@ -82,30 +82,30 @@
|
||||||
|
|
||||||
{#snippet listHeader()}
|
{#snippet listHeader()}
|
||||||
{#if pinnedSpaces.length > 0}
|
{#if pinnedSpaces.length > 0}
|
||||||
<h3 class="mb-2 text-xs font-medium text-white/50">Angepinnte Spaces</h3>
|
<h3 class="mb-2 text-xs font-medium text-muted-foreground">Angepinnte Spaces</h3>
|
||||||
{#each pinnedSpaces as space (space.id)}
|
{#each pinnedSpaces as space (space.id)}
|
||||||
<div class="mb-1 min-h-[44px] rounded-md px-3 py-2 transition-colors hover:bg-white/5">
|
<div class="mb-1 min-h-[44px] rounded-md px-3 py-2 transition-colors hover:bg-muted/50">
|
||||||
<p class="text-sm font-medium text-white/80">{space.name}</p>
|
<p class="text-sm font-medium text-foreground">{space.name}</p>
|
||||||
{#if space.description}
|
{#if space.description}
|
||||||
<p class="truncate text-xs text-white/30">{space.description}</p>
|
<p class="truncate text-xs text-muted-foreground/70">{space.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<h3 class="mb-2 mt-3 text-xs font-medium text-white/50">Zuletzt bearbeitet</h3>
|
<h3 class="mb-2 mt-3 text-xs font-medium text-muted-foreground">Zuletzt bearbeitet</h3>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet item(doc)}
|
{#snippet item(doc)}
|
||||||
<a
|
<a
|
||||||
href="/context/documents/{doc.id}"
|
href="/context/documents/{doc.id}"
|
||||||
class="flex min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-white/5"
|
class="flex min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<span class="text-sm">{@html typeIcons[doc.type] ?? '📄'}</span>
|
<span class="text-sm">{@html typeIcons[doc.type] ?? '📄'}</span>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm text-white/70">{doc.title || 'Unbenannt'}</p>
|
<p class="truncate text-sm text-foreground/90">{doc.title || 'Unbenannt'}</p>
|
||||||
</div>
|
</div>
|
||||||
{#if doc.pinned}
|
{#if doc.pinned}
|
||||||
<span class="text-xs text-white/30">📌</span>
|
<span class="text-xs text-muted-foreground/70">📌</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,8 @@
|
||||||
{#snippet toolbar()}
|
{#snippet toolbar()}
|
||||||
<!-- Calorie progress -->
|
<!-- Calorie progress -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-2xl font-light text-white/90">{Math.round(totalCalories)}</p>
|
<p class="text-2xl font-light text-foreground">{Math.round(totalCalories)}</p>
|
||||||
<p class="text-xs text-white/40">
|
<p class="text-xs text-muted-foreground">
|
||||||
{#if goal}
|
{#if goal}
|
||||||
von {goal.dailyCalories} kcal
|
von {goal.dailyCalories} kcal
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -155,12 +155,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{#if goal}
|
{#if goal}
|
||||||
<div class="mx-auto mt-2 h-1.5 w-32 rounded-full bg-white/10">
|
<div class="mx-auto mt-2 h-1.5 w-32 rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full transition-all {calorieProgress >= 100
|
class="h-full rounded-full {calorieProgress >= 100 ? 'bg-success' : 'bg-primary'}"
|
||||||
? 'bg-green-400'
|
style="width: {calorieProgress}%; transition: width 0.3s ease;"
|
||||||
: 'bg-blue-400'}"
|
|
||||||
style="width: {calorieProgress}%"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -174,7 +172,7 @@
|
||||||
onkeydown={onTextKeydown}
|
onkeydown={onTextKeydown}
|
||||||
placeholder="Was hast du gegessen?"
|
placeholder="Was hast du gegessen?"
|
||||||
disabled={quickSaving}
|
disabled={quickSaving}
|
||||||
class="flex-1 rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/90 placeholder:text-white/30 focus:border-white/20 focus:outline-none disabled:opacity-50"
|
class="flex-1 rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -182,7 +180,7 @@
|
||||||
disabled={!quickText.trim() || quickSaving}
|
disabled={!quickText.trim() || quickSaving}
|
||||||
aria-label="Mahlzeit speichern"
|
aria-label="Mahlzeit speichern"
|
||||||
title="Speichern"
|
title="Speichern"
|
||||||
class="rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/70 transition-colors hover:bg-white/10 disabled:opacity-30"
|
class="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-foreground/80 transition-colors hover:bg-muted disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{quickSaving ? '…' : '↵'}
|
{quickSaving ? '…' : '↵'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -196,25 +194,25 @@
|
||||||
disabled={photoUploading}
|
disabled={photoUploading}
|
||||||
aria-label="Foto hinzufuegen"
|
aria-label="Foto hinzufuegen"
|
||||||
title="Foto"
|
title="Foto"
|
||||||
class="rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/70 transition-colors hover:bg-white/10 disabled:opacity-30"
|
class="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-foreground/80 transition-colors hover:bg-muted disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{photoUploading ? '...' : '📷'}
|
{photoUploading ? '...' : '📷'}
|
||||||
</button>
|
</button>
|
||||||
{#if showPhotoMenu}
|
{#if showPhotoMenu}
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-full right-0 z-10 mb-1 flex flex-col overflow-hidden rounded-lg border border-white/10 bg-[hsl(var(--color-card))] shadow-lg"
|
class="absolute bottom-full right-0 z-10 mb-1 flex flex-col overflow-hidden rounded-lg border border-border bg-card shadow-lg"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={openCamera}
|
onclick={openCamera}
|
||||||
class="flex items-center gap-2 whitespace-nowrap px-4 py-2.5 text-left text-sm text-white/80 transition-colors hover:bg-white/10"
|
class="flex items-center gap-2 whitespace-nowrap px-4 py-2.5 text-left text-sm text-foreground transition-colors hover:bg-muted"
|
||||||
>
|
>
|
||||||
<span>📸</span> Kamera
|
<span>📸</span> Kamera
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={openGallery}
|
onclick={openGallery}
|
||||||
class="flex items-center gap-2 whitespace-nowrap px-4 py-2.5 text-left text-sm text-white/80 transition-colors hover:bg-white/10"
|
class="flex items-center gap-2 whitespace-nowrap px-4 py-2.5 text-left text-sm text-foreground transition-colors hover:bg-muted"
|
||||||
>
|
>
|
||||||
<span>🖼️</span> Mediathek
|
<span>🖼️</span> Mediathek
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -250,19 +248,19 @@
|
||||||
{#snippet item(meal)}
|
{#snippet item(meal)}
|
||||||
<a
|
<a
|
||||||
href="/food/{meal.id}"
|
href="/food/{meal.id}"
|
||||||
class="mb-1 block min-h-[44px] rounded-md px-3 py-2 transition-colors hover:bg-white/5"
|
class="mb-1 block min-h-[44px] rounded-md px-3 py-2 transition-colors hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs text-white/50"
|
<span class="text-xs text-muted-foreground"
|
||||||
>{mealTypeLabels[meal.mealType] ?? meal.mealType}</span
|
>{mealTypeLabels[meal.mealType] ?? meal.mealType}</span
|
||||||
>
|
>
|
||||||
{#if meal.inputType === 'photo'}
|
{#if meal.inputType === 'photo'}
|
||||||
<span class="text-xs text-white/40">📷</span>
|
<span class="text-xs text-muted-foreground/80">📷</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="truncate text-sm text-white/70">{meal.description}</p>
|
<p class="truncate text-sm text-foreground/90">{meal.description}</p>
|
||||||
</div>
|
</div>
|
||||||
{#if meal.photoThumbnailUrl || meal.photoUrl}
|
{#if meal.photoThumbnailUrl || meal.photoUrl}
|
||||||
<img
|
<img
|
||||||
|
|
@ -273,7 +271,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if meal.nutrition}
|
{#if meal.nutrition}
|
||||||
<span class="whitespace-nowrap text-xs text-white/50"
|
<span class="whitespace-nowrap text-xs text-muted-foreground"
|
||||||
>{Math.round(meal.nutrition.calories)} kcal</span
|
>{Math.round(meal.nutrition.calories)} kcal</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function gradientStyle(colors: string[]): string {
|
function gradientStyle(colors: string[]): string {
|
||||||
if (colors.length === 0) return 'background: #333';
|
if (colors.length === 0) return 'background: hsl(var(--color-muted))';
|
||||||
if (colors.length === 1) return `background: ${colors[0]}`;
|
if (colors.length === 1) return `background: ${colors[0]}`;
|
||||||
return `background: linear-gradient(135deg, ${colors.join(', ')})`;
|
return `background: linear-gradient(135deg, ${colors.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
{#if newColors.length > 1}
|
{#if newColors.length > 1}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[8px] text-white"
|
class="absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-error text-[8px] text-white"
|
||||||
onclick={() => removeColor(i)}>x</button
|
onclick={() => removeColor(i)}>x</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -206,7 +206,7 @@
|
||||||
<!-- Save -->
|
<!-- Save -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50"
|
class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
disabled={!newName.trim() || newColors.length === 0}
|
disabled={!newName.trim() || newColors.length === 0}
|
||||||
onclick={handleCreate}
|
onclick={handleCreate}
|
||||||
>
|
>
|
||||||
|
|
@ -220,7 +220,8 @@
|
||||||
<button
|
<button
|
||||||
onclick={() => (fullscreenMood = mood)}
|
onclick={() => (fullscreenMood = mood)}
|
||||||
oncontextmenu={(e) => ctxMenu.open(e, mood)}
|
oncontextmenu={(e) => ctxMenu.open(e, mood)}
|
||||||
class="mood-card group relative aspect-[4/3] w-full select-none overflow-hidden rounded-xl border-2 border-transparent transition-all duration-200 [-webkit-touch-callout:none] hover:border-white/40 focus:outline-none"
|
class="mood-card group relative aspect-[4/3] w-full select-none overflow-hidden rounded-xl border-2 border-transparent [-webkit-touch-callout:none] focus:outline-none"
|
||||||
|
style:transition="border-color 0.2s, transform 0.2s"
|
||||||
style="--mood-color: {mood.colors[0]}"
|
style="--mood-color: {mood.colors[0]}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -252,6 +253,13 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Mood cards always sit on a vivid colour gradient, so the hover ring
|
||||||
|
is intentionally white (brand literal, not theme-intent — see
|
||||||
|
packages/shared-tailwind/src/themes.css §4). */
|
||||||
|
.mood-card:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.anim-gradient {
|
.anim-gradient {
|
||||||
animation: gradient-shift 8s ease infinite;
|
animation: gradient-shift 8s ease infinite;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet listHeader()}
|
{#snippet listHeader()}
|
||||||
<h3 class="mb-2 text-xs font-medium text-white/50">Zuletzt gehört</h3>
|
<h3 class="mb-2 text-xs font-medium text-muted-foreground">Zuletzt gehört</h3>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet item(song)}
|
{#snippet item(song)}
|
||||||
|
|
@ -272,18 +272,18 @@
|
||||||
_siblingIds: recentlyPlayed.map((s) => s.id),
|
_siblingIds: recentlyPlayed.map((s) => s.id),
|
||||||
_siblingKey: 'songId',
|
_siblingKey: 'songId',
|
||||||
})}
|
})}
|
||||||
class="flex w-full min-h-[44px] items-center gap-3 rounded-md px-2 py-1.5 transition-colors hover:bg-white/5 cursor-pointer text-left"
|
class="flex w-full min-h-[44px] items-center gap-3 rounded-md px-2 py-1.5 transition-colors hover:bg-muted/50 cursor-pointer text-left"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded bg-white/10 text-xs text-white/30"
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded bg-muted text-xs text-muted-foreground"
|
||||||
>
|
>
|
||||||
♫
|
♫
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm text-white/80">{song.title}</p>
|
<p class="truncate text-sm text-foreground">{song.title}</p>
|
||||||
<p class="truncate text-xs text-white/40">{song.artist ?? 'Unbekannt'}</p>
|
<p class="truncate text-xs text-muted-foreground">{song.artist ?? 'Unbekannt'}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-white/30">{formatDuration(song.duration)}</span>
|
<span class="text-xs text-muted-foreground/70">{formatDuration(song.duration)}</span>
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</BaseListView>
|
</BaseListView>
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,20 @@
|
||||||
|
|
||||||
{#snippet listHeader()}
|
{#snippet listHeader()}
|
||||||
{#if rootFolders.length > 0}
|
{#if rootFolders.length > 0}
|
||||||
<h3 class="mb-2 text-xs font-medium text-white/50">Ordner</h3>
|
<h3 class="mb-2 text-xs font-medium text-muted-foreground">Ordner</h3>
|
||||||
{#each rootFolders as folder (folder.id)}
|
{#each rootFolders as folder (folder.id)}
|
||||||
<div
|
<div
|
||||||
class="flex min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-white/5"
|
class="flex min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<span class="text-sm" style="color: {folder.color ?? '#6b7280'}">📁</span>
|
<span
|
||||||
<span class="truncate text-sm text-white/70">{folder.name}</span>
|
class="text-sm"
|
||||||
|
style="color: {folder.color ?? 'hsl(var(--color-muted-foreground))'}">📁</span
|
||||||
|
>
|
||||||
|
<span class="truncate text-sm text-foreground/90">{folder.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<h3 class="mb-2 mt-3 text-xs font-medium text-white/50">Zuletzt</h3>
|
<h3 class="mb-2 mt-3 text-xs font-medium text-muted-foreground">Zuletzt</h3>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet item(file)}
|
{#snippet item(file)}
|
||||||
|
|
@ -76,11 +79,11 @@
|
||||||
_siblingIds: recentFiles.map((f) => f.id),
|
_siblingIds: recentFiles.map((f) => f.id),
|
||||||
_siblingKey: 'fileId',
|
_siblingKey: 'fileId',
|
||||||
})}
|
})}
|
||||||
class="flex w-full min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-white/5"
|
class="flex w-full min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<span class="text-sm">{@html fileIcon(file.mimeType)}</span>
|
<span class="text-sm">{@html fileIcon(file.mimeType)}</span>
|
||||||
<span class="min-w-0 flex-1 truncate text-sm text-white/70">{file.name}</span>
|
<span class="min-w-0 flex-1 truncate text-sm text-foreground/90">{file.name}</span>
|
||||||
<span class="shrink-0 text-xs text-white/30">{formatSize(file.size)}</span>
|
<span class="shrink-0 text-xs text-muted-foreground/70">{formatSize(file.size)}</span>
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</BaseListView>
|
</BaseListView>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
||||||
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
|
||||||
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
|
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
|
||||||
|
"validate:theme-tokens": "node scripts/validate-theme-tokens.mjs",
|
||||||
"validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run check:crypto",
|
"validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run check:crypto",
|
||||||
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
"check:crypto": "node scripts/audit-crypto-registry.mjs",
|
||||||
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
|
||||||
|
|
|
||||||
103
scripts/validate-theme-tokens.mjs
Normal file
103
scripts/validate-theme-tokens.mjs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Validate that module ListView.svelte files use theme tokens instead of
|
||||||
|
* raw white-alpha Tailwind utilities.
|
||||||
|
*
|
||||||
|
* Background: the unified Mana app supports multiple theme variants (Lume,
|
||||||
|
* Nature, Stone, Ocean, plus dark). Utilities like `text-white/80`,
|
||||||
|
* `bg-white/5`, `border-white/10` ignore the active theme and can render
|
||||||
|
* white-on-white in light variants. The theme tokens (`text-foreground`,
|
||||||
|
* `bg-muted`, `border-border`, etc.) are the canonical replacements —
|
||||||
|
* they're generated by Tailwind v4 from `packages/shared-tailwind/src/themes.css`
|
||||||
|
* and resolve per-theme automatically.
|
||||||
|
*
|
||||||
|
* Rule: `src/lib/modules/**\/ListView.svelte` must not contain
|
||||||
|
* - `bg-white/` (e.g. bg-white/5, bg-white/10, hover:bg-white/5)
|
||||||
|
* - `text-white/` (e.g. text-white/80, hover:text-white/90)
|
||||||
|
* - `border-white/` (e.g. border-white/10, focus:border-white/20)
|
||||||
|
*
|
||||||
|
* Suggested replacements:
|
||||||
|
* bg-white/N → bg-muted/N or bg-card
|
||||||
|
* text-white → text-foreground
|
||||||
|
* text-white/40-60 → text-muted-foreground
|
||||||
|
* border-white/N → border-border
|
||||||
|
*
|
||||||
|
* `text-white` without an opacity modifier is allowed when it sits on a
|
||||||
|
* guaranteed-dark surface (mood gradient overlays, photo viewer) — those
|
||||||
|
* are brand literals per the themes.css policy.
|
||||||
|
*
|
||||||
|
* Zero deps — runs as plain Node ESM. Uses `git ls-files` to respect
|
||||||
|
* .gitignore.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { readFileSync } 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 LIST_VIEW_GLOB = 'apps/mana/apps/web/src/lib/modules/*/ListView.svelte';
|
||||||
|
|
||||||
|
// `\b` before ensures we catch `hover:bg-white/`, `focus:border-white/`,
|
||||||
|
// etc., without matching unrelated class names like `off-white`.
|
||||||
|
const FORBIDDEN = /(?:^|[\s:"'`])(bg|text|border)-white\//g;
|
||||||
|
|
||||||
|
function listListViews() {
|
||||||
|
const out = execSync(`git ls-files "${LIST_VIEW_GLOB}"`, {
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
return out
|
||||||
|
.split('\n')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const paths = listListViews();
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (const rel of paths) {
|
||||||
|
const abs = join(REPO_ROOT, rel);
|
||||||
|
const src = readFileSync(abs, 'utf8');
|
||||||
|
const lines = src.split('\n');
|
||||||
|
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
FORBIDDEN.lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = FORBIDDEN.exec(line)) !== null) {
|
||||||
|
violations.push({
|
||||||
|
file: rel,
|
||||||
|
line: i + 1,
|
||||||
|
token: `${match[1]}-white/`,
|
||||||
|
snippet: line.trim().slice(0, 120),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error(`\n✗ Theme-token check FAILED (${violations.length} violation(s)):\n`);
|
||||||
|
for (const v of violations) {
|
||||||
|
console.error(` • ${v.file}:${v.line} [${v.token}]`);
|
||||||
|
console.error(` ${v.snippet}`);
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`\nReplace raw white-alpha utilities with theme tokens:\n` +
|
||||||
|
` bg-white/N → bg-muted/N or bg-card\n` +
|
||||||
|
` text-white → text-foreground\n` +
|
||||||
|
` text-white/40-60 → text-muted-foreground\n` +
|
||||||
|
` border-white/N → border-border\n\n` +
|
||||||
|
`Rationale: raw white utilities ignore theme variants (Lume, Nature, ...)\n` +
|
||||||
|
`and can render white-on-white under light themes. See\n` +
|
||||||
|
`packages/shared-tailwind/src/themes.css for the full token set.\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Theme-token check: ${paths.length} ListView(s) use theme tokens correctly.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue