This commit is contained in:
beo3000 2026-02-25 08:10:54 +01:00
parent a62ada936d
commit d1286cd717
7 changed files with 244 additions and 93 deletions

View File

@ -1 +1 @@
1.1.62
1.1.68

View File

@ -6,8 +6,9 @@
interface Props {
entry: HistoryEntry;
onedit?: (id: string, text: string, date: string) => void;
onediting?: (active: boolean) => void;
}
let { entry, onedit }: Props = $props();
let { entry, onedit, onediting }: Props = $props();
let editing = $state(false);
let editText = $state('');
@ -17,15 +18,18 @@
editText = entry.text;
editDate = entry.date;
editing = true;
onediting?.(true);
}
function saveEdit() {
onedit?.(entry.id, editText, editDate);
editing = false;
onediting?.(false);
}
function cancelEdit() {
editing = false;
onediting?.(false);
}
</script>
@ -44,7 +48,20 @@
</div>
{#if editing}
<div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onkeydown={(e) => { if (e.key === 'Escape') cancelEdit(); }}>
<div class="flex items-center gap-2 mb-1 flex-wrap">
<input
type="date"
class="rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white"
bind:value={editDate}
title="Datum des Eintrags"
/>
<div class="ml-auto flex gap-1">
<button class="rounded bg-[#444] px-3 py-1 text-sm text-[#ccc] hover:bg-[#555]" onclick={cancelEdit}>Abbrechen</button>
<button class="rounded bg-success px-3 py-1 text-sm font-bold text-white" onclick={saveEdit}>Speichern</button>
</div>
</div>
<MarkdownEditor
content={editText}
wikiScope={null}
@ -66,7 +83,10 @@
</div>
</div>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div ondblclick={startEdit}>
<RenderedMarkdown text={entry.text} />
</div>
{/if}
</div>
{/if}

View File

@ -236,7 +236,7 @@
</div>
{/if}
<div class="mb-8 flex flex-col gap-2.5 rounded-lg border border-border bg-sidebar p-4">
<div class="mb-8 flex flex-col gap-2.5 rounded-lg border border-border bg-sidebar p-4" class:hidden={editingId !== null}>
<MarkdownEditor
bind:this={entryEditor}
placeholder="Was ist passiert? (1. Zeile = Titel)"
@ -301,12 +301,41 @@
<div class="mb-4 text-xl font-bold text-accent">{selectedDate}</div>
{#each filteredEntries as entry (entry.id)}
{#if editingId === entry.id}
<div class="mb-3 flex flex-col gap-2 rounded border border-accent bg-card-bg p-2.5">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="mb-3 flex flex-col gap-2 rounded border border-accent bg-card-bg p-2.5" onkeydown={(e) => { if (e.key === 'Escape') cancelEdit(); }}>
<div class="flex items-center gap-2 flex-wrap">
<button
class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110"
onclick={saveEdit}
>Speichern</button>
<button
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
onclick={cancelEdit}
>Abbrechen</button>
<input
type="date"
class="rounded border border-[#444] bg-bg px-2 py-1 text-sm text-white"
bind:value={editDate}
title="Datum des Eintrags"
/>
<div class="ml-auto flex gap-1 rounded-full bg-[#333] p-0.5">
<button
class="rounded-full px-2.5 py-1 text-xs font-bold transition-all {!editIsPrivate ? 'bg-accent text-white shadow' : 'text-[#aaa] hover:text-white'}"
onclick={() => editIsPrivate = false}
>Firma</button>
<button
class="rounded-full px-2.5 py-1 text-xs font-bold transition-all {editIsPrivate ? 'text-white shadow' : 'text-[#aaa] hover:text-white'}"
style={editIsPrivate ? `background-color: ${$scopeSettings.privateColor}` : ''}
onclick={() => editIsPrivate = true}
>Privat</button>
</div>
</div>
<input
type="text"
class="rounded border border-[#444] bg-bg px-2.5 py-1.5 font-mono text-white"
bind:value={editTitle}
use:mention
onkeydown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); saveEdit(); } }}
/>
<MarkdownEditor
content={editBody}
@ -314,6 +343,7 @@
minHeight="60px"
wikiScope={journalScope === 'private'}
onchange={(md) => editBody = md}
onsave={saveEdit}
/>
<div class="flex items-center gap-2 flex-wrap">
<button
@ -347,7 +377,8 @@
{@const lines = entry.text.split('\n')}
{@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">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="group mb-3 flex items-start gap-2 rounded bg-card-bg p-2.5" ondblclick={() => startEdit(entry)}>
<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">

View File

@ -114,6 +114,8 @@
}
}
let historyEditing = $state(false);
function handleEditHistory(id: string, text: string, date: string) {
updateHistoryEntry(id, text, undefined, date);
}
@ -238,7 +240,7 @@
{#if !collapsed}
<div class="px-5 pb-5 border-t border-[#444]">
<!-- Note input -->
<div class="mt-2.5 flex flex-col gap-1">
<div class="mt-2.5 flex flex-col gap-1" class:hidden={historyEditing}>
<MarkdownEditor
bind:this={noteEditor}
placeholder="- Notiz... (z.B. '-> NAME' or '@P:PROJECT')"
@ -319,7 +321,7 @@
class="mt-4 max-h-[300px] overflow-y-auto rounded border border-[#333] bg-[#222] p-2.5"
>
{#each $history as entry (entry.id)}
<HistoryItem {entry} onedit={handleEditHistory} />
<HistoryItem {entry} onedit={handleEditHistory} onediting={(a) => historyEditing = a} />
{/each}
</div>
{/if}

View File

@ -2,6 +2,31 @@ import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import type { Page, Notebook } from '@ka-note/shared';
export interface PageWithNotebooks extends Page {
notebooks: Pick<Notebook, 'id' | 'name'>[];
}
async function enrichWithNotebooks(pages: Page[]): Promise<PageWithNotebooks[]> {
if (pages.length === 0) return [];
const allJoins = await db.pageNotebooks
.where('pageId').anyOf(pages.map(p => p.id))
.filter(pn => !pn.deletedAt)
.toArray();
const notebookIds = [...new Set(allJoins.map(j => j.notebookId))];
const notebooks = notebookIds.length > 0
? (await db.notebooks.bulkGet(notebookIds)).filter((n): n is Notebook => !!n && !n.deletedAt)
: [];
const nbMap = new Map(notebooks.map(n => [n.id, n]));
return pages.map(p => ({
...p,
notebooks: allJoins
.filter(j => j.pageId === p.id)
.map(j => nbMap.get(j.notebookId))
.filter((n): n is Notebook => !!n)
.map(n => ({ id: n.id, name: n.name }))
}));
}
export function allNotebooks() {
return liveQuery(() =>
db.notebooks.filter(n => !n.deletedAt).sortBy('sortOrder')
@ -30,9 +55,10 @@ export function notebooksForContext(contextId: string) {
}
export function favoritePages() {
return liveQuery(() =>
db.pages.filter(p => !p.deletedAt && !!p.isFavorite).sortBy('title')
);
return liveQuery(async () => {
const pages = await db.pages.filter(p => !p.deletedAt && !!p.isFavorite).sortBy('title');
return enrichWithNotebooks(pages);
});
}
export function allPages() {
@ -54,6 +80,16 @@ export function pagesForNotebook(notebookId: string) {
});
}
export function recentlyEditedPages(limit = 10) {
return liveQuery(async () => {
const pages = await db.pages.filter(p => !p.deletedAt).toArray();
const sorted = pages
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.slice(0, limit);
return enrichWithNotebooks(sorted);
});
}
export function unassignedPages() {
return liveQuery(async () => {
const allPages = await db.pages.filter(p => !p.deletedAt).toArray();

View File

@ -25,7 +25,7 @@ export function extractTitleAndBody(
maxLen = DEFAULT_MAX_TITLE_LENGTH
): { title: string; body: string } {
const lines = raw.split('\n');
const firstLine = lines[0].replace(/^#{1,6}\s*/, '').trim();
const firstLine = lines[0].replace(/^#{1,6}\s*/, '').replace(/\\$/, '').trim();
const rest = lines.slice(1).join('\n').trim();
return normalizeTitleAndBody(firstLine, rest, maxLen);
}

View File

@ -1,12 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { allNotebooks, unassignedPages, favoritePages } from '$lib/stores/wiki';
import { allNotebooks, unassignedPages, favoritePages, recentlyEditedPages, type PageWithNotebooks } from '$lib/stores/wiki';
import { createNotebook, createPage, toggleNotebookFavorite, upsertNotebook, reorderNotebook } from '$lib/db/repositories';
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
const notebooks$ = allNotebooks();
const unassigned$ = unassignedPages();
const favoritePages$ = favoritePages();
const recentPages$ = recentlyEditedPages();
const SCOPE_KEY = 'wiki-scope';
let scope = $state<'business' | 'private'>(
@ -25,6 +26,23 @@
($favoritePages$ ?? []).filter(p => p.isPrivate === (scope === 'private'))
);
const filteredRecentPages = $derived(
($recentPages$ ?? []).filter(p => p.isPrivate === (scope === 'private'))
);
const TAB_KEY = 'wiki-tab';
let activeTab = $state<'notebooks' | 'favorites' | 'recent'>(
(typeof localStorage !== 'undefined' ? localStorage.getItem(TAB_KEY) as 'notebooks' | 'favorites' | 'recent' : null) ?? 'notebooks'
);
function setTab(t: 'notebooks' | 'favorites' | 'recent') {
activeTab = t;
localStorage.setItem(TAB_KEY, t);
}
const quickTabPages = $derived<PageWithNotebooks[]>(
activeTab === 'favorites' ? filteredFavoritePages : filteredRecentPages
);
let creatingNotebook = $state(false);
let newNotebookName = $state('');
@ -76,48 +94,64 @@
</div>
</h1>
<!-- Favorite pages -->
{#if filteredFavoritePages.length > 0}
<section>
<h2 class="mb-2 text-sm font-semibold uppercase text-muted">Favoriten</h2>
<div class="flex flex-wrap gap-2">
{#each filteredFavoritePages as page (page.id)}
<section class="rounded-lg border border-[#333] bg-[#1a1a1a]">
<!-- Tab bar -->
<div class="flex border-b border-[#333]">
<button
class="flex items-center gap-1.5 rounded-lg border border-warning/40 bg-warning/10 px-3 py-1.5 text-sm text-warning hover:bg-warning/20 transition-colors"
onclick={() => goto(`/wiki/${page.id}`)}
class="flex items-center gap-1.5 px-4 py-2.5 text-sm font-semibold transition-colors border-b-2 -mb-px {activeTab === 'notebooks' ? 'border-accent text-accent' : 'border-transparent text-muted hover:text-white'}"
onclick={() => setTab('notebooks')}
>
<span class="text-xs"></span>
<span>{page.title}</span>
</button>
{/each}
</div>
</section>
<span>📓</span>
<span>Notizbücher</span>
{#if filteredNotebooks.length > 0}
<span class="rounded-full bg-white/10 px-1.5 py-0.5 text-xs">{filteredNotebooks.length}</span>
{/if}
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase text-muted">Notizbücher</h2>
<div class="flex items-center gap-3">
</button>
<button
class="flex items-center gap-1.5 px-4 py-2.5 text-sm font-semibold transition-colors border-b-2 -mb-px {activeTab === 'favorites' ? 'border-warning text-warning' : 'border-transparent text-muted hover:text-white'}"
onclick={() => setTab('favorites')}
>
<span></span>
<span>Favoriten</span>
{#if filteredFavoritePages.length > 0}
<span class="rounded-full bg-white/10 px-1.5 py-0.5 text-xs">{filteredFavoritePages.length}</span>
{/if}
</button>
<button
class="flex items-center gap-1.5 px-4 py-2.5 text-sm font-semibold transition-colors border-b-2 -mb-px {activeTab === 'recent' ? 'border-[#aaa] text-[#ccc]' : 'border-transparent text-muted hover:text-white'}"
onclick={() => setTab('recent')}
>
<span></span>
<span>Zuletzt bearbeitet</span>
</button>
<!-- Actions (right side) -->
{#if activeTab === 'notebooks'}
<div class="ml-auto flex items-center gap-2 px-3">
<button
class="rounded bg-accent px-3 py-1.5 text-sm font-medium text-white hover:bg-accent/80"
onclick={addPage}
>+ Neue Seite</button>
<button class="rounded bg-white/10 px-3 py-1.5 text-sm font-medium text-muted hover:bg-white/20 hover:text-white" onclick={() => creatingNotebook = true}>+ Notizbuch</button>
>+ Seite</button>
<button
class="rounded bg-white/10 px-3 py-1.5 text-sm font-medium text-muted hover:bg-white/20 hover:text-white"
onclick={() => creatingNotebook = true}
>+ Notizbuch</button>
</div>
{/if}
</div>
<div class="space-y-1">
<!-- Notebooks tab -->
{#if activeTab === 'notebooks'}
<div class="divide-y divide-[#2a2a2a]">
{#each filteredNotebooks as nb, i (nb.id)}
<div class="flex items-center gap-1 rounded hover:bg-white/5 group">
<div class="flex flex-col pl-1">
<div class="flex items-center gap-1 hover:bg-white/5 group">
<div class="flex flex-col pl-2">
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none text-[10px] px-0.5" onclick={() => reorderNotebook(nb.id, 'up')} disabled={i === 0} title="Nach oben"></button>
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none text-[10px] px-0.5" onclick={() => reorderNotebook(nb.id, 'down')} disabled={i === filteredNotebooks.length - 1} title="Nach unten"></button>
</div>
<button
class="flex flex-1 items-center gap-2 px-3 py-2 text-left text-sm text-[#ccc] hover:text-white"
class="flex flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm text-[#ccc] hover:text-white"
onclick={() => goto(`/wiki/notebook/${nb.id}`)}
>
<span class="flex-1">{nb.name}</span>
</button>
>{nb.name}</button>
<button
class="pr-3 text-base transition-opacity {nb.isFavorite ? 'text-warning' : 'text-muted hover:text-warning'}"
onclick={() => toggleNotebookFavorite(nb.id)}
@ -125,11 +159,11 @@
>★</button>
</div>
{:else}
<p class="text-sm text-muted px-3">Keine {scope === 'private' ? 'privaten' : 'Firma'}-Notizbücher vorhanden.</p>
<p class="px-4 py-3 text-sm text-muted">Keine {scope === 'private' ? 'privaten' : 'Firma'}-Notizbücher vorhanden.</p>
{/each}
{#if creatingNotebook}
<input
class="w-full rounded bg-white/10 px-3 py-2 text-sm text-white outline-none placeholder:text-muted"
class="w-full rounded-b-lg bg-white/10 px-4 py-2.5 text-sm text-white outline-none placeholder:text-muted"
placeholder="Notizbuchname..."
bind:value={newNotebookName}
onkeydown={handleInputKeydown}
@ -138,24 +172,52 @@
/>
{/if}
</div>
<!-- Unassigned pages -->
<!-- Unassigned pages (shown below notebooks) -->
{#if filteredUnassigned.length > 0}
<section>
<h2 class="mb-2 text-sm font-semibold uppercase text-muted">
Nicht zugeordnet
<span class="ml-1 rounded-full bg-white/10 px-1.5 py-0.5 text-xs">{filteredUnassigned.length}</span>
</h2>
<div class="space-y-1">
<div class="border-t border-[#333] px-4 py-2">
<span class="text-xs font-semibold uppercase text-muted">Nicht zugeordnet</span>
<span class="ml-1 rounded-full bg-white/10 px-1.5 py-0.5 text-xs text-muted">{filteredUnassigned.length}</span>
</div>
<div class="divide-y divide-[#2a2a2a]">
{#each filteredUnassigned as page (page.id)}
<button
class="flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm text-[#ccc] hover:bg-white/5 hover:text-white"
class="flex w-full items-center px-4 py-2 text-left text-sm text-[#ccc] hover:bg-white/5 hover:text-white"
onclick={() => goto(`/wiki/${page.id}`)}
>
<span>{page.title}</span>
</button>
>{page.title}</button>
{/each}
</div>
</section>
{/if}
<!-- Favorites / Recent tab -->
{:else}
{#if quickTabPages.length > 0}
<ul class="divide-y divide-[#2a2a2a]">
{#each quickTabPages as page (page.id)}
<li class="flex items-center gap-2 px-3 py-2 hover:bg-white/5">
<button
class="flex-1 text-left text-sm text-[#ddd] hover:text-white font-medium"
onclick={() => goto(`/wiki/${page.id}`)}
title={activeTab === 'recent' && page.updatedAt ? new Date(page.updatedAt).toLocaleString('de-DE') : undefined}
>{page.title}</button>
{#if page.notebooks.length > 0}
<div class="flex items-center gap-1 flex-shrink-0">
{#each page.notebooks as nb}
<button
class="rounded bg-white/10 px-2 py-0.5 text-xs text-muted hover:bg-white/20 hover:text-white transition-colors"
onclick={(e) => { e.stopPropagation(); goto(`/wiki/notebook/${nb.id}`); }}
title="Notizbuch: {nb.name}"
>{nb.name}</button>
{/each}
</div>
{/if}
</li>
{/each}
</ul>
{:else}
<p class="px-4 py-3 text-sm text-muted">
{activeTab === 'favorites' ? 'Keine Favoriten vorhanden.' : 'Noch keine bearbeiteten Seiten.'}
</p>
{/if}
{/if}
</section>
</div>