ui optimierungen

This commit is contained in:
beo3000 2026-02-24 18:10:53 +01:00
parent b24915783d
commit 53b20cf492
13 changed files with 104 additions and 98 deletions

View File

@ -1 +1 @@
1.1.49
1.1.52

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import { goto } from "$app/navigation";
interface Props {
@ -7,7 +7,7 @@
}
let { onsidebaropen }: Props = $props();
const currentPath = $derived($page.url.pathname);
const currentPath = $derived(page.url.pathname);
interface Tab {
id: string;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { collapsedTopicIds } from '$lib/stores/agenda';
@ -34,7 +34,7 @@
journalScope = s;
localStorage.setItem(SCOPE_KEY, s);
}
const initialDate = $derived($page.url.searchParams.get('date') ?? undefined);
const initialDate = $derived(page.url.searchParams.get('date') ?? undefined);
let compact = $state(false);
// Rating modal state

View File

@ -12,7 +12,7 @@
import LinkTitle from './LinkTitle.svelte';
import { get } from 'svelte/store';
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
import { normalizeTitleAndBody } from '$lib/utils/titleUtils';
import { normalizeTitleAndBody, extractTitleAndBody } from '$lib/utils/titleUtils';
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
interface Props {
@ -27,7 +27,6 @@
const isDailyLog = $derived(contextId === 'daily-log');
// --- Daily-log journal mode ---
let entryTitle = $state('');
let entryText = $state('');
let entryEditor: MarkdownEditor;
let selectedDate = $state(initialDate ?? today());
@ -35,7 +34,7 @@
let wiedervorlageChecked = $state(true);
let meetingPickerOpen = $state(false);
useUnsavedGuard(() => entryTitle.trim() !== '' || entryText.trim() !== '' || editingId !== null);
useUnsavedGuard(() => entryText.trim() !== '' || editingId !== null);
// All meeting contexts for the link dropdown
const meetingContexts = liveQuery(() =>
@ -83,9 +82,12 @@
async function handleAddEntry() {
let title = entryTitle.trim();
let body = entryText.trim();
if (!title) return;
const raw = entryText.trim();
if (!raw) return;
const { maxTitleLength } = get(scopeSettings);
const extracted = extractTitleAndBody(raw, maxTitleLength);
let title = extracted.title;
let body = extracted.body;
// If title is a URL, try to fetch its page title
if (/^https?:\/\//i.test(title)) {
@ -103,14 +105,11 @@
await createHistoryEntry(topic.id, selectedDate, body, null, false, isPrivate);
}
} else {
const { maxTitleLength } = get(scopeSettings);
const { title: shortTitle, body: fullBody } = normalizeTitleAndBody(title, body, maxTitleLength);
const text = fullBody ? `${shortTitle}\n\n${fullBody}` : shortTitle;
const text = body ? `${title}\n\n${body}` : title;
await getOrCreateJournalTopic();
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked, isPrivate);
}
entryTitle = '';
entryText = '';
entryEditor?.clear();
selectedLinkedContextId = '';
@ -118,10 +117,6 @@
wiedervorlageChecked = true;
}
function handleTitleKeypress(e: KeyboardEvent) {
if (e.key === 'Enter') handleAddEntry();
}
let confirmDeleteId = $state<string | null>(null);
// --- Edit mode ---
@ -160,9 +155,13 @@
confirmDeleteId = null;
}
function formatTime(isoString: string): string {
function formatTime(entry: { sortOrder: number; updatedAt: string }): string {
try {
return new Date(isoString).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
// sortOrder >= 1e12 means it was set as Date.now() (ms timestamp) → use as creation time
const ts = entry.sortOrder >= 1_000_000_000_000
? new Date(entry.sortOrder)
: new Date(entry.updatedAt);
return ts.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} catch {
return '';
}
@ -238,52 +237,15 @@
{/if}
<div class="mb-8 flex flex-col gap-2.5 rounded-lg border border-border bg-sidebar p-4">
<div class="relative flex items-center gap-2">
<input
type="text"
class="flex-1 rounded border border-[#444] bg-bg px-2.5 py-2 font-mono text-white"
placeholder="Titel / Was ist passiert?"
bind:value={entryTitle}
onkeypress={handleTitleKeypress}
use:mention
/>
<!-- Meeting picker icon button -->
<button
type="button"
class="flex items-center gap-1 rounded border px-2 py-2 text-sm transition-colors {journalScope === 'private' ? 'hidden' : selectedLinkedContextId ? 'border-accent bg-accent/20 text-accent' : 'border-[#444] bg-bg text-muted hover:text-white'}"
onclick={() => meetingPickerOpen = !meetingPickerOpen}
title="Jour-fix verknüpfen"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</button>
{#if meetingPickerOpen}
<div class="absolute right-0 top-full z-50 mt-1 min-w-48 rounded border border-[#444] bg-[#1e1e1e] py-1 shadow-lg">
<button
class="w-full px-3 py-1.5 text-left text-sm text-muted hover:bg-[#2a2a2a] {!selectedLinkedContextId ? 'text-white' : ''}"
onclick={() => { selectedLinkedContextId = ''; meetingPickerOpen = false; }}
>— kein Jour-fix —</button>
{#each $meetingContexts ?? [] as ctx}
<button
class="w-full px-3 py-1.5 text-left text-sm hover:bg-[#2a2a2a] {selectedLinkedContextId === ctx.id ? 'text-accent' : 'text-white'}"
onclick={() => { selectedLinkedContextId = ctx.id; meetingPickerOpen = false; }}
>{ctx.name}</button>
{/each}
</div>
{/if}
</div>
<MarkdownEditor
bind:this={entryEditor}
placeholder="Details (optional)"
placeholder="Was ist passiert? (1. Zeile = Titel)"
minHeight="60px"
wikiScope={journalScope === 'private'}
onchange={(md) => entryText = md}
onsave={handleAddEntry}
/>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<button
class="rounded px-4 py-2 font-bold text-white hover:brightness-110"
style="background-color: {journalScope === 'private' ? $scopeSettings.privateColor : $scopeSettings.businessColor}"
@ -294,13 +256,41 @@
{#if !selectedLinkedContextId}
<button
type="button"
class="flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors {wiedervorlageChecked ? 'border-amber-500 bg-amber-500/20 text-amber-400' : 'border-[#444] text-muted hover:border-[#666] hover:text-white'}"
class="rounded border p-1.5 text-base leading-none transition-colors {wiedervorlageChecked ? 'border-amber-500 bg-amber-500/20 text-amber-400' : 'border-[#444] text-muted hover:border-[#666] hover:text-white'}"
onclick={() => wiedervorlageChecked = !wiedervorlageChecked}
>
<span class="text-base leading-none">{wiedervorlageChecked ? '⏰' : '○'}</span>
Wiedervorlage
</button>
title="Wiedervorlage"
>{wiedervorlageChecked ? '⏰' : '○'}</button>
{/if}
<!-- Meeting picker button -->
<div class="relative {journalScope === 'private' ? 'hidden' : ''}">
<button
type="button"
class="flex items-center gap-1 rounded border px-2 py-1.5 text-sm transition-colors {selectedLinkedContextId ? 'border-accent bg-accent/20 text-accent' : 'border-[#444] bg-bg text-muted hover:text-white'}"
onclick={() => meetingPickerOpen = !meetingPickerOpen}
title="Jour-fix verknüpfen"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</button>
{#if meetingPickerOpen}
<div class="absolute left-0 top-full z-50 mt-1 min-w-48 rounded border border-[#444] bg-[#1e1e1e] py-1 shadow-lg">
<button
class="w-full px-3 py-1.5 text-left text-sm text-muted hover:bg-[#2a2a2a] {!selectedLinkedContextId ? 'text-white' : ''}"
onclick={() => { selectedLinkedContextId = ''; meetingPickerOpen = false; }}
>— kein Jour-fix —</button>
{#each $meetingContexts ?? [] as ctx}
<button
class="w-full px-3 py-1.5 text-left text-sm hover:bg-[#2a2a2a] {selectedLinkedContextId === ctx.id ? 'text-accent' : 'text-white'}"
onclick={() => { selectedLinkedContextId = ctx.id; meetingPickerOpen = false; }}
>{ctx.name}</button>
{/each}
</div>
{/if}
</div>
</div>
</div>
@ -358,7 +348,7 @@
{@const title = lines[0].replace(/^#{1,6}\s+/, '')}
{@const body = lines.slice(1).join('\n').trim()}
<div class="group mb-3 flex items-start gap-2 rounded bg-card-bg p-2.5">
<span class="mt-0.5 text-xs text-muted whitespace-nowrap">{formatTime(entry.updatedAt)}</span>
<span class="mt-0.5 text-xs text-muted whitespace-nowrap">{formatTime(entry)}</span>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-1.5 font-bold">
<LinkTitle text={title} />

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
@ -26,7 +26,7 @@
}
let { onnavigate, hideLogo = false }: Props = $props();
const currentId = $derived($page.params.id ?? '');
const currentId = $derived(page.params.id ?? '');
const contexts = liveQuery(() => db.contexts.filter(c => !c.deletedAt && !c.archivedAt).sortBy('sortOrder'));
@ -108,12 +108,12 @@
{/if}
<!-- General -->
<div class="mb-1 mt-4 pl-1 text-xs uppercase text-muted">General</div>
<div class="mb-1 mt-6 pl-1 text-xs uppercase text-muted">General</div>
<ul class="m-0 list-none p-0">
{#if dailyLog}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === 'daily-log' ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {currentId === 'daily-log' ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate('daily-log')}
>
{dailyLog.name}
@ -126,7 +126,7 @@
<ul class="m-0 list-none p-0">
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {$page.url.pathname.startsWith('/journal/archive') ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {page.url.pathname.startsWith('/journal/archive') ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => { goto('/journal/archive'); onnavigate?.(); }}
>
📅 Archiv
@ -135,7 +135,7 @@
</ul>
<!-- Jour Fixes -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<div class="mb-1 mt-6 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('meetings')}>
<span class="inline-block text-[10px] transition-transform {collapsed['meetings'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Jour Fixes
@ -147,7 +147,7 @@
{#each meetings as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{ctx.name}
@ -168,7 +168,7 @@
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/meetings'); onnavigate?.(); }}
>
Alle Jour Fixes &rarr;
@ -178,7 +178,7 @@
{/if}
<!-- Projects -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<div class="mb-1 mt-6 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('projects')}>
<span class="inline-block text-[10px] transition-transform {collapsed['projects'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Projects
@ -190,7 +190,7 @@
{#each projects as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
@ -211,7 +211,7 @@
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/projects'); onnavigate?.(); }}
>
Alle Projekte &rarr;
@ -221,7 +221,7 @@
{/if}
<!-- Companies -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<div class="mb-1 mt-6 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('companies')}>
<span class="inline-block text-[10px] transition-transform {collapsed['companies'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Firmen
@ -233,7 +233,7 @@
{#each companies as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
@ -254,7 +254,7 @@
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/companies'); onnavigate?.(); }}
>
Alle Firmen &rarr;
@ -264,7 +264,7 @@
{/if}
<!-- People -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<div class="mb-1 mt-6 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('people')}>
<span class="inline-block text-[10px] transition-transform {collapsed['people'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
People
@ -276,7 +276,7 @@
{#each people as ctx}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors {currentId === ctx.id ? 'bg-accent font-bold text-white' : 'text-[#ccc] hover:bg-white/5 hover:text-white'}"
onclick={() => navigate(ctx.id)}
>
{displayName(ctx)}
@ -297,7 +297,7 @@
{/if}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/persons'); onnavigate?.(); }}
>
Alle Personen &rarr;
@ -307,7 +307,7 @@
{/if}
<!-- Wiki -->
<div class="mb-1 mt-4 flex items-center justify-between pl-1 pr-1">
<div class="mb-1 mt-6 flex items-center justify-between pl-1 pr-1">
<button class="text-xs uppercase text-muted hover:text-white flex items-center gap-1" onclick={() => toggleSection('wiki')}>
<span class="inline-block text-[10px] transition-transform {collapsed['wiki'] ? '-rotate-90' : ''}">{@html '&#9660;'}</span>
Wiki
@ -319,7 +319,7 @@
{#each ($wikiNotebooks$ ?? []) as nb}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors text-[#ccc] hover:bg-white/5 hover:text-white"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors text-[#ccc] hover:bg-white/5 hover:text-white"
onclick={() => { goto(`/wiki/notebook/${nb.id}`); onnavigate?.(); }}
>
{nb.name}
@ -328,7 +328,7 @@
{/each}
<li>
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-xs text-[#666] transition-colors hover:bg-white/5 hover:text-[#aaa]"
onclick={() => { goto('/wiki'); onnavigate?.(); }}
>
Alle Notizbücher &rarr;
@ -339,7 +339,7 @@
<div class="mt-4">
<button
class="mb-1 w-full rounded px-2.5 py-2 text-left text-sm transition-colors flex items-center gap-2 text-[#ccc] hover:bg-white/5 hover:text-white"
class="mb-1 w-full rounded px-2.5 py-2.5 text-left text-sm transition-colors flex items-center gap-2 text-[#ccc] hover:bg-white/5 hover:text-white"
onclick={() => { goto('/trash'); onnavigate?.(); }}
>
<span>🗑</span>

View File

@ -245,7 +245,7 @@ export async function createHistoryEntry(topicId: string, date: string, text: st
topicId,
date,
text,
sortOrder: existing.length,
sortOrder: Date.now(),
linkedContextId,
doneAt: null,
wiedervorlageDate: autoWiedervorlage ? date : null,

View File

@ -13,3 +13,19 @@ export function normalizeTitleAndBody(
const newBody = body.trim() ? `${title}\n\n${body.trim()}` : title;
return { title: short, body: newBody };
}
/**
* Splits raw combined input into title + body.
* First line (stripped of leading markdown heading markers) becomes the title.
* Remaining lines become the body.
* Then normalizeTitleAndBody is applied to enforce maxLen.
*/
export function extractTitleAndBody(
raw: string,
maxLen = DEFAULT_MAX_TITLE_LENGTH
): { title: string; body: string } {
const lines = raw.split('\n');
const firstLine = lines[0].replace(/^#{1,6}\s*/, '').trim();
const rest = lines.slice(1).join('\n').trim();
return normalizeTitleAndBody(firstLine, rest, maxLen);
}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import ContextPage from '$lib/components/ContextPage.svelte';
const contextId = $derived($page.params.id);
const contextId = $derived(page.params.id);
</script>
{#key contextId}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { db } from '$lib/db/schema';
import RenderedMarkdown from '$lib/components/RenderedMarkdown.svelte';
@ -18,8 +18,8 @@
});
// URL params
const month = $derived($page.url.searchParams.get('month') ?? ''); // YYYY-MM
const day = $derived($page.url.searchParams.get('day') ?? ''); // DD
const month = $derived(page.url.searchParams.get('month') ?? ''); // YYYY-MM
const day = $derived(page.url.searchParams.get('day') ?? ''); // DD
// View: 'index' | 'month' | 'day'
const view = $derived(day ? 'day' : month ? 'month' : 'index');

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { db } from '$lib/db/schema';
import { upsertPage, softDeletePage, togglePageFavorite, getNotebooksForPage, getBacklinksForPage, assignPageToNotebook, removePageFromNotebook, getAllNotebooks } from '$lib/db/repositories';
@ -9,7 +9,7 @@
import type { Page, Notebook } from '@ka-note/shared';
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
const pageId = $derived($page.params.id);
const pageId = $derived(page.params.id);
let currentPage = $state<Page | undefined>(undefined);
const SCOPE_KEY = 'wiki-scope';

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
@ -12,7 +12,7 @@
if ($notebook$) currentScope.set($notebook$.isPrivate ? 'private' : 'business');
});
const notebookId = $derived($page.params.id);
const notebookId = $derived(page.params.id);
const notebook$ = liveQuery(() => db.notebooks.get(notebookId));
const pages$ = pagesForNotebook(notebookId);

Binary file not shown.

Binary file not shown.