mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 11:21:09 +02:00
feat(dreams): polish symbol library — sort, auto-save, merge, navigation
SymbolsView: - Sort tabs (Häufigkeit / A-Z / Zuletzt) above the cloud - More dramatic font scaling using sqrt easing for visual hierarchy - "?" badge on symbols without a personal meaning, dimmed until hover - "Zuletzt" sort shows the most recent dreamDate per symbol - A-Z and Zuletzt switch the cloud to a vertical list layout - Hides symbols whose count dropped to zero (e.g. after merge) SymbolDetailView: - Auto-save with 500ms debounce + transient "Gespeichert" hint - Co-occurring chips are clickable and navigate to that symbol's detail - Dream refs are clickable buttons; ListView passes onOpenDream so a click jumps back to the timeline and opens the dream for editing - Manual merge UI: "Zusammenführen…" button reveals a select with all other symbols, confirmation dialog before merging - Re-initializes edit buffer when navigating between symbols (lastInitId guard instead of one-shot initialized flag) Helpers: - getLastUsedBySymbol returns a Map of symbol → most recent dreamDate Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
216746721e
commit
771721ca30
4 changed files with 421 additions and 66 deletions
|
|
@ -184,7 +184,12 @@
|
|||
</div>
|
||||
|
||||
{#if viewMode === 'symbols'}
|
||||
<SymbolsView />
|
||||
<SymbolsView
|
||||
onOpenDream={(d) => {
|
||||
viewMode = 'list';
|
||||
startEdit(d);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Quick create -->
|
||||
<form onsubmit={(e) => e.preventDefault()} class="quick-add">
|
||||
|
|
|
|||
|
|
@ -120,6 +120,18 @@ export function formatDreamDate(iso: string): string {
|
|||
return date.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
/** Map of symbol name → most recent dreamDate that references it. */
|
||||
export function getLastUsedBySymbol(dreams: Dream[]): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
for (const d of dreams) {
|
||||
for (const sym of d.symbols ?? []) {
|
||||
const prev = map.get(sym);
|
||||
if (!prev || d.dreamDate > prev) map.set(sym, d.dreamDate);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** All dreams that contain the given symbol, newest first. */
|
||||
export function getDreamsWithSymbol(dreams: Dream[], symbolName: string): Dream[] {
|
||||
return dreams
|
||||
|
|
|
|||
|
|
@ -12,9 +12,19 @@
|
|||
useAllDreamSymbols,
|
||||
} from '../queries';
|
||||
import { dreamsStore } from '../stores/dreams.svelte';
|
||||
import { MOOD_COLORS, MOOD_LABELS, type DreamMood } from '../types';
|
||||
import { MOOD_COLORS, MOOD_LABELS, type Dream, type DreamMood } from '../types';
|
||||
|
||||
let { symbolId, onBack }: { symbolId: string; onBack: () => void } = $props();
|
||||
let {
|
||||
symbolId,
|
||||
onBack,
|
||||
onSelectSymbol,
|
||||
onOpenDream,
|
||||
}: {
|
||||
symbolId: string;
|
||||
onBack: () => void;
|
||||
onSelectSymbol?: (id: string) => void;
|
||||
onOpenDream?: (dream: Dream) => void;
|
||||
} = $props();
|
||||
|
||||
let symbols$ = useAllDreamSymbols();
|
||||
let dreams$ = useAllDreams();
|
||||
|
|
@ -27,14 +37,14 @@
|
|||
let editName = $state('');
|
||||
let editMeaning = $state('');
|
||||
let editColor = $state<string>('#6366f1');
|
||||
let initialized = $state(false);
|
||||
let lastInitId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (symbol && !initialized) {
|
||||
if (symbol && lastInitId !== symbol.id) {
|
||||
editName = symbol.name;
|
||||
editMeaning = symbol.meaning ?? '';
|
||||
editColor = symbol.color ?? '#6366f1';
|
||||
initialized = true;
|
||||
lastInitId = symbol.id;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -45,11 +55,42 @@
|
|||
editColor !== (symbol.color ?? '#6366f1'))
|
||||
);
|
||||
|
||||
let savedHint = $state(false);
|
||||
|
||||
// Debounced auto-save
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
$effect(() => {
|
||||
// react to edit fields
|
||||
void editName;
|
||||
void editMeaning;
|
||||
void editColor;
|
||||
|
||||
if (!dirty || !symbol) return;
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
void save();
|
||||
}, 500);
|
||||
return () => {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
};
|
||||
});
|
||||
|
||||
let dreamsWithSymbol = $derived(symbol ? getDreamsWithSymbol(dreams, symbol.name) : []);
|
||||
let moodDist = $derived(symbol ? getMoodDistribution(dreams, symbol.name) : []);
|
||||
let cooccurring = $derived(symbol ? getCooccurringSymbols(dreams, symbol.name) : []);
|
||||
let totalForBars = $derived(moodDist.reduce((sum, m) => sum + m.count, 0) || 1);
|
||||
|
||||
// Merge target candidates
|
||||
let mergeOpen = $state(false);
|
||||
let mergeTargetId = $state('');
|
||||
let mergeCandidates = $derived(
|
||||
symbol
|
||||
? symbols
|
||||
.filter((s) => s.id !== symbol.id && s.count > 0)
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'de'))
|
||||
: []
|
||||
);
|
||||
|
||||
const PALETTE = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#06b6d4', '#a855f7', '#ec4899'];
|
||||
|
||||
async function save() {
|
||||
|
|
@ -59,6 +100,8 @@
|
|||
meaning: editMeaning.trim() || null,
|
||||
color: editColor,
|
||||
});
|
||||
savedHint = true;
|
||||
setTimeout(() => (savedHint = false), 1500);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
|
|
@ -71,6 +114,25 @@
|
|||
onBack();
|
||||
}
|
||||
|
||||
async function handleMerge() {
|
||||
if (!symbol || !mergeTargetId) return;
|
||||
const target = symbols.find((s) => s.id === mergeTargetId);
|
||||
if (!target) return;
|
||||
const ok = confirm(
|
||||
`"${symbol.name}" in "${target.name}" zusammenführen? Alle Träume werden umgeschrieben.`
|
||||
);
|
||||
if (!ok) return;
|
||||
await dreamsStore.mergeSymbols(symbol.id, mergeTargetId);
|
||||
mergeOpen = false;
|
||||
mergeTargetId = '';
|
||||
onBack();
|
||||
}
|
||||
|
||||
function navigateToCooccurring(name: string) {
|
||||
const target = symbols.find((s) => s.name === name);
|
||||
if (target && onSelectSymbol) onSelectSymbol(target.id);
|
||||
}
|
||||
|
||||
function moodColor(mood: string): string {
|
||||
if (mood in MOOD_COLORS) return MOOD_COLORS[mood as DreamMood];
|
||||
return '#9ca3af';
|
||||
|
|
@ -85,11 +147,35 @@
|
|||
<div class="detail-view">
|
||||
<div class="header">
|
||||
<button class="back-btn" onclick={onBack}>← Symbole</button>
|
||||
{#if symbol}
|
||||
<button class="del-btn" onclick={handleDelete}>Löschen</button>
|
||||
{/if}
|
||||
<div class="header-actions">
|
||||
{#if savedHint}
|
||||
<span class="saved-hint">Gespeichert</span>
|
||||
{/if}
|
||||
{#if symbol && mergeCandidates.length > 0}
|
||||
<button class="meta-btn" onclick={() => (mergeOpen = !mergeOpen)}>Zusammenführen…</button>
|
||||
{/if}
|
||||
{#if symbol}
|
||||
<button class="del-btn" onclick={handleDelete}>Löschen</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if mergeOpen && symbol}
|
||||
<div class="merge-panel">
|
||||
<span class="merge-label">"{symbol.name}" zusammenführen mit:</span>
|
||||
<select class="merge-select" bind:value={mergeTargetId}>
|
||||
<option value="">– Symbol wählen –</option>
|
||||
{#each mergeCandidates as c}
|
||||
<option value={c.id}>{c.name} ({c.count})</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="merge-confirm" disabled={!mergeTargetId} onclick={handleMerge}>OK</button>
|
||||
<button class="merge-cancel" onclick={() => ((mergeOpen = false), (mergeTargetId = ''))}
|
||||
>Abbrechen</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !symbol}
|
||||
<p class="empty">Symbol nicht gefunden.</p>
|
||||
{:else}
|
||||
|
|
@ -129,12 +215,6 @@
|
|||
></textarea>
|
||||
</div>
|
||||
|
||||
{#if dirty}
|
||||
<div class="save-row">
|
||||
<button class="save-btn" onclick={save}>Speichern</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Mood distribution -->
|
||||
{#if moodDist.length > 0}
|
||||
<div class="section">
|
||||
|
|
@ -162,7 +242,13 @@
|
|||
<span class="section-label">Häufig zusammen mit</span>
|
||||
<div class="cooc-row">
|
||||
{#each cooccurring as c}
|
||||
<span class="cooc-chip">{c.name} <span class="cooc-count">{c.count}</span></span>
|
||||
<button
|
||||
class="cooc-chip"
|
||||
onclick={() => navigateToCooccurring(c.name)}
|
||||
title={`Zu "${c.name}" wechseln`}
|
||||
>
|
||||
{c.name} <span class="cooc-count">{c.count}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -174,7 +260,7 @@
|
|||
<span class="section-label">Träume mit diesem Symbol</span>
|
||||
<div class="dream-refs">
|
||||
{#each dreamsWithSymbol as d (d.id)}
|
||||
<div class="dream-ref">
|
||||
<button class="dream-ref" onclick={() => onOpenDream?.(d)} disabled={!onOpenDream}>
|
||||
{#if d.mood}
|
||||
<span class="ref-dot" style="background: {MOOD_COLORS[d.mood]}"></span>
|
||||
{:else}
|
||||
|
|
@ -184,7 +270,7 @@
|
|||
<span class="ref-title">{d.title || 'Traum ohne Titel'}</span>
|
||||
<span class="ref-date">{formatDreamDate(d.dreamDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -206,6 +292,13 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
|
|
@ -220,6 +313,41 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.saved-hint {
|
||||
font-size: 0.625rem;
|
||||
color: #22c55e;
|
||||
font-weight: 500;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: #6b7280;
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.meta-btn:hover {
|
||||
border-color: #6366f1;
|
||||
color: #6366f1;
|
||||
}
|
||||
:global(.dark) .meta-btn {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.del-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
|
|
@ -233,6 +361,62 @@
|
|||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
/* Merge panel */
|
||||
.merge-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
.merge-label {
|
||||
font-size: 0.6875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.merge-select {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #374151;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
:global(.dark) .merge-select {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
color: #f3f4f6;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.merge-confirm {
|
||||
padding: 0.1875rem 0.625rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
font-size: 0.6875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.merge-confirm:disabled {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.merge-cancel {
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
font-size: 0.6875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.merge-cancel:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.sym-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -318,24 +502,6 @@
|
|||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.save-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.save-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: #5558e6;
|
||||
}
|
||||
|
||||
/* Bars */
|
||||
.bars {
|
||||
display: flex;
|
||||
|
|
@ -384,6 +550,14 @@
|
|||
background: rgba(99, 102, 241, 0.08);
|
||||
color: #6366f1;
|
||||
font-size: 0.6875rem;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.cooc-chip:hover {
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
.cooc-count {
|
||||
font-size: 0.5625rem;
|
||||
|
|
@ -401,13 +575,22 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.25rem;
|
||||
padding: 0.375rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.dream-ref:hover {
|
||||
.dream-ref:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.dream-ref:not(:disabled):hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
:global(.dark) .dream-ref:hover {
|
||||
:global(.dark) .dream-ref:not(:disabled):hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,61 +4,141 @@
|
|||
Click → opens detail panel.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { useAllDreamSymbols } from '../queries';
|
||||
import type { DreamSymbol } from '../types';
|
||||
import {
|
||||
formatDreamDate,
|
||||
getLastUsedBySymbol,
|
||||
useAllDreams,
|
||||
useAllDreamSymbols,
|
||||
} from '../queries';
|
||||
import type { Dream, DreamSymbol } from '../types';
|
||||
import SymbolDetailView from './SymbolDetailView.svelte';
|
||||
|
||||
let symbols$ = useAllDreamSymbols();
|
||||
let symbols = $derived(symbols$.value);
|
||||
let { onOpenDream }: { onOpenDream?: (dream: Dream) => void } = $props();
|
||||
|
||||
let symbols$ = useAllDreamSymbols();
|
||||
let dreams$ = useAllDreams();
|
||||
let symbols = $derived(symbols$.value);
|
||||
let dreams = $derived(dreams$.value);
|
||||
|
||||
type SortMode = 'count' | 'alpha' | 'recent';
|
||||
let sortMode = $state<SortMode>('count');
|
||||
let searchQuery = $state('');
|
||||
let selectedSymbolId = $state<string | null>(null);
|
||||
|
||||
let lastUsedMap = $derived(getLastUsedBySymbol(dreams));
|
||||
|
||||
let active = $derived(symbols.filter((s) => s.count > 0));
|
||||
|
||||
let filtered = $derived(
|
||||
searchQuery.trim()
|
||||
? symbols.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: symbols
|
||||
? active.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: active
|
||||
);
|
||||
|
||||
let maxCount = $derived(filtered.reduce((m, s) => Math.max(m, s.count), 1));
|
||||
let sorted = $derived.by(() => {
|
||||
const list = [...filtered];
|
||||
switch (sortMode) {
|
||||
case 'alpha':
|
||||
return list.sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
case 'recent':
|
||||
return list.sort((a, b) => {
|
||||
const ra = lastUsedMap.get(a.name) ?? '';
|
||||
const rb = lastUsedMap.get(b.name) ?? '';
|
||||
return rb.localeCompare(ra);
|
||||
});
|
||||
default:
|
||||
return list.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, 'de'));
|
||||
}
|
||||
});
|
||||
|
||||
let maxCount = $derived(sorted.reduce((m, s) => Math.max(m, s.count), 1));
|
||||
|
||||
function fontSize(sym: DreamSymbol): string {
|
||||
// Scale 0.75rem .. 1.5rem based on count
|
||||
const ratio = Math.max(0.2, sym.count / maxCount);
|
||||
return `${0.75 + ratio * 0.75}rem`;
|
||||
// More dramatic scaling: 0.75rem .. 1.625rem
|
||||
const ratio = Math.max(0.1, sym.count / maxCount);
|
||||
// ease-out so big symbols stand out more
|
||||
const eased = Math.sqrt(ratio);
|
||||
return `${0.75 + eased * 0.875}rem`;
|
||||
}
|
||||
|
||||
function selectSymbol(id: string) {
|
||||
selectedSymbolId = id;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if selectedSymbolId}
|
||||
<SymbolDetailView symbolId={selectedSymbolId} onBack={() => (selectedSymbolId = null)} />
|
||||
<SymbolDetailView
|
||||
symbolId={selectedSymbolId}
|
||||
onBack={() => (selectedSymbolId = null)}
|
||||
onSelectSymbol={selectSymbol}
|
||||
{onOpenDream}
|
||||
/>
|
||||
{:else}
|
||||
<div class="symbols-view">
|
||||
{#if symbols.length > 5}
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Symbol suchen..."
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Toolbar: search + sort -->
|
||||
<div class="toolbar">
|
||||
{#if active.length > 5}
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Symbol suchen..."
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
{/if}
|
||||
<div class="sort-tabs">
|
||||
<button
|
||||
class="sort-tab"
|
||||
class:active={sortMode === 'count'}
|
||||
onclick={() => (sortMode = 'count')}
|
||||
>
|
||||
Häufigkeit
|
||||
</button>
|
||||
<button
|
||||
class="sort-tab"
|
||||
class:active={sortMode === 'alpha'}
|
||||
onclick={() => (sortMode = 'alpha')}
|
||||
>
|
||||
A-Z
|
||||
</button>
|
||||
<button
|
||||
class="sort-tab"
|
||||
class:active={sortMode === 'recent'}
|
||||
onclick={() => (sortMode = 'recent')}
|
||||
>
|
||||
Zuletzt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if filtered.length === 0}
|
||||
{#if sorted.length === 0}
|
||||
<p class="empty">
|
||||
{symbols.length === 0
|
||||
{active.length === 0
|
||||
? 'Noch keine Symbole. Füge Symbole zu deinen Träumen hinzu, um sie hier zu sehen.'
|
||||
: 'Keine Treffer'}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="cloud">
|
||||
{#each filtered as sym (sym.id)}
|
||||
<div class="cloud" class:list-mode={sortMode !== 'count'}>
|
||||
{#each sorted as sym (sym.id)}
|
||||
{@const lastDate = lastUsedMap.get(sym.name)}
|
||||
{@const noMeaning = !sym.meaning?.trim()}
|
||||
<button
|
||||
class="sym-chip"
|
||||
style="font-size: {fontSize(sym)}; --sym-color: {sym.color ?? '#6366f1'}"
|
||||
class:no-meaning={noMeaning}
|
||||
style="font-size: {sortMode === 'count'
|
||||
? fontSize(sym)
|
||||
: '0.8125rem'}; --sym-color: {sym.color ?? '#6366f1'}"
|
||||
onclick={() => (selectedSymbolId = sym.id)}
|
||||
title={sym.meaning ?? 'Noch keine Bedeutung hinterlegt'}
|
||||
>
|
||||
<span class="sym-dot"></span>
|
||||
<span class="sym-name">{sym.name}</span>
|
||||
<span class="sym-count">{sym.count}</span>
|
||||
{#if sortMode === 'recent' && lastDate}
|
||||
<span class="sym-meta">· {formatDreamDate(lastDate)}</span>
|
||||
{/if}
|
||||
{#if noMeaning}
|
||||
<span class="sym-badge" title="Keine Bedeutung hinterlegt">?</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -70,11 +150,17 @@
|
|||
.symbols-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 0.625rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
|
|
@ -92,14 +178,46 @@
|
|||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.sort-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sort-tab {
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
font-size: 0.625rem;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sort-tab:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
.sort-tab.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
:global(.dark) .sort-tab {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem 0.75rem;
|
||||
gap: 0.5rem 0.625rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.cloud.list-mode {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.1875rem;
|
||||
}
|
||||
|
||||
.sym-chip {
|
||||
display: inline-flex;
|
||||
|
|
@ -114,11 +232,22 @@
|
|||
transition: all 0.15s;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
.cloud.list-mode .sym-chip {
|
||||
border-radius: 0.375rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sym-chip:hover {
|
||||
background: color-mix(in srgb, var(--sym-color) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--sym-color) 12%, transparent);
|
||||
border-color: var(--sym-color);
|
||||
}
|
||||
.sym-chip.no-meaning {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.sym-chip.no-meaning:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
:global(.dark) .sym-chip {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
|
@ -141,6 +270,32 @@
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sym-meta {
|
||||
font-size: 0.625rem;
|
||||
color: #c0bfba;
|
||||
font-weight: 400;
|
||||
}
|
||||
:global(.dark) .sym-meta {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.sym-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 9999px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #9ca3af;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 700;
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
:global(.dark) .sym-badge {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue