mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
refactor(theming): migrate remaining 738 token violations across routes + components
Expand validate-theme-tokens.mjs scope from ListViews only to all
lib/modules/**/*.svelte and routes/(app)/**/*.svelte. Add a second rule
banning the neutral Tailwind palette (gray/slate/zinc/neutral/stone-N)
— these should be theme tokens (bg-card, bg-muted, text-foreground,
text-muted-foreground, border-border) instead.
Apply one-shot codemod (scripts/migrate-theme-tokens.mjs) that
replaces:
bg-gray-800/900 → bg-card
bg-gray-600/700 → bg-muted (with opacity preserved)
border-gray-600..900 → border-border
text-gray-800/900 → text-foreground
text-gray-300 → text-foreground/90
text-gray-400/500/700 → text-muted-foreground
placeholder-gray-* → placeholder:text-muted-foreground/60
bg/border-white/N → bg-muted/N, border-border/N
text-white/70-90 → text-foreground
text-white/40-60 → text-muted-foreground
text-white/10-30 → text-muted-foreground/70
42 files touched; biggest: presi/deck/[id] (91 subs), uload/analytics
(58), uload/+page (53), presi/+page (47), who/PlayView (35),
skilltree/Edit+AddXpModal (28 each), context/* (115 across 4 pages),
uload/links+tags (50 across 2).
Brand-literal overlays in moodlit/components/mood/{MoodFullscreen,
MoodCard,CreateMoodDialog}.svelte stay unmigrated — they render on
vivid colour gradients. Validator exempts these 3 files from the
white-alpha rule; they still obey the neutral-palette rule.
Result: 527 files pass validate:theme-tokens; svelte-check clean with
0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db2023a77f
commit
7d6a340b13
44 changed files with 747 additions and 460 deletions
|
|
@ -143,8 +143,8 @@
|
|||
<div class="meta">
|
||||
<div class="meta-badges">
|
||||
<span class="badge {catInfo.color}">{catInfo.label}</span>
|
||||
<span class="badge bg-white/10">{DIFFICULTY_LABELS[guide.difficulty]}</span>
|
||||
<span class="badge bg-white/10">{guide.estimatedMinutes} min</span>
|
||||
<span class="badge bg-muted/10">{DIFFICULTY_LABELS[guide.difficulty]}</span>
|
||||
<span class="badge bg-muted/10">{guide.estimatedMinutes} min</span>
|
||||
</div>
|
||||
<h1 class="title">{guide.title}</h1>
|
||||
<p class="description">{guide.description}</p>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
lent: 'bg-amber-100 text-amber-800 dark:bg-amber-900/20 dark:text-amber-400',
|
||||
stored: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400',
|
||||
for_sale: 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-400',
|
||||
disposed: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400',
|
||||
disposed: 'bg-muted text-foreground dark:bg-card/20 dark:text-muted-foreground',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<div
|
||||
class="relative rounded-xl border p-4 transition-all duration-200 {achievement.unlocked
|
||||
? `${rarity.bgColor} ${rarity.borderColor}`
|
||||
: 'border-gray-700/50 bg-gray-800/30'} {achievement.unlocked
|
||||
: 'border-border/50 bg-card/30'} {achievement.unlocked
|
||||
? 'hover:-translate-y-0.5 hover:shadow-lg'
|
||||
: 'opacity-70'}"
|
||||
>
|
||||
|
|
@ -36,34 +36,38 @@
|
|||
<div
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-full {achievement.unlocked
|
||||
? 'bg-yellow-500/20'
|
||||
: 'bg-gray-700/50'}"
|
||||
: 'bg-muted/50'}"
|
||||
>
|
||||
{#if achievement.unlocked}
|
||||
<Trophy class="h-6 w-6 text-yellow-400" />
|
||||
{:else}
|
||||
<Lock class="h-6 w-6 text-gray-500" />
|
||||
<Lock class="h-6 w-6 text-muted-foreground" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<!-- Name -->
|
||||
<h3 class="font-semibold {achievement.unlocked ? 'text-white' : 'text-gray-400'}">
|
||||
<h3 class="font-semibold {achievement.unlocked ? 'text-white' : 'text-muted-foreground'}">
|
||||
{achievement.name}
|
||||
</h3>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="mt-0.5 text-sm {achievement.unlocked ? 'text-gray-300' : 'text-gray-500'}">
|
||||
<p
|
||||
class="mt-0.5 text-sm {achievement.unlocked
|
||||
? 'text-foreground/90'
|
||||
: 'text-muted-foreground'}"
|
||||
>
|
||||
{achievement.description}
|
||||
</p>
|
||||
|
||||
<!-- Progress bar (if not unlocked) -->
|
||||
{#if !achievement.unlocked}
|
||||
<div class="mt-2">
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{achievement.progress} / {achievement.condition.threshold}</span>
|
||||
<span>{progressPercent}%</span>
|
||||
</div>
|
||||
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-gray-700">
|
||||
<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"
|
||||
style="width: {progressPercent}%"
|
||||
|
|
@ -77,13 +81,13 @@
|
|||
<span
|
||||
class="flex items-center gap-1 {achievement.unlocked
|
||||
? 'text-yellow-400'
|
||||
: 'text-gray-500'}"
|
||||
: 'text-muted-foreground'}"
|
||||
>
|
||||
<Star class="h-3 w-3" />
|
||||
+{achievement.xpReward} XP
|
||||
</span>
|
||||
{#if achievement.unlocked && achievement.unlockedAt}
|
||||
<span class="text-gray-500">
|
||||
<span class="text-muted-foreground">
|
||||
{new Date(achievement.unlockedAt).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<p class="mb-2 text-xl font-semibold text-white">{result.achievement.name}</p>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="mb-4 text-gray-400">{result.achievement.description}</p>
|
||||
<p class="mb-4 text-muted-foreground">{result.achievement.description}</p>
|
||||
|
||||
<!-- Rarity + XP reward -->
|
||||
<div class="inline-flex items-center gap-3">
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Click to close -->
|
||||
<p class="mt-6 text-sm text-gray-500">Klicken zum Schließen</p>
|
||||
<p class="mt-6 text-sm text-muted-foreground">Klicken zum Schließen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@
|
|||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-gray-700 bg-gray-800 p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-border bg-card p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-white">Neuer Skill</h2>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-white"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
|
|
@ -64,20 +64,20 @@
|
|||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-gray-300"> Name * </label>
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-foreground/90"> Name * </label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder="z.B. TypeScript"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-gray-300">
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -85,13 +85,15 @@
|
|||
bind:value={description}
|
||||
placeholder="Worum geht es bei diesem Skill?"
|
||||
rows="3"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Branch -->
|
||||
<div>
|
||||
<label for="branch" class="mb-2 block text-sm font-medium text-gray-300"> Kategorie </label>
|
||||
<label for="branch" class="mb-2 block text-sm font-medium text-foreground/90">
|
||||
Kategorie
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each Object.entries(BRANCH_INFO) as [key, info]}
|
||||
<button
|
||||
|
|
@ -100,7 +102,7 @@
|
|||
class="flex items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors {branch ===
|
||||
key
|
||||
? 'border-emerald-500 bg-emerald-500/20 text-white'
|
||||
: 'border-gray-600 bg-gray-700/50 text-gray-300 hover:border-gray-500'}"
|
||||
: 'border-border bg-muted/50 text-foreground/90 hover:border-border-strong'}"
|
||||
>
|
||||
<span class="h-3 w-3 rounded-full" style="background-color: {info.color}"></span>
|
||||
{info.name}
|
||||
|
|
@ -114,7 +116,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="flex-1 rounded-lg border border-gray-600 bg-transparent px-4 py-2 font-medium text-gray-300 transition-colors hover:bg-gray-700"
|
||||
class="flex-1 rounded-lg border border-border bg-transparent px-4 py-2 font-medium text-foreground/90 transition-colors hover:bg-muted"
|
||||
>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -61,17 +61,17 @@
|
|||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-gray-700 bg-gray-800 p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-border bg-card p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">XP hinzufügen</h2>
|
||||
<p class="text-sm text-gray-400">{skill.name} (Lvl {skill.level})</p>
|
||||
<p class="text-sm text-muted-foreground">{skill.name} (Lvl {skill.level})</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-white"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<!-- Quick XP Presets -->
|
||||
<div>
|
||||
<span class="mb-2 block text-sm font-medium text-gray-300"> Schnellauswahl </span>
|
||||
<span class="mb-2 block text-sm font-medium text-foreground/90"> Schnellauswahl </span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each xpPresets as preset}
|
||||
<button
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
class="rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors {xp ===
|
||||
preset.value
|
||||
? 'border-emerald-500 bg-emerald-500/20 text-emerald-400'
|
||||
: 'border-gray-600 bg-gray-700/50 text-gray-300 hover:border-gray-500'}"
|
||||
: 'border-border bg-muted/50 text-foreground/90 hover:border-border-strong'}"
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
|
||||
<!-- Custom XP -->
|
||||
<div>
|
||||
<label for="xp" class="mb-1 block text-sm font-medium text-gray-300">
|
||||
<label for="xp" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
<Lightning class="mr-1 inline h-4 w-4 text-yellow-500" />
|
||||
XP Menge
|
||||
</label>
|
||||
|
|
@ -109,13 +109,13 @@
|
|||
bind:value={xp}
|
||||
min="1"
|
||||
max="1000"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-gray-300">
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
Was hast du gemacht?
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -123,14 +123,14 @@
|
|||
type="text"
|
||||
bind:value={description}
|
||||
placeholder="z.B. Tutorial abgeschlossen"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Duration (optional) -->
|
||||
<div>
|
||||
<label for="duration" class="mb-1 block text-sm font-medium text-gray-300">
|
||||
<Clock class="mr-1 inline h-4 w-4 text-gray-400" />
|
||||
<label for="duration" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
<Clock class="mr-1 inline h-4 w-4 text-muted-foreground" />
|
||||
Dauer (optional, Minuten)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -139,17 +139,17 @@
|
|||
bind:value={duration}
|
||||
min="1"
|
||||
placeholder="z.B. 30"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="rounded-lg bg-gray-700/50 p-3">
|
||||
<div class="rounded-lg bg-muted/50 p-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-400">Vorschau</span>
|
||||
<span class="text-muted-foreground">Vorschau</span>
|
||||
<span class="font-medium text-emerald-400">+{xp} XP</span>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
<div class="mt-1 text-xs text-muted-foreground">
|
||||
Neuer Stand: {(skill.totalXp + xp).toLocaleString()} XP
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="flex-1 rounded-lg border border-gray-600 bg-transparent px-4 py-2 font-medium text-gray-300 transition-colors hover:bg-gray-700"
|
||||
class="flex-1 rounded-lg border border-border bg-transparent px-4 py-2 font-medium text-foreground/90 transition-colors hover:bg-muted"
|
||||
>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -60,14 +60,14 @@
|
|||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-gray-700 bg-gray-800 p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
class="w-full max-w-md rounded-t-2xl sm:rounded-2xl border border-border bg-card p-6 shadow-xl max-h-[95vh] sm:max-h-[90vh] sm:mx-4"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-white">Skill bearbeiten</h2>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-white"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
|
|
@ -82,13 +82,13 @@
|
|||
<Trash class="h-8 w-8 text-red-500" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-white">Skill löschen?</h3>
|
||||
<p class="mb-6 text-gray-400">
|
||||
<p class="mb-6 text-muted-foreground">
|
||||
"{skill.name}" und alle zugehörigen Aktivitäten werden unwiderruflich gelöscht.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
onclick={() => (showDeleteConfirm = false)}
|
||||
class="flex-1 rounded-lg border border-gray-600 bg-transparent px-4 py-2 font-medium text-gray-300 transition-colors hover:bg-gray-700"
|
||||
class="flex-1 rounded-lg border border-border bg-transparent px-4 py-2 font-medium text-foreground/90 transition-colors hover:bg-muted"
|
||||
>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
|
|
@ -104,20 +104,22 @@
|
|||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-gray-300"> Name * </label>
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder="z.B. TypeScript"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-gray-300">
|
||||
<label for="description" class="mb-1 block text-sm font-medium text-foreground/90">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -125,13 +127,13 @@
|
|||
bind:value={description}
|
||||
placeholder="Worum geht es bei diesem Skill?"
|
||||
rows="3"
|
||||
class="w-full rounded-lg border border-gray-600 bg-gray-700 px-4 py-2 text-white placeholder-gray-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
class="w-full rounded-lg border border-border bg-muted px-4 py-2 text-white placeholder:text-muted-foreground/60 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Branch -->
|
||||
<div>
|
||||
<span class="mb-2 block text-sm font-medium text-gray-300"> Kategorie </span>
|
||||
<span class="mb-2 block text-sm font-medium text-foreground/90"> Kategorie </span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each Object.entries(BRANCH_INFO) as [key, info]}
|
||||
<button
|
||||
|
|
@ -140,7 +142,7 @@
|
|||
class="flex items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors {branch ===
|
||||
key
|
||||
? 'border-emerald-500 bg-emerald-500/20 text-white'
|
||||
: 'border-gray-600 bg-gray-700/50 text-gray-300 hover:border-gray-500'}"
|
||||
: 'border-border bg-muted/50 text-foreground/90 hover:border-border-strong'}"
|
||||
>
|
||||
<span class="h-3 w-3 rounded-full" style="background-color: {info.color}"></span>
|
||||
{info.name}
|
||||
|
|
@ -150,18 +152,18 @@
|
|||
</div>
|
||||
|
||||
<!-- Stats (read-only) -->
|
||||
<div class="rounded-lg bg-gray-700/50 p-3">
|
||||
<div class="rounded-lg bg-muted/50 p-3">
|
||||
<div class="grid grid-cols-3 gap-4 text-center text-sm">
|
||||
<div>
|
||||
<div class="text-gray-400">Level</div>
|
||||
<div class="text-muted-foreground">Level</div>
|
||||
<div class="font-semibold text-white">{skill.level}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-400">Total XP</div>
|
||||
<div class="text-muted-foreground">Total XP</div>
|
||||
<div class="font-semibold text-white">{skill.totalXp.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-400">Erstellt</div>
|
||||
<div class="text-muted-foreground">Erstellt</div>
|
||||
<div class="font-semibold text-white">
|
||||
{new Date(skill.createdAt).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
|
|
@ -182,7 +184,7 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
class="flex-1 rounded-lg border border-gray-600 bg-transparent px-4 py-2 font-medium text-gray-300 transition-colors hover:bg-gray-700"
|
||||
class="flex-1 rounded-lg border border-border bg-transparent px-4 py-2 font-medium text-foreground/90 transition-colors hover:bg-muted"
|
||||
>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
<h2 class="mb-2 text-3xl font-bold text-white level-up-text">LEVEL UP!</h2>
|
||||
|
||||
<!-- Skill name -->
|
||||
<p class="mb-4 text-xl text-gray-300">{skillName}</p>
|
||||
<p class="mb-4 text-xl text-foreground/90">{skillName}</p>
|
||||
|
||||
<!-- New level badge -->
|
||||
<div
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Click to close -->
|
||||
<p class="mt-6 text-sm text-gray-500">Klicken zum Schließen</p>
|
||||
<p class="mt-6 text-sm text-muted-foreground">Klicken zum Schließen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
function getLevelColor(level: number): string {
|
||||
const colors = [
|
||||
'text-gray-400',
|
||||
'text-muted-foreground',
|
||||
'text-blue-400',
|
||||
'text-purple-400',
|
||||
'text-pink-400',
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="skill-card group relative overflow-hidden rounded-xl border border-gray-700 bg-gray-800/50 p-4"
|
||||
class="skill-card group relative overflow-hidden rounded-xl border border-border bg-card/50 p-4"
|
||||
>
|
||||
<!-- Branch Indicator -->
|
||||
<div
|
||||
|
|
@ -45,14 +45,14 @@
|
|||
<div class="mb-3 flex items-start justify-between pl-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white">{skill.name}</h3>
|
||||
<p class="text-sm text-gray-400">{branchInfo.name}</p>
|
||||
<p class="text-sm text-muted-foreground">{branchInfo.name}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{#each Array(skill.level) as _, i}
|
||||
<Star class="h-4 w-4 fill-yellow-500 text-yellow-500" />
|
||||
{/each}
|
||||
{#each Array(5 - skill.level) as _, i}
|
||||
<Star class="h-4 w-4 text-gray-600" />
|
||||
<Star class="h-4 w-4 text-muted-foreground/70" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<!-- Level Badge -->
|
||||
<div class="mb-3 pl-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-700/50 px-3 py-1 text-sm {getLevelColor(
|
||||
class="inline-flex items-center gap-1 rounded-full bg-muted/50 px-3 py-1 text-sm {getLevelColor(
|
||||
skill.level
|
||||
)}"
|
||||
>
|
||||
|
|
@ -71,8 +71,8 @@
|
|||
<!-- XP Progress -->
|
||||
<div class="mb-4 pl-3">
|
||||
<div class="mb-1 flex justify-between text-sm">
|
||||
<span class="text-gray-400">XP</span>
|
||||
<span class="text-gray-300">
|
||||
<span class="text-muted-foreground">XP</span>
|
||||
<span class="text-foreground/90">
|
||||
{skill.totalXp.toLocaleString()}
|
||||
{#if !isMaxLevel}
|
||||
/ {nextLevelXp.toLocaleString()}
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
|
||||
<!-- Description -->
|
||||
{#if skill.description}
|
||||
<p class="mb-4 pl-3 text-sm text-gray-400 line-clamp-2">{skill.description}</p>
|
||||
<p class="mb-4 pl-3 text-sm text-muted-foreground line-clamp-2">{skill.description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Actions -->
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
</button>
|
||||
<button
|
||||
onclick={onEdit}
|
||||
class="rounded-lg bg-gray-600/20 p-2 text-gray-400 opacity-0 transition-all hover:bg-gray-600/30 hover:text-white group-hover:opacity-100"
|
||||
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"
|
||||
title={$_('common.edit')}
|
||||
>
|
||||
<PencilSimple class="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="w-full max-w-2xl rounded-2xl border border-gray-700 bg-gray-800 p-6 shadow-xl my-8">
|
||||
<div class="w-full max-w-2xl rounded-2xl border border-border bg-card p-6 shadow-xl my-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -121,13 +121,13 @@
|
|||
</div>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-white"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mb-6 text-gray-400">
|
||||
<p class="mb-6 text-muted-foreground">
|
||||
Starte schnell mit vorgefertigten Skill-Sets. Wähle eine Vorlage und füge einzelne Skills oder
|
||||
alle auf einmal hinzu.
|
||||
</p>
|
||||
|
|
@ -135,15 +135,15 @@
|
|||
<!-- Template List -->
|
||||
<div class="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
|
||||
{#each Object.entries(templates) as [name, skills]}
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-900/50 overflow-hidden">
|
||||
<div class="rounded-xl border border-border bg-card/50 overflow-hidden">
|
||||
<!-- Template Header -->
|
||||
<div class="flex items-center justify-between p-4 hover:bg-gray-800/50 transition-colors">
|
||||
<div class="flex items-center justify-between p-4 hover:bg-card/50 transition-colors">
|
||||
<button
|
||||
onclick={() => (selectedTemplate = selectedTemplate === name ? null : name)}
|
||||
class="flex-1 text-left"
|
||||
>
|
||||
<h3 class="font-semibold text-white">{name}</h3>
|
||||
<p class="text-sm text-gray-400">{skills.length} Skills</p>
|
||||
<p class="text-sm text-muted-foreground">{skills.length} Skills</p>
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
</button>
|
||||
<button
|
||||
onclick={() => (selectedTemplate = selectedTemplate === name ? null : name)}
|
||||
class="text-gray-500 text-xl px-2"
|
||||
class="text-muted-foreground text-xl px-2"
|
||||
>
|
||||
{selectedTemplate === name ? '−' : '+'}
|
||||
</button>
|
||||
|
|
@ -164,10 +164,10 @@
|
|||
|
||||
<!-- Expanded Skills -->
|
||||
{#if selectedTemplate === name}
|
||||
<div class="border-t border-gray-700 p-4 space-y-2">
|
||||
<div class="border-t border-border p-4 space-y-2">
|
||||
{#each skills as skill}
|
||||
{@const isAdded = addedSkills.has(skill.name)}
|
||||
<div class="flex items-center justify-between rounded-lg bg-gray-800/50 px-3 py-2">
|
||||
<div class="flex items-center justify-between rounded-lg bg-card/50 px-3 py-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="h-3 w-3 rounded-full"
|
||||
|
|
@ -175,7 +175,7 @@
|
|||
></span>
|
||||
<div>
|
||||
<span class="font-medium text-white">{skill.name}</span>
|
||||
<span class="text-gray-400 text-sm"> - {skill.description}</span>
|
||||
<span class="text-muted-foreground text-sm"> - {skill.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
disabled={isAdded || adding}
|
||||
class="rounded-lg p-1.5 transition-colors {isAdded
|
||||
? 'bg-emerald-600/20 text-emerald-400'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
||||
: 'bg-muted text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
{#if isAdded}
|
||||
<Check size={16} />
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="rounded-lg bg-gray-700 px-4 py-2 font-medium text-white transition-colors hover:bg-gray-600"
|
||||
class="rounded-lg bg-muted px-4 py-2 font-medium text-white transition-colors hover:bg-muted"
|
||||
>
|
||||
Fertig
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<!-- Total XP -->
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/50 p-4">
|
||||
<div class="rounded-xl border border-border bg-card/50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-yellow-500/20">
|
||||
<Lightning class="h-6 w-6 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Gesamt-XP</p>
|
||||
<p class="text-sm text-muted-foreground">Gesamt-XP</p>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{userStats.totalXp.toLocaleString()}
|
||||
</p>
|
||||
|
|
@ -33,13 +33,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Total Skills -->
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/50 p-4">
|
||||
<div class="rounded-xl border border-border bg-card/50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/20">
|
||||
<Target class="h-6 w-6 text-emerald-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Skills</p>
|
||||
<p class="text-sm text-muted-foreground">Skills</p>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{userStats.totalSkills}
|
||||
</p>
|
||||
|
|
@ -48,13 +48,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Highest Level -->
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/50 p-4">
|
||||
<div class="rounded-xl border border-border bg-card/50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-purple-500/20">
|
||||
<Trophy class="h-6 w-6 text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Hochstes Level</p>
|
||||
<p class="text-sm text-muted-foreground">Hochstes Level</p>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{userStats.highestLevel}
|
||||
</p>
|
||||
|
|
@ -63,13 +63,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Streak -->
|
||||
<div class="rounded-xl border border-gray-700 bg-gray-800/50 p-4">
|
||||
<div class="rounded-xl border border-border bg-card/50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-orange-500/20">
|
||||
<Fire class="h-6 w-6 text-orange-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Streak</p>
|
||||
<p class="text-sm text-muted-foreground">Streak</p>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{userStats.streakDays} Tage
|
||||
</p>
|
||||
|
|
@ -80,16 +80,16 @@
|
|||
<!-- Achievements -->
|
||||
<a
|
||||
href="/skilltree/achievements"
|
||||
class="rounded-xl border border-gray-700 bg-gray-800/50 p-4 transition-colors hover:border-yellow-600/50 hover:bg-yellow-900/10"
|
||||
class="rounded-xl border border-border bg-card/50 p-4 transition-colors hover:border-yellow-600/50 hover:bg-yellow-900/10"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-yellow-500/20">
|
||||
<Medal class="h-6 w-6 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Achievements</p>
|
||||
<p class="text-sm text-muted-foreground">Achievements</p>
|
||||
<p class="text-2xl font-bold text-white">
|
||||
{achievementStats.unlocked}<span class="text-sm font-normal text-gray-500"
|
||||
{achievementStats.unlocked}<span class="text-sm font-normal text-muted-foreground"
|
||||
>/{achievementStats.total}</span
|
||||
>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -116,17 +116,17 @@
|
|||
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Header -->
|
||||
<header class="flex items-center gap-2 border-b border-white/5 px-3 py-2">
|
||||
<header class="flex items-center gap-2 border-b border-border/5 px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1.5 text-white/60 hover:bg-white/5 hover:text-white/90"
|
||||
class="rounded p-1.5 text-muted-foreground hover:bg-muted/5 hover:text-foreground"
|
||||
onclick={onBack}
|
||||
aria-label="Zurück"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-white/90">
|
||||
<div class="text-sm font-medium text-foreground">
|
||||
{#if game?.status === 'won'}
|
||||
✅ {game.revealedName}
|
||||
{:else if game?.status === 'surrendered'}
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
Wer bin ich?
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-[11px] text-white/40">
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
{#if game}
|
||||
{game.deckId} · {difficultyEmoji(game.difficulty)} · {game.messageCount} Fragen
|
||||
{/if}
|
||||
|
|
@ -144,14 +144,14 @@
|
|||
{#if game?.status === 'playing'}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded px-2 py-1 text-xs text-white/60 hover:bg-white/5 hover:text-white/90"
|
||||
class="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted/5 hover:text-foreground"
|
||||
onclick={() => (showGuessModal = true)}
|
||||
>
|
||||
Tippen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded px-2 py-1 text-xs text-white/40 hover:bg-white/5 hover:text-white/70"
|
||||
class="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted/5 hover:text-foreground"
|
||||
onclick={surrender}
|
||||
>
|
||||
Aufgeben
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
<!-- Result banner (post-game only) -->
|
||||
{#if game && game.status !== 'playing'}
|
||||
<div
|
||||
class="border-b border-white/5 px-4 py-3 {game.status === 'won'
|
||||
class="border-b border-border/5 px-4 py-3 {game.status === 'won'
|
||||
? 'bg-emerald-500/10'
|
||||
: 'bg-amber-500/10'}"
|
||||
>
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
<p class="text-sm font-medium text-emerald-300">
|
||||
Erraten in {game.messageCount} Nachrichten!
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-white/60">
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
Das war {game.revealedName}.
|
||||
</p>
|
||||
{:else}
|
||||
|
|
@ -182,7 +182,9 @@
|
|||
<!-- Messages scroll -->
|
||||
<div bind:this={scrollContainer} class="flex-1 overflow-y-auto px-3 py-4">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex h-full items-center justify-center text-center text-sm text-white/40">
|
||||
<div
|
||||
class="flex h-full items-center justify-center text-center text-sm text-muted-foreground"
|
||||
>
|
||||
<p>
|
||||
Stell die erste Frage.<br />
|
||||
Versuche, die Persönlichkeit durch geschickte Fragen herauszufinden.
|
||||
|
|
@ -200,7 +202,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="max-w-[80%] whitespace-pre-wrap rounded-lg bg-white/5 px-3 py-2 text-sm leading-relaxed text-white/90"
|
||||
class="max-w-[80%] whitespace-pre-wrap rounded-lg bg-muted/5 px-3 py-2 text-sm leading-relaxed text-foreground"
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
|
|
@ -219,14 +221,14 @@
|
|||
|
||||
<!-- Input or notes -->
|
||||
{#if game?.status === 'playing'}
|
||||
<div class="border-t border-white/5 p-3">
|
||||
<div class="border-t border-border/5 p-3">
|
||||
<div class="mx-auto flex max-w-2xl items-end gap-2">
|
||||
<textarea
|
||||
bind:value={inputText}
|
||||
onkeydown={onInputKeydown}
|
||||
placeholder="Frag mich etwas…"
|
||||
rows="1"
|
||||
class="flex-1 resize-none rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/90 placeholder-white/30 focus:border-purple-400/50 focus:outline-none"
|
||||
class="flex-1 resize-none rounded-lg border border-border/10 bg-muted/5 px-3 py-2 text-sm text-foreground placeholder-white/30 focus:border-purple-400/50 focus:outline-none"
|
||||
disabled={sending}
|
||||
></textarea>
|
||||
<button
|
||||
|
|
@ -240,9 +242,12 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if game}
|
||||
<div class="border-t border-white/5 p-3">
|
||||
<div class="border-t border-border/5 p-3">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<label for="who-notes" class="mb-1 block text-[11px] uppercase tracking-wide text-white/40">
|
||||
<label
|
||||
for="who-notes"
|
||||
class="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground"
|
||||
>
|
||||
Notiz {notesDirty ? '(speichert…)' : ''}
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -251,7 +256,7 @@
|
|||
oninput={onNotesInput}
|
||||
placeholder="Notiz zum Spiel…"
|
||||
rows="2"
|
||||
class="w-full resize-none rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/90 placeholder-white/30 focus:border-white/20 focus:outline-none"
|
||||
class="w-full resize-none rounded-lg border border-border/10 bg-muted/5 px-3 py-2 text-sm text-foreground placeholder-white/30 focus:border-border/20 focus:outline-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -266,9 +271,9 @@
|
|||
onkeydown={(e) => e.key === 'Escape' && (showGuessModal = false)}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="w-full max-w-md rounded-lg bg-zinc-900 p-5">
|
||||
<h3 class="mb-3 text-base font-medium text-white/90">Wer ist es?</h3>
|
||||
<p class="mb-3 text-xs text-white/50">
|
||||
<div class="w-full max-w-md rounded-lg bg-card p-5">
|
||||
<h3 class="mb-3 text-base font-medium text-foreground">Wer ist es?</h3>
|
||||
<p class="mb-3 text-xs text-muted-foreground">
|
||||
Wenn die KI deine Vermutung nicht erkannt hat, kannst du den Namen hier direkt eintragen.
|
||||
</p>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
|
|
@ -277,13 +282,13 @@
|
|||
bind:value={guessText}
|
||||
onkeydown={(e) => e.key === 'Enter' && submitGuess()}
|
||||
placeholder="z.B. Marie Curie"
|
||||
class="mb-3 w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white/90 placeholder-white/30 focus:border-purple-400/50 focus:outline-none"
|
||||
class="mb-3 w-full rounded-lg border border-border/10 bg-muted/5 px-3 py-2 text-sm text-foreground placeholder-white/30 focus:border-purple-400/50 focus:outline-none"
|
||||
autofocus
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded px-3 py-1.5 text-sm text-white/60 hover:bg-white/5"
|
||||
class="rounded px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted/5"
|
||||
onclick={() => {
|
||||
showGuessModal = false;
|
||||
guessText = '';
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@
|
|||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
|
||||
{userData.user.role === 'admin'
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}"
|
||||
: 'bg-muted text-muted-foreground dark:bg-card dark:text-foreground/90'}"
|
||||
>
|
||||
{userData.user.role}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -43,25 +43,25 @@
|
|||
<!-- Stats -->
|
||||
<div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-4 text-center dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="text-2xl font-bold">{spaces.length}</div>
|
||||
<div class="mt-1 text-xs opacity-60">Spaces</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-4 text-center dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="text-2xl font-bold">{stats.total}</div>
|
||||
<div class="mt-1 text-xs opacity-60">Dokumente</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-4 text-center dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="text-2xl font-bold">{stats.totalWords.toLocaleString()}</div>
|
||||
<div class="mt-1 text-xs opacity-60">Woerter</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-center dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-4 text-center dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="text-2xl font-bold">{stats.text}/{stats.context}/{stats.prompt}</div>
|
||||
<div class="mt-1 text-xs opacity-60">Text/Kontext/Prompt</div>
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
</a>
|
||||
<a
|
||||
href="/context/documents"
|
||||
class="flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="flex items-center gap-2 rounded-lg border border-border-strong px-4 py-2 text-sm font-medium transition-colors hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
<FileText size={16} />
|
||||
Alle Dokumente
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
{#each pinnedSpaces as space}
|
||||
<a
|
||||
href="/context/spaces/{space.id}"
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-4 transition-all 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-gray-200 bg-white p-4 transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
class="group rounded-xl border border-border-strong bg-white p-4 transition-all hover:shadow-md dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
e.preventDefault();
|
||||
handleTogglePinDoc(doc.id);
|
||||
}}
|
||||
class="ml-2 rounded p-1 opacity-0 transition-opacity hover:bg-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="ml-2 rounded p-1 opacity-0 transition-opacity hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={doc.pinned ? 'Loslassen' : 'Anheften'}
|
||||
>
|
||||
{doc.pinned ? '★' : '☆'}
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
<div class="mt-2 flex items-center gap-3 text-xs opacity-40">
|
||||
{#if doc.metadata?.tags && doc.metadata.tags.length > 0}
|
||||
{#each doc.metadata.tags.slice(0, 3) as tag}
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-700">{tag}</span>
|
||||
<span class="rounded bg-muted px-1.5 py-0.5 dark:bg-muted">{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="ml-auto">
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
</section>
|
||||
{:else}
|
||||
<div
|
||||
class="rounded-xl border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-600"
|
||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<FileText size={48} class="mx-auto mb-4 opacity-20" />
|
||||
<h3 class="text-lg font-medium opacity-60">Noch keine Dokumente</h3>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm transition-colors {typeFilter === filter.value
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'opacity-60 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
: 'opacity-60 hover:bg-muted dark:hover:bg-muted'}"
|
||||
onclick={() => (typeFilter = filter.value)}
|
||||
>
|
||||
{filter.label}
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Dokumente durchsuchen..."
|
||||
class="w-full rounded-lg border border-gray-300 bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-full rounded-lg border border-border-strong bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
<button
|
||||
class="rounded-full px-2 py-1 text-xs transition-colors {tagFilter.includes(tag)
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-gray-100 opacity-60 hover:opacity-100 dark:bg-gray-700'}"
|
||||
: 'bg-muted opacity-60 hover:opacity-100 dark:bg-muted'}"
|
||||
onclick={() => toggleTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
|
|
@ -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-gray-200 bg-white p-4 transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
class="group rounded-xl border border-border-strong bg-white p-4 transition-all 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">
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
>
|
||||
<button
|
||||
onclick={() => handleTogglePin(doc.id)}
|
||||
class="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
class="rounded p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
title={doc.pinned ? 'Loslassen' : 'Anheften'}
|
||||
>
|
||||
{doc.pinned ? '★' : '☆'}
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
<div class="mt-2 flex items-center gap-3 text-xs opacity-40">
|
||||
{#if doc.metadata?.tags && doc.metadata.tags.length > 0}
|
||||
{#each doc.metadata.tags.slice(0, 3) as tag}
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-700">{tag}</span>
|
||||
<span class="rounded bg-muted px-1.5 py-0.5 dark:bg-muted">{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="ml-auto">
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-300 py-16 text-center dark:border-gray-600"
|
||||
class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-border-strong py-16 text-center dark:border-border"
|
||||
>
|
||||
<FileText size={48} class="mb-4 opacity-20" />
|
||||
<h2 class="text-lg font-medium opacity-60">Noch keine Dokumente</h2>
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="none"
|
||||
>
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
onclick={() => (deleteTarget = null)}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@
|
|||
|
||||
<!-- Editor -->
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"
|
||||
class="rounded-xl border border-border-strong bg-white p-6 dark:border-border dark:bg-card"
|
||||
>
|
||||
<!-- Title -->
|
||||
<input
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
|
||||
<!-- Type + Tags bar -->
|
||||
<div
|
||||
class="mb-4 flex flex-wrap items-center gap-3 border-b border-gray-100 pb-4 dark:border-gray-700"
|
||||
class="mb-4 flex flex-wrap items-center gap-3 border-b border-border-strong pb-4 dark:border-border"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
{#each typeOptions as opt}
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors {editType ===
|
||||
opt.value
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-gray-100 opacity-60 hover:opacity-100 dark:bg-gray-700'}"
|
||||
: 'bg-muted opacity-60 hover:opacity-100 dark:bg-muted'}"
|
||||
onclick={() => {
|
||||
editType = opt.value;
|
||||
scheduleAutoSave();
|
||||
|
|
@ -166,7 +166,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="h-4 w-px bg-gray-200 dark:bg-gray-600"></div>
|
||||
<div class="h-4 w-px bg-muted dark:bg-muted"></div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editTags}
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="none"
|
||||
>
|
||||
|
|
@ -223,7 +223,7 @@
|
|||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
onclick={() => (showDeleteConfirm = false)}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full rounded-lg border border-gray-300 bg-white px-4 py-3 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-gray-600 dark:bg-gray-700';
|
||||
'w-full rounded-lg border border-border-strong bg-white px-4 py-3 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-border dark:bg-muted';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
<!-- Create Form -->
|
||||
{#if showCreateForm}
|
||||
<div
|
||||
class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
||||
class="mb-6 rounded-xl border border-border-strong bg-white p-6 shadow-sm dark:border-border dark:bg-card"
|
||||
>
|
||||
<h3 class="mb-4 text-lg font-semibold">Neuen Space erstellen</h3>
|
||||
<div class="space-y-4">
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
onclick={() => (showCreateForm = false)}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Spaces durchsuchen..."
|
||||
class="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-9 pr-4 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-full rounded-lg border border-border-strong bg-white py-2.5 pl-9 pr-4 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -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-gray-200 bg-white p-4 transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
class="group rounded-xl border border-border-strong bg-white p-4 transition-all 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">
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
>
|
||||
<button
|
||||
onclick={() => handleTogglePin(space.id)}
|
||||
class="rounded p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
class="rounded p-1.5 hover:bg-muted dark:hover:bg-muted"
|
||||
title={space.pinned ? 'Loslassen' : 'Anheften'}
|
||||
>
|
||||
{space.pinned ? '★' : '☆'}
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-300 py-16 text-center dark:border-gray-600"
|
||||
class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-border-strong py-16 text-center dark:border-border"
|
||||
>
|
||||
<Plus size={48} class="mb-4 opacity-20" />
|
||||
<h2 class="text-lg font-medium opacity-60">Noch keine Spaces</h2>
|
||||
|
|
@ -227,7 +227,7 @@
|
|||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="none"
|
||||
>
|
||||
|
|
@ -239,7 +239,7 @@
|
|||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
onclick={() => (deleteTarget = null)}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -116,20 +116,20 @@
|
|||
{:else}
|
||||
<!-- Space Header -->
|
||||
<div
|
||||
class="mb-6 rounded-xl border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800"
|
||||
class="mb-6 rounded-xl border border-border-strong bg-white p-6 dark:border-border dark:bg-card"
|
||||
>
|
||||
{#if editingName}
|
||||
<div class="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editName}
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-xl font-bold focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-full rounded-lg border border-border-strong bg-white px-3 py-2 text-xl font-bold focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
<textarea
|
||||
bind:value={editDescription}
|
||||
rows="2"
|
||||
placeholder="Beschreibung..."
|
||||
class="w-full resize-none rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-full resize-none rounded-lg border border-border-strong bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
></textarea>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
<Check size={14} /> Speichern
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1 rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="flex items-center gap-1 rounded-lg border border-border-strong px-3 py-1.5 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
onclick={cancelEdit}
|
||||
>
|
||||
<X size={14} /> Abbrechen
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-lg p-2 opacity-60 transition-colors hover:bg-gray-100 hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
|
||||
onclick={startEdit}
|
||||
title={$_('common.edit')}
|
||||
>
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm transition-colors {typeFilter === filter.value
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'opacity-60 hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
: 'opacity-60 hover:bg-muted dark:hover:bg-muted'}"
|
||||
onclick={() => (typeFilter = filter.value)}
|
||||
>
|
||||
{filter.label}
|
||||
|
|
@ -194,7 +194,7 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder={$_('common.search')}
|
||||
class="w-48 rounded-lg border border-gray-300 bg-white py-1.5 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-48 rounded-lg border border-border-strong bg-white py-1.5 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -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-gray-200 bg-white p-4 transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
class="group rounded-xl border border-border-strong bg-white p-4 transition-all 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">
|
||||
|
|
@ -243,7 +243,7 @@
|
|||
>
|
||||
<button
|
||||
onclick={() => handleTogglePinDoc(doc.id)}
|
||||
class="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
class="rounded p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
title={doc.pinned ? 'Loslassen' : 'Anheften'}
|
||||
>
|
||||
{doc.pinned ? '★' : '☆'}
|
||||
|
|
@ -265,7 +265,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="rounded-xl border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-600"
|
||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<p class="opacity-60">Keine Dokumente in diesem Space</p>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
if (percentage >= 100) return 'bg-green-500';
|
||||
if (percentage >= 75) return 'bg-blue-500';
|
||||
if (percentage >= 50) return 'bg-yellow-500';
|
||||
return 'bg-gray-400';
|
||||
return 'bg-muted';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -523,6 +523,6 @@
|
|||
aria-label="Bild schließen"
|
||||
>
|
||||
<img src={meal.photoUrl} alt={meal.description} class="max-h-full max-w-full object-contain" />
|
||||
<span class="absolute right-4 top-4 text-3xl text-white/80">×</span>
|
||||
<span class="absolute right-4 top-4 text-3xl text-foreground">×</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@
|
|||
{tag.name}
|
||||
<button
|
||||
onclick={() => handleRemoveTag(tag.id)}
|
||||
class="ml-0.5 rounded-full hover:bg-white/20"
|
||||
class="ml-0.5 rounded-full hover:bg-muted/20"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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-white/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-all duration-200 hover:border-border/40 focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
style="--mood-color: {mood.colors[0]}"
|
||||
>
|
||||
<div
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
<div class="absolute inset-x-0 bottom-0 p-4 text-left">
|
||||
<h3 class="text-lg font-semibold text-white drop-shadow-md">{mood.name}</h3>
|
||||
<span
|
||||
class="mt-1 inline-block rounded-full bg-white/20 px-2 py-0.5 text-[10px] font-medium text-white/80 backdrop-blur-sm capitalize"
|
||||
class="mt-1 inline-block rounded-full bg-muted/20 px-2 py-0.5 text-[10px] font-medium text-foreground backdrop-blur-sm capitalize"
|
||||
>{mood.animation}</span
|
||||
>
|
||||
</div>
|
||||
|
|
@ -197,7 +197,7 @@
|
|||
deleteMood(mood);
|
||||
}
|
||||
}}
|
||||
class="absolute right-2 top-2 rounded-full bg-black/20 p-1.5 text-white/70 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-all hover:bg-black/40 hover:text-white group-hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<X size={14} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@
|
|||
<Card>
|
||||
<div class="py-12 text-center">
|
||||
<span class="mb-4 block text-6xl">🏢</span>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No organizations yet</h3>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<h3 class="mb-2 text-lg font-semibold text-foreground dark:text-white">
|
||||
No organizations yet
|
||||
</h3>
|
||||
<p class="mb-6 text-sm text-muted-foreground dark:text-muted-foreground">
|
||||
Create your first organization to get started
|
||||
</p>
|
||||
<Button variant="primary">Create Organization</Button>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
{#snippet actions()}
|
||||
<a
|
||||
href="/organizations"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
class="text-sm text-muted-foreground hover:text-muted-foreground dark:text-muted-foreground dark:hover:text-foreground"
|
||||
>
|
||||
Back
|
||||
</a>
|
||||
|
|
@ -94,21 +94,21 @@
|
|||
{#if activeTab === 'overview'}
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Details</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground dark:text-white mb-4">Details</h3>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Name</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{org.name}</dd>
|
||||
<dt class="text-muted-foreground dark:text-muted-foreground">Name</dt>
|
||||
<dd class="font-medium text-foreground dark:text-white">{org.name}</dd>
|
||||
</div>
|
||||
{#if org.slug}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Slug</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{org.slug}</dd>
|
||||
<dt class="text-muted-foreground dark:text-muted-foreground">Slug</dt>
|
||||
<dd class="font-medium text-foreground dark:text-white">{org.slug}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Created</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">
|
||||
<dt class="text-muted-foreground dark:text-muted-foreground">Created</dt>
|
||||
<dd class="font-medium text-foreground dark:text-white">
|
||||
{new Date(org.createdAt).toLocaleDateString()}
|
||||
</dd>
|
||||
</div>
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Landing Page</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground dark:text-white mb-4">Landing Page</h3>
|
||||
{#if org.metadata?.landingPage?.enabled}
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-green-600 dark:text-green-400 flex items-center gap-2">
|
||||
|
|
@ -135,7 +135,9 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Not configured yet</p>
|
||||
<p class="text-sm text-muted-foreground dark:text-muted-foreground">
|
||||
Not configured yet
|
||||
</p>
|
||||
{/if}
|
||||
<div class="mt-4">
|
||||
<a href="/organizations/{data.orgId}/landing">
|
||||
|
|
@ -146,7 +148,7 @@
|
|||
</div>
|
||||
{:else if activeTab === 'members'}
|
||||
<Card>
|
||||
<div class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div class="py-8 text-center text-sm text-muted-foreground dark:text-muted-foreground">
|
||||
Member management coming soon.
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
{#snippet actions()}
|
||||
<a
|
||||
href="/organizations/{data.orgId}"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
class="text-sm text-muted-foreground hover:text-muted-foreground dark:text-muted-foreground dark:hover:text-foreground"
|
||||
>
|
||||
Back to {org.name}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
>
|
||||
<button
|
||||
onclick={() => handleRestore(img)}
|
||||
class="rounded-lg bg-white/90 p-2 text-foreground hover:bg-white transition-colors"
|
||||
class="rounded-lg bg-muted/90 p-2 text-foreground hover:bg-white transition-colors"
|
||||
title="Wiederherstellen"
|
||||
>
|
||||
<ArrowCounterClockwise size={18} />
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
>
|
||||
<h3 class="font-semibold text-white truncate">{plant.name}</h3>
|
||||
{#if plant.commonName}
|
||||
<p class="text-xs text-white/70 truncate">{plant.commonName}</p>
|
||||
<p class="text-xs text-foreground truncate">{plant.commonName}</p>
|
||||
{/if}
|
||||
{#if getWateringText(plant.id)}
|
||||
<div class="water-status {getWateringClass(plant.id)} mt-1">
|
||||
|
|
|
|||
|
|
@ -61,8 +61,10 @@
|
|||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">My Presentations</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-1">Create and manage your slide decks</p>
|
||||
<h1 class="text-2xl font-bold text-foreground dark:text-white">My Presentations</h1>
|
||||
<p class="text-muted-foreground/70 dark:text-muted-foreground mt-1">
|
||||
Create and manage your slide decks
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (showCreateModal = true)}
|
||||
|
|
@ -76,12 +78,14 @@
|
|||
{#if decks.length === 0}
|
||||
<div class="text-center py-16">
|
||||
<div
|
||||
class="mx-auto w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mb-4"
|
||||
class="mx-auto w-16 h-16 bg-muted dark:bg-card rounded-full flex items-center justify-center mb-4"
|
||||
>
|
||||
<Presentation class="w-8 h-8 text-slate-400" />
|
||||
<Presentation class="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 class="text-lg font-medium text-slate-900 dark:text-white mb-2">No presentations yet</h2>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4">Create your first deck to get started</p>
|
||||
<h2 class="text-lg font-medium text-foreground dark:text-white mb-2">No presentations yet</h2>
|
||||
<p class="text-muted-foreground/70 dark:text-muted-foreground mb-4">
|
||||
Create your first deck to get started
|
||||
</p>
|
||||
<button
|
||||
onclick={() => (showCreateModal = true)}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
|
||||
|
|
@ -94,22 +98,26 @@
|
|||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{#each decks as deck (deck.id)}
|
||||
<div
|
||||
class="group bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden hover:shadow-md transition-shadow"
|
||||
class="group bg-white dark:bg-card rounded-xl shadow-sm border border-border-strong dark:border-border overflow-hidden hover:shadow-md transition-shadow"
|
||||
>
|
||||
<a href="/presi/deck/{deck.id}" class="block">
|
||||
<div
|
||||
class="aspect-video bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center"
|
||||
>
|
||||
<Presentation class="w-12 h-12 text-white/80" />
|
||||
<Presentation class="w-12 h-12 text-foreground" />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-slate-900 dark:text-white truncate">{deck.title}</h3>
|
||||
<h3 class="font-semibold text-foreground dark:text-white truncate">{deck.title}</h3>
|
||||
{#if deck.description}
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-sm text-muted-foreground/70 dark:text-muted-foreground mt-1 line-clamp-2"
|
||||
>
|
||||
{deck.description}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex items-center gap-4 mt-3 text-xs text-slate-500 dark:text-slate-400">
|
||||
<div
|
||||
class="flex items-center gap-4 mt-3 text-xs text-muted-foreground dark:text-muted-foreground"
|
||||
>
|
||||
<span class="flex items-center gap-1">
|
||||
<Clock class="w-3.5 h-3.5" />
|
||||
{formatDate(deck.updatedAt)}
|
||||
|
|
@ -123,7 +131,7 @@
|
|||
e.preventDefault();
|
||||
confirmDelete({ id: deck.id, title: deck.title });
|
||||
}}
|
||||
class="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
|
||||
class="p-2 text-muted-foreground hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Trash class="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -137,15 +145,17 @@
|
|||
<!-- Create Deck Modal -->
|
||||
{#if showCreateModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl w-full max-w-md">
|
||||
<div class="bg-white dark:bg-card rounded-xl shadow-xl w-full max-w-md">
|
||||
<form onsubmit={handleCreateDeck}>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-4">Create New Deck</h2>
|
||||
<h2 class="text-xl font-semibold text-foreground dark:text-white mb-4">
|
||||
Create New Deck
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="title"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-1"
|
||||
>Title</label
|
||||
>
|
||||
<input
|
||||
|
|
@ -153,31 +163,31 @@
|
|||
id="title"
|
||||
bind:value={newDeckTitle}
|
||||
required
|
||||
class="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
class="w-full px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="My Presentation"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="description"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-1"
|
||||
>Description (optional)</label
|
||||
>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={newDeckDescription}
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
|
||||
class="w-full px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
|
||||
placeholder="What is this presentation about?"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-xl">
|
||||
<div class="px-6 py-4 bg-muted dark:bg-card/50 flex justify-end gap-3 rounded-b-xl">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showCreateModal = false)}
|
||||
class="px-4 py-2 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
||||
class="px-4 py-2 text-muted-foreground dark:text-foreground/90 hover:bg-muted dark:hover:bg-muted rounded-lg transition-colors"
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
|
|
@ -195,9 +205,9 @@
|
|||
<!-- Delete Modal -->
|
||||
{#if showDeleteModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl w-full max-w-md p-6">
|
||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-2">Delete Deck</h2>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
||||
<div class="bg-white dark:bg-card rounded-xl shadow-xl w-full max-w-md p-6">
|
||||
<h2 class="text-xl font-semibold text-foreground dark:text-white mb-2">Delete Deck</h2>
|
||||
<p class="text-muted-foreground/70 dark:text-muted-foreground mb-6">
|
||||
Are you sure you want to delete "{deckToDelete?.title}"?
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
|
|
@ -206,7 +216,7 @@
|
|||
showDeleteModal = false;
|
||||
deckToDelete = null;
|
||||
}}
|
||||
class="px-4 py-2 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
||||
class="px-4 py-2 text-muted-foreground dark:text-foreground/90 hover:bg-muted dark:hover:bg-muted rounded-lg transition-colors"
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -132,29 +132,28 @@
|
|||
{#if currentDeck}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="/presi"
|
||||
class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft class="w-5 h-5 text-slate-600 dark:text-slate-400" />
|
||||
<a href="/presi" class="p-2 hover:bg-muted dark:hover:bg-card rounded-lg transition-colors">
|
||||
<ArrowLeft class="w-5 h-5 text-muted-foreground/70 dark:text-muted-foreground" />
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">{currentDeck.title}</h1>
|
||||
<h1 class="text-2xl font-bold text-foreground dark:text-white">{currentDeck.title}</h1>
|
||||
{#if currentDeck.description}
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-1">{currentDeck.description}</p>
|
||||
<p class="text-muted-foreground/70 dark:text-muted-foreground mt-1">
|
||||
{currentDeck.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
onclick={openCreateSlide}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 font-medium rounded-lg transition-colors"
|
||||
class="flex items-center gap-2 px-4 py-2 bg-muted dark:bg-muted hover:bg-muted dark:hover:bg-muted text-muted-foreground dark:text-foreground font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<Plus class="w-5 h-5" /> Add Slide
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showShare = true)}
|
||||
class="rounded-lg p-2 text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors"
|
||||
class="rounded-lg p-2 text-muted-foreground dark:text-muted-foreground hover:text-muted-foreground dark:hover:text-foreground transition-colors"
|
||||
title="Kurzlink teilen"
|
||||
>
|
||||
<ShareNetwork size={20} />
|
||||
|
|
@ -173,11 +172,11 @@
|
|||
{#if currentSlides.length === 0}
|
||||
<div class="text-center py-16">
|
||||
<div
|
||||
class="mx-auto w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mb-4"
|
||||
class="mx-auto w-16 h-16 bg-muted dark:bg-card rounded-full flex items-center justify-center mb-4"
|
||||
>
|
||||
<TextT class="w-8 h-8 text-slate-400" />
|
||||
<TextT class="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 class="text-lg font-medium text-slate-900 dark:text-white mb-2">No slides yet</h2>
|
||||
<h2 class="text-lg font-medium text-foreground dark:text-white mb-2">No slides yet</h2>
|
||||
<button
|
||||
onclick={openCreateSlide}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
|
||||
|
|
@ -189,11 +188,11 @@
|
|||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{#each currentSlides as slide, index (slide.id)}
|
||||
<div
|
||||
class="group bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
|
||||
class="group bg-white dark:bg-card rounded-xl shadow-sm border border-border-strong dark:border-border overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onclick={() => openEditSlide(slide)}
|
||||
class="w-full aspect-video bg-slate-100 dark:bg-slate-700 p-4 flex flex-col items-center justify-center text-left"
|
||||
class="w-full aspect-video bg-muted dark:bg-muted p-4 flex flex-col items-center justify-center text-left"
|
||||
>
|
||||
{#if slide.content.imageUrl}
|
||||
<img
|
||||
|
|
@ -204,12 +203,14 @@
|
|||
{:else}
|
||||
<div class="w-full h-full flex flex-col items-center justify-center p-4">
|
||||
{#if slide.content.title}<h3
|
||||
class="text-lg font-semibold text-slate-900 dark:text-white text-center line-clamp-2"
|
||||
class="text-lg font-semibold text-foreground dark:text-white text-center line-clamp-2"
|
||||
>
|
||||
{slide.content.title}
|
||||
</h3>{/if}
|
||||
{#if slide.content.bulletPoints?.length}
|
||||
<ul class="mt-2 text-sm text-slate-600 dark:text-slate-400 space-y-1">
|
||||
<ul
|
||||
class="mt-2 text-sm text-muted-foreground/70 dark:text-muted-foreground space-y-1"
|
||||
>
|
||||
{#each slide.content.bulletPoints.slice(0, 3) as point}<li class="truncate">
|
||||
• {point}
|
||||
</li>{/each}
|
||||
|
|
@ -219,28 +220,34 @@
|
|||
{/if}
|
||||
</button>
|
||||
<div
|
||||
class="p-3 flex items-center justify-between border-t border-slate-200 dark:border-slate-700"
|
||||
class="p-3 flex items-center justify-between border-t border-border-strong dark:border-border"
|
||||
>
|
||||
<span class="text-sm text-slate-500">Slide {index + 1}</span>
|
||||
<span class="text-sm text-muted-foreground">Slide {index + 1}</span>
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<button
|
||||
onclick={() => moveSlide(slide, 'up')}
|
||||
disabled={index === 0}
|
||||
class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded disabled:opacity-30"
|
||||
><CaretUp class="w-4 h-4 text-slate-600 dark:text-slate-400" /></button
|
||||
class="p-1.5 hover:bg-muted dark:hover:bg-muted rounded disabled:opacity-30"
|
||||
><CaretUp
|
||||
class="w-4 h-4 text-muted-foreground/70 dark:text-muted-foreground"
|
||||
/></button
|
||||
>
|
||||
<button
|
||||
onclick={() => moveSlide(slide, 'down')}
|
||||
disabled={index === currentSlides.length - 1}
|
||||
class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded disabled:opacity-30"
|
||||
><CaretDown class="w-4 h-4 text-slate-600 dark:text-slate-400" /></button
|
||||
class="p-1.5 hover:bg-muted dark:hover:bg-muted rounded disabled:opacity-30"
|
||||
><CaretDown
|
||||
class="w-4 h-4 text-muted-foreground/70 dark:text-muted-foreground"
|
||||
/></button
|
||||
>
|
||||
<button
|
||||
onclick={() => openEditSlide(slide)}
|
||||
class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
|
||||
><PencilSimple class="w-4 h-4 text-slate-600 dark:text-slate-400" /></button
|
||||
class="p-1.5 hover:bg-muted dark:hover:bg-muted rounded"
|
||||
><PencilSimple
|
||||
class="w-4 h-4 text-muted-foreground/70 dark:text-muted-foreground"
|
||||
/></button
|
||||
>
|
||||
<button
|
||||
onclick={() => confirmDeleteSlide(slide)}
|
||||
|
|
@ -259,39 +266,40 @@
|
|||
<!-- Slide Editor Modal -->
|
||||
{#if showSlideModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl w-full max-w-2xl my-8">
|
||||
<div class="bg-white dark:bg-card rounded-xl shadow-xl w-full max-w-2xl my-8">
|
||||
<form onsubmit={handleSaveSlide}>
|
||||
<div
|
||||
class="p-6 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between"
|
||||
class="p-6 border-b border-border-strong dark:border-border flex items-center justify-between"
|
||||
>
|
||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
<h2 class="text-xl font-semibold text-foreground dark:text-white">
|
||||
{editingSlide ? 'Edit Slide' : 'New Slide'}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showSlideModal = false)}
|
||||
class="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg"
|
||||
><X class="w-5 h-5 text-slate-600 dark:text-slate-400" /></button
|
||||
class="p-2 hover:bg-muted dark:hover:bg-muted rounded-lg"
|
||||
><X class="w-5 h-5 text-muted-foreground/70 dark:text-muted-foreground" /></button
|
||||
>
|
||||
</div>
|
||||
<div class="p-6 space-y-6 max-h-[60vh] overflow-y-auto">
|
||||
<div>
|
||||
<label
|
||||
for="slideTitle"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Title</label
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-1"
|
||||
>Title</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="slideTitle"
|
||||
bind:value={slideTitle}
|
||||
class="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
class="w-full px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Slide title"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="slideImage"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-1"
|
||||
><span class="flex items-center gap-2"
|
||||
><Image class="w-4 h-4" /> Image URL (optional)</span
|
||||
></label
|
||||
|
|
@ -300,38 +308,39 @@
|
|||
type="url"
|
||||
id="slideImage"
|
||||
bind:value={slideImageUrl}
|
||||
class="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
class="w-full px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="slideBody"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-1"
|
||||
>Body Text (optional)</label
|
||||
>
|
||||
<textarea
|
||||
id="slideBody"
|
||||
bind:value={slideBody}
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 resize-none"
|
||||
class="w-full px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500 resize-none"
|
||||
placeholder="Main content text..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
<label
|
||||
class="block text-sm font-medium text-muted-foreground dark:text-foreground/90 mb-2"
|
||||
><span class="flex items-center gap-2"><List class="w-4 h-4" /> Bullet Points</span
|
||||
></label
|
||||
>
|
||||
<div class="space-y-2">
|
||||
{#each slideBulletPoints as point, index}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-slate-400">•</span>
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<input
|
||||
type="text"
|
||||
value={point}
|
||||
oninput={(e) => updateBulletPoint(index, (e.target as HTMLInputElement).value)}
|
||||
class="flex-1 px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
class="flex-1 px-4 py-2 border border-border-strong dark:border-border rounded-lg bg-white dark:bg-muted text-foreground dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Add a point..."
|
||||
/>
|
||||
<button
|
||||
|
|
@ -351,11 +360,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-xl">
|
||||
<div class="px-6 py-4 bg-muted dark:bg-card/50 flex justify-end gap-3 rounded-b-xl">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showSlideModal = false)}
|
||||
class="px-4 py-2 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
||||
class="px-4 py-2 text-muted-foreground dark:text-foreground/90 hover:bg-muted dark:hover:bg-muted rounded-lg transition-colors"
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
|
|
@ -373,9 +382,9 @@
|
|||
<!-- Delete Slide Modal -->
|
||||
{#if showDeleteModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl w-full max-w-md p-6">
|
||||
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-2">Delete Slide</h2>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
||||
<div class="bg-white dark:bg-card rounded-xl shadow-xl w-full max-w-md p-6">
|
||||
<h2 class="text-xl font-semibold text-foreground dark:text-white mb-2">Delete Slide</h2>
|
||||
<p class="text-muted-foreground/70 dark:text-muted-foreground mb-6">
|
||||
Are you sure you want to delete this slide?
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
|
|
@ -384,7 +393,7 @@
|
|||
showDeleteModal = false;
|
||||
slideToDelete = null;
|
||||
}}
|
||||
class="px-4 py-2 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
||||
class="px-4 py-2 text-muted-foreground dark:text-foreground/90 hover:bg-muted dark:hover:bg-muted rounded-lg transition-colors"
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
|
||||
<svelte:head><title>Presenting: {currentDeck?.title || 'Loading...'}</title></svelte:head>
|
||||
|
||||
<div class="fixed inset-0 bg-slate-900 text-white flex flex-col">
|
||||
<div class="fixed inset-0 bg-card text-white flex flex-col">
|
||||
{#if currentSlide}
|
||||
<div
|
||||
class="absolute top-0 left-0 right-0 z-10 p-4 flex items-center justify-between bg-gradient-to-b from-black/50 to-transparent transition-opacity duration-300"
|
||||
|
|
@ -125,18 +125,18 @@
|
|||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-lg font-medium truncate max-w-xs">{currentDeck?.title}</h1>
|
||||
<span class="text-sm text-slate-400"
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>Slide {currentSlideIndex + 1} of {currentSlides.length}</span
|
||||
>
|
||||
</div>
|
||||
<button onclick={exitPresentation} class="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
<button onclick={exitPresentation} class="p-2 hover:bg-muted/10 rounded-lg transition-colors"
|
||||
><X class="w-6 h-6" /></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center p-8 pt-20 pb-32">
|
||||
<div
|
||||
class="w-full max-w-6xl aspect-video bg-slate-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col items-center justify-center p-12"
|
||||
class="w-full max-w-6xl aspect-video bg-card rounded-2xl shadow-2xl overflow-hidden flex flex-col items-center justify-center p-12"
|
||||
>
|
||||
{#if currentSlide.content.imageUrl}
|
||||
<img
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
>
|
||||
{currentSlide.content.title}
|
||||
</h2>{/if}
|
||||
{#if currentSlide.content.body}<p class="text-xl md:text-2xl text-slate-300 mb-8">
|
||||
{#if currentSlide.content.body}<p class="text-xl md:text-2xl text-foreground/90 mb-8">
|
||||
{currentSlide.content.body}
|
||||
</p>{/if}
|
||||
{#if currentSlide.content.bulletPoints?.length}
|
||||
|
|
@ -175,10 +175,10 @@
|
|||
>
|
||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<button onclick={toggleTimer} class="p-2 hover:bg-white/10 rounded-lg transition-colors">
|
||||
<button onclick={toggleTimer} class="p-2 hover:bg-muted/10 rounded-lg transition-colors">
|
||||
{#if isTimerRunning}<Pause class="w-5 h-5" />{:else}<Play class="w-5 h-5" />{/if}
|
||||
</button>
|
||||
<div class="flex items-center gap-2 text-slate-300">
|
||||
<div class="flex items-center gap-2 text-foreground/90">
|
||||
<Clock class="w-4 h-4" /><span class="font-mono">{formatTime(elapsedSeconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
<button
|
||||
onclick={prevSlide}
|
||||
disabled={currentSlideIndex === 0}
|
||||
class="p-3 hover:bg-white/10 rounded-lg transition-colors disabled:opacity-30"
|
||||
class="p-3 hover:bg-muted/10 rounded-lg transition-colors disabled:opacity-30"
|
||||
><CaretLeft class="w-6 h-6" /></button
|
||||
>
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
|
|
@ -197,27 +197,27 @@
|
|||
class="w-2 h-2 rounded-full transition-all"
|
||||
class:bg-primary-500={index === currentSlideIndex}
|
||||
class:w-4={index === currentSlideIndex}
|
||||
class:bg-slate-500={index !== currentSlideIndex}
|
||||
class:bg-muted={index !== currentSlideIndex}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
onclick={nextSlide}
|
||||
disabled={currentSlideIndex === currentSlides.length - 1}
|
||||
class="p-3 hover:bg-white/10 rounded-lg transition-colors disabled:opacity-30"
|
||||
class="p-3 hover:bg-muted/10 rounded-lg transition-colors disabled:opacity-30"
|
||||
><CaretRight class="w-6 h-6" /></button
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={() => (showNotes = !showNotes)}
|
||||
class="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
class="p-2 hover:bg-muted/10 rounded-lg transition-colors"
|
||||
>
|
||||
{#if showNotes}<EyeSlash class="w-5 h-5" />{:else}<Eye class="w-5 h-5" />{/if}
|
||||
</button>
|
||||
<button
|
||||
onclick={toggleFullscreen}
|
||||
class="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
class="p-2 hover:bg-muted/10 rounded-lg transition-colors"
|
||||
>
|
||||
{#if isFullscreen}<ArrowsIn class="w-5 h-5" />{:else}<ArrowsOut class="w-5 h-5" />{/if}
|
||||
</button>
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<p class="text-slate-400">No slides in this deck</p>
|
||||
<p class="text-muted-foreground">No slides in this deck</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@
|
|||
);
|
||||
|
||||
const statusIcons = {
|
||||
open: { icon: Clock, color: 'text-gray-500' },
|
||||
open: { icon: Clock, color: 'text-muted-foreground' },
|
||||
researching: { icon: CircleNotch, color: 'text-blue-500' },
|
||||
answered: { icon: CheckCircle, color: 'text-green-500' },
|
||||
archived: { icon: Archive, color: 'text-gray-400' },
|
||||
archived: { icon: Archive, color: 'text-muted-foreground' },
|
||||
};
|
||||
|
||||
const depthLabels: Record<ResearchDepth, string> = {
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
<div class="space-y-3">
|
||||
{#each filteredQuestions as question (question.id)}
|
||||
{@const StatusIcon = statusIcons[question.status]?.icon || Clock}
|
||||
{@const statusColor = statusIcons[question.status]?.color || 'text-gray-500'}
|
||||
{@const statusColor = statusIcons[question.status]?.color || 'text-muted-foreground'}
|
||||
|
||||
<a
|
||||
href="/questions/{question.id}"
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: question.priority === 'high'
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'}"
|
||||
: 'bg-muted text-muted-foreground/70 dark:bg-card dark:text-muted-foreground'}"
|
||||
>
|
||||
{question.priority}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
const statusLabels: Record<string, { label: string; color: string }> = {
|
||||
open: {
|
||||
label: 'Offen',
|
||||
color: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
||||
color: 'bg-muted text-muted-foreground dark:bg-card dark:text-foreground/90',
|
||||
},
|
||||
researching: {
|
||||
label: 'Recherche',
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
},
|
||||
archived: {
|
||||
label: 'Archiviert',
|
||||
color: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
|
||||
color: 'bg-muted text-muted-foreground dark:bg-card dark:text-muted-foreground',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
>
|
||||
<div class="text-4xl mb-3">{data.icon}</div>
|
||||
<h2 class="text-xl font-semibold mb-1">{$_(data.labelKey)}</h2>
|
||||
<p class="text-white/80 text-sm">
|
||||
<p class="text-foreground text-sm">
|
||||
{$_('categories.quotes', { values: { count: data.count } })}
|
||||
</p>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -125,9 +125,8 @@
|
|||
}
|
||||
|
||||
async function handleExport() {
|
||||
const { skillTable, activityTable, achievementTable } = await import(
|
||||
'$lib/modules/skilltree/collections'
|
||||
);
|
||||
const { skillTable, activityTable, achievementTable } =
|
||||
await import('$lib/modules/skilltree/collections');
|
||||
const [allSkillsData, allActivitiesData, allAchievementsData] = await Promise.all([
|
||||
skillTable.toArray(),
|
||||
activityTable.toArray(),
|
||||
|
|
@ -159,9 +158,8 @@
|
|||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
const { skillTable, activityTable, achievementTable } = await import(
|
||||
'$lib/modules/skilltree/collections'
|
||||
);
|
||||
const { skillTable, activityTable, achievementTable } =
|
||||
await import('$lib/modules/skilltree/collections');
|
||||
if (data.skills) await skillTable.bulkPut(data.skills);
|
||||
if (data.activities) await activityTable.bulkPut(data.activities);
|
||||
if (data.achievements) await achievementTable.bulkPut(data.achievements);
|
||||
|
|
@ -181,7 +179,7 @@
|
|||
|
||||
<div class="min-h-screen">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-gray-800 bg-gray-900/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<div class="mx-auto max-w-7xl px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -192,13 +190,13 @@
|
|||
<!-- Achievements -->
|
||||
<a
|
||||
href="/skilltree/achievements"
|
||||
class="relative rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-yellow-400"
|
||||
class="relative rounded-lg p-2 text-muted-foreground transition-colors hover:bg-card hover:text-yellow-400"
|
||||
title="Achievements"
|
||||
>
|
||||
<Trophy class="h-5 w-5" />
|
||||
{#if achievementStats.unlocked > 0}
|
||||
<span
|
||||
class="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-yellow-500 text-[10px] font-bold text-gray-900"
|
||||
class="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-yellow-500 text-[10px] font-bold text-foreground"
|
||||
>
|
||||
{achievementStats.unlocked}
|
||||
</span>
|
||||
|
|
@ -207,7 +205,7 @@
|
|||
<!-- Tree View -->
|
||||
<a
|
||||
href="/skilltree/tree"
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-emerald-400"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-card hover:text-emerald-400"
|
||||
title="Skill-Tree Ansicht"
|
||||
>
|
||||
<Graph class="h-5 w-5" />
|
||||
|
|
@ -215,7 +213,7 @@
|
|||
<!-- Templates -->
|
||||
<button
|
||||
onclick={() => (showTemplatesModal = true)}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-yellow-500"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-card hover:text-yellow-500"
|
||||
title="Skill-Vorlagen"
|
||||
>
|
||||
<Sparkle class="h-5 w-5" />
|
||||
|
|
@ -223,14 +221,14 @@
|
|||
<!-- Export/Import -->
|
||||
<button
|
||||
onclick={handleExport}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-card hover:text-white"
|
||||
title="Daten exportieren"
|
||||
>
|
||||
<DownloadSimple class="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleImport}
|
||||
class="rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-card hover:text-white"
|
||||
title="Daten importieren"
|
||||
>
|
||||
<UploadSimple class="h-5 w-5" />
|
||||
|
|
@ -260,7 +258,7 @@
|
|||
class="rounded-full px-4 py-2 text-sm font-medium transition-colors {selectedBranch ===
|
||||
'all'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
|
||||
: 'bg-card text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
Alle ({skills.length})
|
||||
</button>
|
||||
|
|
@ -272,7 +270,7 @@
|
|||
class="rounded-full px-4 py-2 text-sm font-medium transition-colors {selectedBranch ===
|
||||
branch
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
|
||||
: 'bg-card text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
{info.name} ({count})
|
||||
</button>
|
||||
|
|
@ -284,13 +282,13 @@
|
|||
<!-- Skills Grid -->
|
||||
{#if filteredSkills.length === 0}
|
||||
<div class="mt-16 text-center">
|
||||
<div
|
||||
class="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-gray-800"
|
||||
>
|
||||
<Tree class="h-12 w-12 text-gray-600" />
|
||||
<div class="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-card">
|
||||
<Tree class="h-12 w-12 text-muted-foreground/70" />
|
||||
</div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-gray-300">Noch keine Skills</h2>
|
||||
<p class="mb-6 text-gray-500">Füge deinen ersten Skill hinzu und beginne dein Abenteuer!</p>
|
||||
<h2 class="mb-2 text-xl font-semibold text-foreground/90">Noch keine Skills</h2>
|
||||
<p class="mb-6 text-muted-foreground">
|
||||
Füge deinen ersten Skill hinzu und beginne dein Abenteuer!
|
||||
</p>
|
||||
<button
|
||||
onclick={() => (showAddSkillModal = true)}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-6 py-3 font-medium text-white transition-colors hover:bg-emerald-500"
|
||||
|
|
@ -323,7 +321,7 @@
|
|||
{#each getRecentActivities(activities).slice(0, 5) as activity}
|
||||
{@const skill = getSkillById(skills, activity.skillId)}
|
||||
{#if skill}
|
||||
<div class="flex items-center justify-between rounded-lg bg-gray-800/50 px-4 py-3">
|
||||
<div class="flex items-center justify-between rounded-lg bg-card/50 px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-900/50 text-sm font-medium text-emerald-400"
|
||||
|
|
@ -332,10 +330,10 @@
|
|||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-white">{skill.name}</span>
|
||||
<span class="text-gray-400"> - {activity.description}</span>
|
||||
<span class="text-muted-foreground"> - {activity.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{new Date(activity.timestamp).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@
|
|||
|
||||
<div class="min-h-screen">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-gray-800 bg-gray-900/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<div class="mx-auto max-w-7xl px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<a
|
||||
href="/skilltree"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-muted-foreground transition-colors hover:bg-card hover:text-white"
|
||||
>
|
||||
<ArrowLeft class="h-5 w-5" />
|
||||
</a>
|
||||
|
|
@ -71,12 +71,12 @@
|
|||
|
||||
<main class="mx-auto max-w-7xl px-4 py-8">
|
||||
<!-- Progress overview -->
|
||||
<div class="mb-8 rounded-xl border border-gray-700 bg-gray-800/50 p-6">
|
||||
<div class="mb-8 rounded-xl border border-border bg-card/50 p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-white">Fortschritt</h2>
|
||||
<span class="text-2xl font-bold text-yellow-400">{completion}%</span>
|
||||
</div>
|
||||
<div class="h-3 overflow-hidden rounded-full bg-gray-700">
|
||||
<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"
|
||||
style="width: {completion}%"
|
||||
|
|
@ -100,8 +100,8 @@
|
|||
onclick={() => (selectedCategory = 'all')}
|
||||
class="rounded-full px-4 py-2 text-sm font-medium transition-colors {selectedCategory ===
|
||||
'all'
|
||||
? 'bg-yellow-500 text-gray-900'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
|
||||
? 'bg-yellow-500 text-foreground'
|
||||
: 'bg-card text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
Alle ({achievements.length})
|
||||
</button>
|
||||
|
|
@ -111,8 +111,8 @@
|
|||
onclick={() => (selectedCategory = category)}
|
||||
class="rounded-full px-4 py-2 text-sm font-medium transition-colors {selectedCategory ===
|
||||
category
|
||||
? 'bg-yellow-500 text-gray-900'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
|
||||
? 'bg-yellow-500 text-foreground'
|
||||
: 'bg-card text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
{info.name} ({count})
|
||||
</button>
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
onclick={() => (showOnlyUnlocked = !showOnlyUnlocked)}
|
||||
class="rounded-full px-4 py-2 text-sm font-medium transition-colors {showOnlyUnlocked
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'}"
|
||||
: 'bg-card text-foreground/90 hover:bg-muted'}"
|
||||
>
|
||||
{showOnlyUnlocked ? 'Nur freigeschaltete' : 'Alle zeigen'}
|
||||
</button>
|
||||
|
|
@ -133,13 +133,11 @@
|
|||
<!-- Achievement grid -->
|
||||
{#if filteredAchievements().length === 0}
|
||||
<div class="mt-16 text-center">
|
||||
<div
|
||||
class="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-gray-800"
|
||||
>
|
||||
<Trophy class="h-12 w-12 text-gray-600" />
|
||||
<div class="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-card">
|
||||
<Trophy class="h-12 w-12 text-muted-foreground/70" />
|
||||
</div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-gray-300">Keine Achievements gefunden</h2>
|
||||
<p class="text-gray-500">
|
||||
<h2 class="mb-2 text-xl font-semibold text-foreground/90">Keine Achievements gefunden</h2>
|
||||
<p class="text-muted-foreground">
|
||||
{showOnlyUnlocked
|
||||
? 'Du hast in dieser Kategorie noch keine Achievements freigeschaltet.'
|
||||
: 'Keine Achievements in dieser Kategorie.'}
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@
|
|||
<title>Skill Tree View - SkillTree</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-gray-900 text-white">
|
||||
<div class="min-h-screen bg-card text-white">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-gray-800 bg-gray-900/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<div class="mx-auto max-w-7xl px-4 py-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="/skilltree"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-muted-foreground transition-colors hover:bg-card hover:text-white"
|
||||
>
|
||||
<ArrowLeft class="h-5 w-5" />
|
||||
Zurück
|
||||
|
|
@ -79,7 +79,9 @@
|
|||
<main class="p-4">
|
||||
{#if skills.length === 0}
|
||||
<div class="mt-16 text-center">
|
||||
<p class="text-gray-400">Noch keine Skills vorhanden. Erstelle zuerst einige Skills!</p>
|
||||
<p class="text-muted-foreground">
|
||||
Noch keine Skills vorhanden. Erstelle zuerst einige Skills!
|
||||
</p>
|
||||
<a
|
||||
href="/skilltree"
|
||||
class="mt-4 inline-block rounded-lg bg-emerald-600 px-4 py-2 font-medium text-white hover:bg-emerald-500"
|
||||
|
|
@ -93,7 +95,7 @@
|
|||
{#each Object.entries(BRANCH_INFO) as [branch, info]}
|
||||
{@const count = skills.filter((s) => s.branch === branch).length}
|
||||
{#if count > 0}
|
||||
<div class="flex items-center gap-2 rounded-full bg-gray-800 px-3 py-1.5 text-sm">
|
||||
<div class="flex items-center gap-2 rounded-full bg-card px-3 py-1.5 text-sm">
|
||||
<span class="h-3 w-3 rounded-full" style="background-color: {info.color}"></span>
|
||||
{info.name} ({count})
|
||||
</div>
|
||||
|
|
@ -255,7 +257,7 @@
|
|||
>
|
||||
{level}
|
||||
</div>
|
||||
<span class="text-gray-400">{name}</span>
|
||||
<span class="text-muted-foreground">{name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
<Card>
|
||||
<div class="py-12 text-center">
|
||||
<span class="mb-4 block text-6xl">👥</span>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No teams yet</h3>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<h3 class="mb-2 text-lg font-semibold text-foreground dark:text-white">No teams yet</h3>
|
||||
<p class="mb-6 text-sm text-muted-foreground dark:text-muted-foreground">
|
||||
Create or join a team to start collaborating
|
||||
</p>
|
||||
<Button variant="primary">Create Team</Button>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
style="background-color: {project.color}"
|
||||
></div>
|
||||
{:else}
|
||||
<div class="h-3 w-3 shrink-0 rounded-full bg-gray-400"></div>
|
||||
<div class="h-3 w-3 shrink-0 rounded-full bg-muted"></div>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-[hsl(var(--color-foreground))]">{template.name}</p>
|
||||
|
|
|
|||
|
|
@ -248,9 +248,9 @@
|
|||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full rounded-lg border border-gray-300 bg-white px-4 py-3 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-gray-600 dark:bg-gray-700';
|
||||
'w-full rounded-lg border border-border-strong bg-white px-4 py-3 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-200 dark:border-border dark:bg-muted';
|
||||
const inputSmClass =
|
||||
'w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700';
|
||||
'w-full rounded-lg border border-border-strong bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -273,7 +273,7 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/uload/links"
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-3 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Alle Links
|
||||
</a>
|
||||
|
|
@ -289,7 +289,7 @@
|
|||
<!-- Create Form -->
|
||||
{#if showCreateForm}
|
||||
<div
|
||||
class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
||||
class="mb-6 rounded-xl border border-border-strong bg-white p-6 shadow-sm dark:border-border dark:bg-card"
|
||||
>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
|
|
@ -447,7 +447,7 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Links durchsuchen..."
|
||||
class="w-60 rounded-lg border border-gray-300 bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-60 rounded-lg border border-border-strong bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||
|
|
@ -469,12 +469,12 @@
|
|||
{#if allLinks.loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(3) as _}
|
||||
<div class="h-20 animate-pulse rounded-xl bg-gray-100 dark:bg-gray-800"></div>
|
||||
<div class="h-20 animate-pulse rounded-xl bg-muted dark:bg-card"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if filteredLinks.length === 0}
|
||||
<div
|
||||
class="rounded-xl border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-600"
|
||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<LinkIcon size={48} class="mx-auto mb-4 opacity-20" />
|
||||
<p class="text-lg font-medium opacity-60">Noch keine Links</p>
|
||||
|
|
@ -490,7 +490,7 @@
|
|||
<div class="space-y-3">
|
||||
{#each filteredLinks as link (link.id)}
|
||||
<div
|
||||
class="group rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
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"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
|
|
@ -498,7 +498,7 @@
|
|||
<span
|
||||
class="inline-block h-2 w-2 shrink-0 rounded-full {link.isActive
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-400'}"
|
||||
: 'bg-muted'}"
|
||||
></span>
|
||||
<h3 class="truncate font-semibold">{link.title || link.shortCode}</h3>
|
||||
<span
|
||||
|
|
@ -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-gray-100 hover:opacity-100 dark:hover:bg-gray-700"
|
||||
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"
|
||||
title="Analytics"
|
||||
>
|
||||
<ChartBar size={16} />
|
||||
|
|
@ -553,31 +553,34 @@
|
|||
</a>
|
||||
<button
|
||||
onclick={() => copyShortUrl(link.shortCode)}
|
||||
class="rounded-lg p-2 opacity-0 transition-all hover:bg-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all 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-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all 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-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all 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-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
||||
>
|
||||
<Lightning size={16} class={link.isActive ? 'text-green-500' : 'text-gray-400'} />
|
||||
<Lightning
|
||||
size={16}
|
||||
class={link.isActive ? 'text-green-500' : 'text-muted-foreground'}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => deleteLink(link)}
|
||||
|
|
@ -605,7 +608,7 @@
|
|||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
class="w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="none"
|
||||
>
|
||||
|
|
@ -613,7 +616,7 @@
|
|||
<h3 class="text-lg font-semibold">Link bearbeiten</h3>
|
||||
<button
|
||||
onclick={() => (editingLink = null)}
|
||||
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
|
@ -636,7 +639,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
||||
<p class="mb-2 text-sm font-medium opacity-70">UTM-Parameter</p>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<input
|
||||
|
|
@ -660,7 +663,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
||||
<p class="mb-2 text-sm font-medium opacity-70">Erweitert</p>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
|
|
@ -701,7 +704,7 @@
|
|||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button
|
||||
onclick={() => (editingLink = null)}
|
||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
|
@ -727,7 +730,7 @@
|
|||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="none"
|
||||
>
|
||||
|
|
@ -735,7 +738,7 @@
|
|||
<h3 class="text-lg font-semibold">QR-Code</h3>
|
||||
<button
|
||||
onclick={() => (qrLink = null)}
|
||||
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
|
@ -753,7 +756,7 @@
|
|||
<div class="flex w-full gap-2">
|
||||
<button
|
||||
onclick={() => copyShortUrl(qrLink!.shortCode)}
|
||||
class="flex-1 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
class="flex-1 rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Link kopieren
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@
|
|||
<div class="mx-auto max-w-4xl p-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-white/5" title="Zurueck">
|
||||
<CaretLeft size={20} class="text-white/60" />
|
||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5" title="Zurueck">
|
||||
<CaretLeft size={20} class="text-muted-foreground" />
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Analytics</h1>
|
||||
{#if link}
|
||||
<p class="mt-1 text-sm text-white/50">
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
<span class="font-mono text-indigo-400">/{link.shortCode}</span>
|
||||
→ <span class="truncate">{link.originalUrl}</span>
|
||||
</p>
|
||||
|
|
@ -92,40 +92,40 @@
|
|||
{#if loading}
|
||||
<div class="space-y-4">
|
||||
{#each Array(4) as _}
|
||||
<div class="h-32 animate-pulse rounded-xl bg-white/5"></div>
|
||||
<div class="h-32 animate-pulse rounded-xl bg-muted/5"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !link}
|
||||
<div class="rounded-xl border border-white/10 p-12 text-center">
|
||||
<p class="text-white/50">Link nicht gefunden</p>
|
||||
<div class="rounded-xl border border-border/10 p-12 text-center">
|
||||
<p class="text-muted-foreground">Link nicht gefunden</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Stats Overview -->
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-white/40">Clicks</p>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Clicks</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">
|
||||
{stats?.totalClicks ?? link.clickCount}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-white/40">Unique</p>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Unique</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">
|
||||
{stats?.uniqueVisitors ?? '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-white/40">Status</p>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Status</p>
|
||||
<p class="mt-1 text-3xl font-bold">
|
||||
{#if link.isActive}
|
||||
<span class="text-green-400">Aktiv</span>
|
||||
{:else}
|
||||
<span class="text-white/30">Inaktiv</span>
|
||||
<span class="text-muted-foreground/70">Inaktiv</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-white/40">Erstellt</p>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Erstellt</p>
|
||||
<p class="mt-1 text-lg font-bold text-white">
|
||||
{new Date(link.createdAt).toLocaleDateString('de')}
|
||||
</p>
|
||||
|
|
@ -133,11 +133,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Link Details -->
|
||||
<div class="mb-6 rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Link Details</h2>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-white/50">Ziel-URL</span>
|
||||
<span class="text-muted-foreground">Ziel-URL</span>
|
||||
<a
|
||||
href={link.originalUrl}
|
||||
target="_blank"
|
||||
|
|
@ -149,31 +149,31 @@
|
|||
</div>
|
||||
{#if link.title}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-white/50">Titel</span>
|
||||
<span class="text-muted-foreground">Titel</span>
|
||||
<span class="text-white">{link.title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
||||
<div class="border-t border-white/10 pt-3">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-white/40">
|
||||
<div class="border-t border-border/10 pt-3">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
UTM-Parameter
|
||||
</p>
|
||||
<div class="grid gap-2 sm:grid-cols-3">
|
||||
{#if link.utmSource}
|
||||
<div class="text-sm text-white/70">
|
||||
<span class="text-white/40">Source:</span>
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Source:</span>
|
||||
{link.utmSource}
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmMedium}
|
||||
<div class="text-sm text-white/70">
|
||||
<span class="text-white/40">Medium:</span>
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Medium:</span>
|
||||
{link.utmMedium}
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmCampaign}
|
||||
<div class="text-sm text-white/70">
|
||||
<span class="text-white/40">Campaign:</span>
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Campaign:</span>
|
||||
{link.utmCampaign}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -182,19 +182,19 @@
|
|||
{/if}
|
||||
{#if link.expiresAt}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-white/50">Laeuft ab</span>
|
||||
<span class="text-muted-foreground">Laeuft ab</span>
|
||||
<span class="text-white">{new Date(link.expiresAt).toLocaleDateString('de')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.maxClicks}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-white/50">Max Klicks</span>
|
||||
<span class="text-muted-foreground">Max Klicks</span>
|
||||
<span class="text-white">{link.clickCount} / {link.maxClicks}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.password}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-white/50">Passwortgeschuetzt</span>
|
||||
<span class="text-muted-foreground">Passwortgeschuetzt</span>
|
||||
<span class="text-white">Ja</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="mb-6 rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Clicks ueber Zeit</h2>
|
||||
<div class="flex gap-1">
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
onclick={() => changeDays(d)}
|
||||
class="rounded-md px-3 py-1 text-xs font-medium transition-colors {days === d
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-white/10 text-white/60 hover:bg-white/15'}"
|
||||
: 'bg-muted/10 text-muted-foreground hover:bg-muted/15'}"
|
||||
>
|
||||
{d}T
|
||||
</button>
|
||||
|
|
@ -227,28 +227,30 @@
|
|||
style="height: {Math.max((day.count / maxTimelineCount) * 100, 2)}%"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute -top-8 hidden rounded bg-white/90 px-2 py-1 text-xs text-gray-900 group-hover:block"
|
||||
class="pointer-events-none absolute -top-8 hidden rounded bg-muted/90 px-2 py-1 text-xs text-foreground group-hover:block"
|
||||
>
|
||||
{day.count}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-xs text-white/30">
|
||||
<div class="mt-1 flex justify-between text-xs text-muted-foreground/70">
|
||||
<span>{timeline[0]?.date}</span>
|
||||
<span>{timeline[timeline.length - 1]?.date}</span>
|
||||
</div>
|
||||
{:else if !serverAvailable}
|
||||
<div class="py-8 text-center">
|
||||
<p class="text-sm text-white/40">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Detaillierte Analytics sind verfuegbar, wenn der uLoad-Server verbunden ist.
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-white/25">
|
||||
<p class="mt-1 text-xs text-muted-foreground/70">
|
||||
Lokaler Click-Count: {link.clickCount}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="py-8 text-center text-sm text-white/40">Noch keine Daten fuer diesen Zeitraum</p>
|
||||
<p class="py-8 text-center text-sm text-muted-foreground">
|
||||
Noch keine Daten fuer diesen Zeitraum
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -256,19 +258,19 @@
|
|||
{#if serverAvailable}
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<!-- Devices -->
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Geraete</h2>
|
||||
{#if devices.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each devices as d}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-white/70">{d.deviceType || 'Unbekannt'}</span>
|
||||
<span class="text-foreground">{d.deviceType || 'Unbekannt'}</span>
|
||||
<span class="font-medium text-white">
|
||||
{Math.round((d.count / totalDevices) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-white/10">
|
||||
<div class="h-2 rounded-full bg-muted/10">
|
||||
<div
|
||||
class="h-2 rounded-full bg-indigo-500"
|
||||
style="width: {(d.count / totalDevices) * 100}%"
|
||||
|
|
@ -278,18 +280,18 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-white/40">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Referrers -->
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Referrer</h2>
|
||||
{#if referrers.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each referrers.slice(0, 8) as r}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="max-w-[140px] truncate text-white/70">
|
||||
<span class="max-w-[140px] truncate text-foreground">
|
||||
{r.referer || 'Direkt'}
|
||||
</span>
|
||||
<span class="font-medium tabular-nums text-white">{r.count}</span>
|
||||
|
|
@ -297,24 +299,24 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-white/40">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Countries -->
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Laender</h2>
|
||||
{#if countries.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each countries.slice(0, 8) as c}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-white/70">{c.country || 'Unbekannt'}</span>
|
||||
<span class="text-foreground">{c.country || 'Unbekannt'}</span>
|
||||
<span class="font-medium text-white">
|
||||
{Math.round((c.count / totalCountries) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 rounded-full bg-white/10">
|
||||
<div class="h-2 rounded-full bg-muted/10">
|
||||
<div
|
||||
class="h-2 rounded-full bg-emerald-500"
|
||||
style="width: {(c.count / totalCountries) * 100}%"
|
||||
|
|
@ -324,7 +326,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-white/40">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
}
|
||||
|
||||
const inputSmClass =
|
||||
'w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700';
|
||||
'w-full rounded-lg border border-border-strong bg-white px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -134,11 +134,7 @@
|
|||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<a
|
||||
href="/uload"
|
||||
class="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Zurueck"
|
||||
>
|
||||
<a href="/uload" class="rounded-lg p-2 hover:bg-muted dark:hover:bg-muted" title="Zurueck">
|
||||
<ArrowLeft size={20} />
|
||||
</a>
|
||||
<div>
|
||||
|
|
@ -159,9 +155,9 @@
|
|||
selectedIds = selectedIds;
|
||||
}
|
||||
}}
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium transition-colors {selectMode
|
||||
class="rounded-lg border border-border-strong px-3 py-2 text-sm font-medium transition-colors {selectMode
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700'}"
|
||||
: 'hover:bg-muted dark:border-border dark:hover:bg-muted'}"
|
||||
>
|
||||
{selectMode ? 'Fertig' : 'Auswaehlen'}
|
||||
</button>
|
||||
|
|
@ -176,7 +172,7 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Links durchsuchen..."
|
||||
class="w-60 rounded-lg border border-gray-300 bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700"
|
||||
class="w-60 rounded-lg border border-border-strong bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||
|
|
@ -226,12 +222,12 @@
|
|||
{#if allLinks.loading}
|
||||
<div class="space-y-3">
|
||||
{#each Array(5) as _}
|
||||
<div class="h-20 animate-pulse rounded-xl bg-gray-100 dark:bg-gray-800"></div>
|
||||
<div class="h-20 animate-pulse rounded-xl bg-muted dark:bg-card"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if filteredLinks.length === 0}
|
||||
<div
|
||||
class="rounded-xl border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-600"
|
||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<LinkIcon size={48} class="mx-auto mb-4 opacity-20" />
|
||||
<p class="text-lg font-medium opacity-60">Keine Links gefunden</p>
|
||||
|
|
@ -245,7 +241,7 @@
|
|||
<div class="space-y-3">
|
||||
{#each filteredLinks as link (link.id)}
|
||||
<div
|
||||
class="group rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
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"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
{#if selectMode}
|
||||
|
|
@ -261,7 +257,7 @@
|
|||
<span
|
||||
class="inline-block h-2 w-2 shrink-0 rounded-full {link.isActive
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-400'}"
|
||||
: 'bg-muted'}"
|
||||
></span>
|
||||
<h3 class="truncate font-semibold">{link.title || link.shortCode}</h3>
|
||||
<span
|
||||
|
|
@ -295,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-gray-100 hover:opacity-100 dark:hover:bg-gray-700"
|
||||
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"
|
||||
title="Analytics"
|
||||
>
|
||||
<ChartBar size={16} />
|
||||
|
|
@ -303,17 +299,20 @@
|
|||
</a>
|
||||
<button
|
||||
onclick={() => copyShortUrl(link.shortCode)}
|
||||
class="rounded-lg p-2 opacity-0 transition-all hover:bg-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all 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-gray-100 group-hover:opacity-100 dark:hover:bg-gray-700"
|
||||
class="rounded-lg p-2 opacity-0 transition-all hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
||||
>
|
||||
<Lightning size={16} class={link.isActive ? 'text-green-500' : 'text-gray-400'} />
|
||||
<Lightning
|
||||
size={16}
|
||||
class={link.isActive ? 'text-green-500' : 'text-muted-foreground'}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => deleteLink(link)}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@
|
|||
<div class="mx-auto max-w-4xl p-4">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-white/5">
|
||||
<ArrowLeft size={20} class="text-white/60" />
|
||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5">
|
||||
<ArrowLeft size={20} class="text-muted-foreground" />
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-white">Tags</h1>
|
||||
</div>
|
||||
|
|
@ -68,26 +68,30 @@
|
|||
</div>
|
||||
|
||||
{#if showCreateForm}
|
||||
<div class="mb-6 rounded-xl border border-white/10 bg-white/5 p-5">
|
||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="tag-name" class="mb-1 block text-sm font-medium text-white/60">Name</label>
|
||||
<label for="tag-name" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||
>Name</label
|
||||
>
|
||||
<input
|
||||
id="tag-name"
|
||||
type="text"
|
||||
bind:value={newName}
|
||||
placeholder="z.B. Social Media"
|
||||
class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-white placeholder-white/30 focus:border-indigo-500 focus:outline-none"
|
||||
class="w-full rounded-lg border border-border/10 bg-muted/5 px-4 py-2 text-white placeholder-white/30 focus:border-indigo-500 focus:outline-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && createTag()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tag-color" class="mb-1 block text-sm font-medium text-white/60">Farbe</label>
|
||||
<label for="tag-color" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||
>Farbe</label
|
||||
>
|
||||
<input
|
||||
id="tag-color"
|
||||
type="color"
|
||||
bind:value={newColor}
|
||||
class="h-10 w-16 cursor-pointer rounded-lg border border-white/10"
|
||||
class="h-10 w-16 cursor-pointer rounded-lg border border-border/10"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -102,22 +106,24 @@
|
|||
{/if}
|
||||
|
||||
{#if !tags.value || tags.value.length === 0}
|
||||
<div class="rounded-xl border-2 border-dashed border-white/10 p-12 text-center">
|
||||
<p class="text-lg font-medium text-white/60">Noch keine Tags</p>
|
||||
<p class="mt-1 text-sm text-white/40">Erstelle Tags um deine Links zu organisieren.</p>
|
||||
<div class="rounded-xl border-2 border-dashed border-border/10 p-12 text-center">
|
||||
<p class="text-lg font-medium text-muted-foreground">Noch keine Tags</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Erstelle Tags um deine Links zu organisieren.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<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-white/10 bg-white/5 p-4 transition-all hover:bg-white/8"
|
||||
class="group rounded-xl border border-border/10 bg-muted/5 p-4 transition-all hover:bg-muted/8"
|
||||
>
|
||||
{#if editingTag?.id === tag.id}
|
||||
<div class="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingTag.name}
|
||||
class="w-full rounded border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white"
|
||||
class="w-full rounded border border-border/10 bg-muted/5 px-3 py-1.5 text-sm text-white"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="color" bind:value={editingTag.color} class="h-8 w-12 rounded" />
|
||||
|
|
@ -128,7 +134,7 @@
|
|||
>
|
||||
<button
|
||||
onclick={() => (editingTag = null)}
|
||||
class="rounded border border-white/10 px-3 py-1 text-sm text-white/60"
|
||||
class="rounded border border-border/10 px-3 py-1 text-sm text-muted-foreground"
|
||||
>{$_('common.cancel')}</button
|
||||
>
|
||||
</div>
|
||||
|
|
@ -143,16 +149,16 @@
|
|||
<span class="font-medium text-white">{tag.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-white/40">{getUsageCount(tag.id)} Links</span>
|
||||
<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-white/40 opacity-0 transition-all hover:bg-white/10 hover:text-white group-hover:opacity-100"
|
||||
class="rounded p-1 text-muted-foreground opacity-0 transition-all hover:bg-muted/10 hover:text-white group-hover:opacity-100"
|
||||
>
|
||||
<PencilSimple size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => deleteTag(tag)}
|
||||
class="rounded p-1 text-white/40 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-all hover:bg-red-900/20 hover:text-red-400 group-hover:opacity-100"
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
|
|
|
|||
184
scripts/migrate-theme-tokens.mjs
Normal file
184
scripts/migrate-theme-tokens.mjs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* One-shot migration: replace raw Tailwind neutral-palette + white-alpha
|
||||
* utilities with theme tokens across the unified Mana web app.
|
||||
*
|
||||
* This is a surgical codemod, not a general-purpose tool. The mappings
|
||||
* encode a specific design decision: `bg-gray-800` = `bg-card`, etc.
|
||||
* Re-running is a no-op once the codebase is clean.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/migrate-theme-tokens.mjs [--dry-run]
|
||||
*
|
||||
* The mappings are ordered by specificity — longer patterns first so
|
||||
* `bg-gray-700/50` is tried before `bg-gray-700`.
|
||||
*/
|
||||
|
||||
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/lib/modules/**/*.svelte',
|
||||
'apps/mana/apps/web/src/routes/(app)/**/*.svelte',
|
||||
];
|
||||
|
||||
/**
|
||||
* Files where `bg-white/N`, `text-white/N`, etc. are brand-literal overlays
|
||||
* on vivid gradient backgrounds, not theme-intent. These stay untouched
|
||||
* and are allowlisted in the validator via scoped <style> migration.
|
||||
*/
|
||||
const EXCLUDE_PATHS = new Set([
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodFullscreen.svelte',
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodCard.svelte',
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/CreateMoodDialog.svelte',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Each entry: [regex, replacement]. Regex MUST use `\b` or class-boundary
|
||||
* anchors so we don't accidentally rewrite inside longer identifiers.
|
||||
* The boundary pattern `(?<=^|[\s:"'\`])` ensures the match starts at a
|
||||
* class boundary (space, colon for variant prefixes, quote char).
|
||||
*/
|
||||
const B = String.raw`(?<=^|[\s:"'\`])`; // class-boundary lookbehind
|
||||
|
||||
const MAPPINGS = [
|
||||
// ─── Backgrounds: surfaces ────────────────────────────────────
|
||||
// Dark modals/cards use gray-800/900 as surface.
|
||||
[new RegExp(`${B}bg-gray-900\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-gray-800\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-neutral-900\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-neutral-800\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-slate-900\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-slate-800\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-zinc-900\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-zinc-800\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-stone-900\\b`, 'g'), 'bg-card'],
|
||||
[new RegExp(`${B}bg-stone-800\\b`, 'g'), 'bg-card'],
|
||||
|
||||
// Mid-grays are muted surfaces. Preserve opacity suffix.
|
||||
[new RegExp(`${B}bg-gray-(?:700|600)\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}bg-gray-(?:700|600)\\b`, 'g'), 'bg-muted'],
|
||||
[new RegExp(`${B}bg-neutral-(?:700|600)\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}bg-neutral-(?:700|600)\\b`, 'g'), 'bg-muted'],
|
||||
[new RegExp(`${B}bg-slate-(?:700|600)\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}bg-slate-(?:700|600)\\b`, 'g'), 'bg-muted'],
|
||||
[new RegExp(`${B}bg-zinc-(?:700|600)\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}bg-zinc-(?:700|600)\\b`, 'g'), 'bg-muted'],
|
||||
[new RegExp(`${B}bg-stone-(?:700|600)\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}bg-stone-(?:700|600)\\b`, 'g'), 'bg-muted'],
|
||||
|
||||
// Light grays 100-500 — same bucket (rarely used in dark-first design).
|
||||
[
|
||||
new RegExp(`${B}bg-(?:gray|slate|zinc|neutral|stone)-(?:500|400|300|200|100|50)\\b`, 'g'),
|
||||
'bg-muted',
|
||||
],
|
||||
|
||||
// ─── Borders ─────────────────────────────────────────────────
|
||||
[
|
||||
new RegExp(`${B}border-(?:gray|slate|zinc|neutral|stone)-(?:700|800|900)\\/(\\d+)\\b`, 'g'),
|
||||
'border-border/$1',
|
||||
],
|
||||
[
|
||||
new RegExp(`${B}border-(?:gray|slate|zinc|neutral|stone)-(?:700|800|900)\\b`, 'g'),
|
||||
'border-border',
|
||||
],
|
||||
[new RegExp(`${B}border-(?:gray|slate|zinc|neutral|stone)-(?:600)\\b`, 'g'), 'border-border'],
|
||||
[
|
||||
new RegExp(`${B}border-(?:gray|slate|zinc|neutral|stone)-(?:500|400|300|200|100)\\b`, 'g'),
|
||||
'border-border-strong',
|
||||
],
|
||||
|
||||
// ─── Text ─────────────────────────────────────────────────────
|
||||
// Dark text on light bg (gray-900/800) = foreground.
|
||||
[new RegExp(`${B}text-(?:gray|slate|zinc|neutral|stone)-(?:900|800)\\b`, 'g'), 'text-foreground'],
|
||||
// text-gray-200/100 on dark bg = foreground
|
||||
[new RegExp(`${B}text-(?:gray|slate|zinc|neutral|stone)-(?:200|100)\\b`, 'g'), 'text-foreground'],
|
||||
// text-gray-300 = foreground/90 (slightly muted primary text)
|
||||
[new RegExp(`${B}text-(?:gray|slate|zinc|neutral|stone)-300\\b`, 'g'), 'text-foreground/90'],
|
||||
// text-gray-400/500/600/700 = muted-foreground (labels, captions)
|
||||
[
|
||||
new RegExp(`${B}text-(?:gray|slate|zinc|neutral|stone)-(?:400|500|700)\\b`, 'g'),
|
||||
'text-muted-foreground',
|
||||
],
|
||||
[
|
||||
new RegExp(`${B}text-(?:gray|slate|zinc|neutral|stone)-600\\b`, 'g'),
|
||||
'text-muted-foreground/70',
|
||||
],
|
||||
|
||||
// ─── Placeholders ────────────────────────────────────────────
|
||||
[
|
||||
new RegExp(`${B}placeholder-(?:gray|slate|zinc|neutral|stone)-(?:400|500|600)\\b`, 'g'),
|
||||
'placeholder:text-muted-foreground/60',
|
||||
],
|
||||
[
|
||||
new RegExp(`${B}placeholder:text-(?:gray|slate|zinc|neutral|stone)-(?:400|500|600)\\b`, 'g'),
|
||||
'placeholder:text-muted-foreground/60',
|
||||
],
|
||||
|
||||
// ─── Hover/Focus variants ────────────────────────────────────
|
||||
// Handled uniformly because the boundary lookbehind already matches
|
||||
// after `hover:`, `focus:`, `active:`, `group-hover:`, etc.
|
||||
|
||||
// ─── White-alpha utilities ───────────────────────────────────
|
||||
// Preserve opacity modifier as given.
|
||||
[new RegExp(`${B}bg-white\\/(\\d+)\\b`, 'g'), 'bg-muted/$1'],
|
||||
[new RegExp(`${B}border-white\\/(\\d+)\\b`, 'g'), 'border-border/$1'],
|
||||
// Text: /70+ = foreground, /30-60 = muted-foreground
|
||||
[new RegExp(`${B}text-white\\/(?:90|80|70)\\b`, 'g'), 'text-foreground'],
|
||||
[new RegExp(`${B}text-white\\/(?:60|50|40)\\b`, 'g'), 'text-muted-foreground'],
|
||||
[new RegExp(`${B}text-white\\/(?:30|20|10)\\b`, 'g'), 'text-muted-foreground/70'],
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function migrate() {
|
||||
const paths = listFiles();
|
||||
let changedFiles = 0;
|
||||
let totalSubs = 0;
|
||||
|
||||
for (const rel of paths) {
|
||||
if (EXCLUDE_PATHS.has(rel)) continue;
|
||||
const abs = join(REPO_ROOT, rel);
|
||||
let src = readFileSync(abs, 'utf8');
|
||||
let fileSubs = 0;
|
||||
|
||||
for (const [pattern, replacement] of MAPPINGS) {
|
||||
const before = src;
|
||||
src = src.replace(pattern, replacement);
|
||||
if (src !== before) {
|
||||
// Count replacements this rule made.
|
||||
const matches = before.match(pattern);
|
||||
fileSubs += matches ? matches.length : 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileSubs > 0) {
|
||||
changedFiles++;
|
||||
totalSubs += fileSubs;
|
||||
if (!DRY_RUN) writeFileSync(abs, src, 'utf8');
|
||||
console.log(` ${fileSubs.toString().padStart(4)} subs ${rel}`);
|
||||
}
|
||||
}
|
||||
|
||||
const verb = DRY_RUN ? 'Would migrate' : 'Migrated';
|
||||
console.log(`\n${verb} ${totalSubs} token(s) across ${changedFiles} file(s).`);
|
||||
if (DRY_RUN) console.log('Run without --dry-run to apply.');
|
||||
}
|
||||
|
||||
migrate();
|
||||
|
|
@ -1,30 +1,42 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate that module ListView.svelte files use theme tokens instead of
|
||||
* raw white-alpha Tailwind utilities.
|
||||
* Validate that the unified Mana web app uses theme tokens instead of raw
|
||||
* Tailwind utilities that bypass the theme system.
|
||||
*
|
||||
* 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`
|
||||
* `bg-gray-800`, `border-neutral-700` ignore the active theme and can
|
||||
* render white-on-white (or dark-on-dark) under the wrong variant. The
|
||||
* theme tokens (`text-foreground`, `bg-muted`, `border-border`, etc.) are
|
||||
* 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)
|
||||
* Rules (enforced across `apps/mana/apps/web/src/lib/modules/**` and
|
||||
* `apps/mana/apps/web/src/routes/(app)/**`):
|
||||
*
|
||||
* 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
|
||||
* 1. No white-alpha utilities:
|
||||
* `bg-white/N`, `text-white/N`, `border-white/N` (and hover:/focus:/etc. variants)
|
||||
*
|
||||
* `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.
|
||||
* 2. No neutral-palette utilities:
|
||||
* `(bg|text|border)-(gray|slate|zinc|neutral|stone)-N` (with or without /N)
|
||||
* — these should be theme tokens instead.
|
||||
*
|
||||
* Replacement cheat-sheet:
|
||||
* 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
|
||||
* bg-gray-800 → bg-card or bg-muted
|
||||
* bg-neutral-900 → bg-card
|
||||
* text-gray-400 → text-muted-foreground
|
||||
* text-slate-300 → text-foreground/90
|
||||
* border-gray-700 → border-border
|
||||
*
|
||||
* Brand-literal escape hatch: if a color must stay literal (overlay on a
|
||||
* vivid gradient, photo viewer backdrop, domain-semantic palette), move
|
||||
* it into a scoped <style> block — the validator only scans class
|
||||
* attributes. See `lib/modules/moodlit/ListView.svelte:.mood-card:hover`
|
||||
* for the pattern.
|
||||
*
|
||||
* Zero deps — runs as plain Node ESM. Uses `git ls-files` to respect
|
||||
* .gitignore.
|
||||
|
|
@ -38,14 +50,41 @@ 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';
|
||||
const SCAN_GLOBS = [
|
||||
'apps/mana/apps/web/src/lib/modules/**/*.svelte',
|
||||
'apps/mana/apps/web/src/routes/(app)/**/*.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;
|
||||
/**
|
||||
* Files where white-alpha utilities are intentional brand literals —
|
||||
* they render overlays on vivid mood-gradient backgrounds, not theme
|
||||
* surfaces. Exempt from the `white/` rule but still must follow the
|
||||
* neutral-palette rule.
|
||||
*/
|
||||
const BRAND_OVERLAY_FILES = new Set([
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodFullscreen.svelte',
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/MoodCard.svelte',
|
||||
'apps/mana/apps/web/src/lib/modules/moodlit/components/mood/CreateMoodDialog.svelte',
|
||||
]);
|
||||
|
||||
function listListViews() {
|
||||
const out = execSync(`git ls-files "${LIST_VIEW_GLOB}"`, {
|
||||
// `(?:^|[\s:"'`])` anchors so `hover:bg-white/5` and class=`bg-white/10`
|
||||
// both match, without tripping on unrelated substrings like `off-white`.
|
||||
const RULES = [
|
||||
{
|
||||
name: 'white-alpha',
|
||||
pattern: /(?:^|[\s:"'`])(bg|text|border)-white\/\d/g,
|
||||
describe: (m) => `${m[1]}-white/`,
|
||||
},
|
||||
{
|
||||
name: 'neutral-palette',
|
||||
pattern: /(?:^|[\s:"'`])(bg|text|border)-(gray|slate|zinc|neutral|stone)-\d/g,
|
||||
describe: (m) => `${m[1]}-${m[2]}-`,
|
||||
},
|
||||
];
|
||||
|
||||
function listFiles() {
|
||||
const args = SCAN_GLOBS.map((g) => `"${g}"`).join(' ');
|
||||
const out = execSync(`git ls-files ${args}`, {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
|
@ -56,7 +95,7 @@ function listListViews() {
|
|||
}
|
||||
|
||||
function validate() {
|
||||
const paths = listListViews();
|
||||
const paths = listFiles();
|
||||
const violations = [];
|
||||
|
||||
for (const rel of paths) {
|
||||
|
|
@ -64,40 +103,60 @@ function validate() {
|
|||
const src = readFileSync(abs, 'utf8');
|
||||
const lines = src.split('\n');
|
||||
|
||||
const brandOverlay = BRAND_OVERLAY_FILES.has(rel);
|
||||
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),
|
||||
});
|
||||
for (const rule of RULES) {
|
||||
if (brandOverlay && rule.name === 'white-alpha') continue;
|
||||
rule.pattern.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = rule.pattern.exec(line)) !== null) {
|
||||
violations.push({
|
||||
file: rel,
|
||||
line: i + 1,
|
||||
rule: rule.name,
|
||||
token: rule.describe(match),
|
||||
snippet: line.trim().slice(0, 120),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n✗ Theme-token check FAILED (${violations.length} violation(s)):\n`);
|
||||
// Group by file for readability.
|
||||
const byFile = new Map();
|
||||
for (const v of violations) {
|
||||
console.error(` • ${v.file}:${v.line} [${v.token}]`);
|
||||
console.error(` ${v.snippet}`);
|
||||
if (!byFile.has(v.file)) byFile.set(v.file, []);
|
||||
byFile.get(v.file).push(v);
|
||||
}
|
||||
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`
|
||||
`\n✗ Theme-token check FAILED (${violations.length} violation(s) in ${byFile.size} file(s)):\n`
|
||||
);
|
||||
for (const [file, vs] of byFile) {
|
||||
console.error(` ${file} (${vs.length})`);
|
||||
for (const v of vs.slice(0, 5)) {
|
||||
console.error(` :${v.line} [${v.rule}: ${v.token}] ${v.snippet.slice(0, 80)}`);
|
||||
}
|
||||
if (vs.length > 5) console.error(` … +${vs.length - 5} more`);
|
||||
}
|
||||
console.error(
|
||||
`\nReplace 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` +
|
||||
` bg-gray/slate/neutral-N → bg-muted / bg-card\n` +
|
||||
` text-gray/slate-N → text-foreground / text-muted-foreground\n` +
|
||||
` border-gray/slate-N → border-border\n\n` +
|
||||
`Rationale: raw color utilities ignore theme variants (Lume, Nature, ...)\n` +
|
||||
`and can render invisible under other themes. See\n` +
|
||||
`packages/shared-tailwind/src/themes.css for the full token set.\n` +
|
||||
`Brand-literal overlays may move into scoped <style> blocks.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ Theme-token check: ${paths.length} ListView(s) use theme tokens correctly.`);
|
||||
console.log(`✓ Theme-token check: ${paths.length} files use theme tokens correctly.`);
|
||||
}
|
||||
|
||||
validate();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue