This commit is contained in:
beo3000 2026-02-23 08:49:19 +01:00
parent 7886947a65
commit d63c75e551
16 changed files with 174 additions and 33 deletions

View File

@ -1 +1 @@
1.1.7
1.1.11

View File

@ -49,12 +49,19 @@
editing = false;
}
}
const headerColor = $derived(
showScopeSwitch
? (journalScope === 'private' ? $scopeSettings.privateColor : $scopeSettings.businessColor)
: $scopeSettings.businessColor
);
</script>
<h1 class="flex items-center justify-between border-b-2 border-accent pb-2.5 mb-5">
<h1 class="flex items-center justify-between border-b-2 pb-2.5 mb-5" style="border-color: {headerColor}">
{#if editing}
<input
class="text-2xl font-bold bg-transparent border-b-2 border-accent text-inherit outline-none w-full mr-4"
class="text-2xl font-bold bg-transparent border-b-2 text-inherit outline-none w-full mr-4"
style="border-color: {headerColor}"
bind:value={nameInput}
onkeydown={onKeydown}
onblur={saveEdit}

View File

@ -346,6 +346,15 @@
<input class="rounded border border-[#555] bg-[#111] px-2.5 py-1.5 text-[#ddd]"
value={meta.links} onchange={(e) => updateMeta('links', e.currentTarget.value)} />
</div>
<div class="mb-2.5 flex flex-col">
<label class="mb-1 text-sm text-[#aaa]">Notizen:</label>
<EditableMarkdown
content={(context.meta as ProjectMeta)?.notes ?? ''}
placeholder="Notizen..."
minHeight="60px"
onchange={(md) => updateMeta('notes', md)}
/>
</div>
{:else if isCompany}
{@const meta = (context.meta ?? { website: '', address: '' }) as CompanyMeta}
<div class="mb-2.5 flex flex-col">

View File

@ -10,7 +10,7 @@
import ConfirmDialog from './ConfirmDialog.svelte';
import WiedervorlageSection from './WiedervorlageSection.svelte';
import LinkTitle from './LinkTitle.svelte';
import { currentScope } from '$lib/stores/scopeContext';
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
interface Props {
contextId: string;
@ -120,12 +120,14 @@
let editingId = $state<string | null>(null);
let editTitle = $state('');
let editBody = $state('');
let editIsPrivate = $state(false);
function startEdit(entry: { id: string; text: string }) {
function startEdit(entry: { id: string; text: string; isPrivate?: boolean }) {
const lines = entry.text.split('\n');
editingId = entry.id;
editTitle = lines[0];
editBody = lines.slice(1).join('\n').trim();
editIsPrivate = !!entry.isPrivate;
}
function cancelEdit() {
@ -135,7 +137,7 @@
async function saveEdit() {
if (!editingId || !editTitle.trim()) return;
const text = editBody.trim() ? `${editTitle.trim()}\n${editBody.trim()}` : editTitle.trim();
await updateHistoryEntry(editingId, text);
await updateHistoryEntry(editingId, text, editIsPrivate);
editingId = null;
}
@ -273,7 +275,8 @@
/>
<div class="flex items-center gap-4">
<button
class="rounded bg-accent px-4 py-2 font-bold text-white hover:brightness-110"
class="rounded px-4 py-2 font-bold text-white hover:brightness-110"
style="background-color: {journalScope === 'private' ? $scopeSettings.privateColor : $scopeSettings.businessColor}"
onclick={handleAddEntry}
>
+ {selectedLinkedContextId ? 'Thema hinzufügen' : 'Notiz hinzufügen'}
@ -312,7 +315,7 @@
wikiScope={journalScope === 'private'}
onchange={(md) => editBody = md}
/>
<div class="flex gap-2">
<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}
@ -321,6 +324,17 @@
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
onclick={cancelEdit}
>Abbrechen</button>
<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>
</div>
{:else}
@ -330,10 +344,13 @@
<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>
<div class="flex-1">
<div class="font-bold">
<div class="flex flex-wrap items-center gap-1.5 font-bold">
<LinkTitle text={title} />
{#if entry.wiedervorlageDate && !entry.wiedervorlageResolvedAt}
<span class="text-base leading-none text-amber-400" title="In Wiedervorlage bis {entry.wiedervorlageDate}"></span>
{/if}
{#if entry.linkedContextId}
<span class="ml-1 inline-block rounded bg-accent/20 px-1.5 py-0.5 text-xs font-normal text-accent">
<span class="inline-block rounded bg-accent/20 px-1.5 py-0.5 text-xs font-normal text-accent">
{contextNameMap().get(entry.linkedContextId) ?? entry.linkedContextId}
</span>
{/if}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { softDeleteContext, toggleFavorite } from '$lib/db/repositories';
import { softDeleteContext, toggleFavorite, reorderContext } from '$lib/db/repositories';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { AgendaContext, PersonMeta, PersonSubType } from '@ka-note/shared';
@ -41,9 +41,13 @@
{#if persons.length > 0}
<div class="flex flex-col gap-2">
{#each persons as ctx (ctx.id)}
{#each persons as ctx, i (ctx.id)}
{@const subType = ((ctx.meta as PersonMeta | null)?.personSubType ?? 'contact') as PersonSubType}
<div class="flex items-center gap-2">
<div class="flex flex-col">
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'up')} disabled={i === 0} title="Nach oben"></button>
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'down')} disabled={i === persons.length - 1} title="Nach unten"></button>
</div>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}

View File

@ -98,23 +98,27 @@
>Abbrechen</button>
</div>
{:else}
<div class="flex gap-2">
<div class="flex gap-1.5">
<button
class="rounded bg-green-700 px-3 py-1 text-sm font-bold text-white hover:brightness-110"
class="rounded bg-green-700 px-2.5 py-1.5 text-base leading-none text-white hover:brightness-110 active:brightness-90"
title="Erledigt"
onclick={handleOk}
>Ok</button>
></button>
<button
class="rounded bg-amber-700 px-3 py-1 text-sm text-white hover:brightness-110"
class="rounded bg-amber-700 px-2.5 py-1.5 text-base leading-none text-white hover:brightness-110 active:brightness-90"
title="Verschieben"
onclick={() => showVerschieben = true}
>Verschieben</button>
>📅</button>
<button
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
class="rounded bg-[#444] px-2.5 py-1.5 text-base leading-none text-white hover:bg-[#555] active:bg-[#555]"
title="In Thema wandeln"
onclick={() => showConvert = true}
>In Thema wandeln</button>
></button>
<button
class="ml-auto rounded bg-red-900/60 px-3 py-1 text-sm text-red-300 hover:bg-red-800"
class="ml-auto rounded bg-red-900/60 px-2.5 py-1.5 text-base leading-none text-red-300 hover:bg-red-800 active:bg-red-800"
title="Löschen"
onclick={handleDelete}
>Löschen</button>
>🗑</button>
</div>
{/if}
</div>

View File

@ -66,6 +66,21 @@ export async function toggleFavorite(id: string): Promise<void> {
}
}
export async function reorderContext(id: string, direction: 'up' | 'down'): Promise<void> {
const ctx = await db.contexts.get(id);
if (!ctx) return;
const siblings = await db.contexts
.filter(c => !c.deletedAt && c.type === ctx.type)
.sortBy('sortOrder');
const idx = siblings.findIndex(c => c.id === id);
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
if (swapIdx < 0 || swapIdx >= siblings.length) return;
const other = siblings[swapIdx];
const ts = now();
await db.contexts.update(id, { sortOrder: other.sortOrder, updatedAt: ts, version: ctx.version + 1 });
await db.contexts.update(other.id, { sortOrder: ctx.sortOrder, updatedAt: ts, version: other.version + 1 });
}
export async function findContextByMentionName(name: string, type: 'person' | 'project' | 'company'): Promise<AgendaContext | undefined> {
const q = name.toLowerCase();
return db.contexts
@ -257,10 +272,12 @@ export async function convertToTopic(entryId: string, contextId: string): Promis
return topic;
}
export async function updateHistoryEntry(id: string, text: string): Promise<void> {
export async function updateHistoryEntry(id: string, text: string, isPrivate?: boolean): Promise<void> {
const entry = await db.historyEntries.get(id);
if (entry) {
await db.historyEntries.update(id, { text, updatedAt: now(), version: entry.version + 1 });
const patch: Record<string, unknown> = { text, updatedAt: now(), version: entry.version + 1 };
if (isPrivate !== undefined) patch.isPrivate = isPrivate;
await db.historyEntries.update(id, patch);
}
}
@ -381,6 +398,13 @@ export async function upsertPage(page: Partial<Page> & { id: string }): Promise<
}
}
export async function togglePageFavorite(id: string): Promise<void> {
const p = await db.pages.get(id);
if (p) {
await db.pages.update(id, { isFavorite: !p.isFavorite, updatedAt: now(), version: p.version + 1 });
}
}
export async function softDeletePage(id: string): Promise<void> {
const page = await db.pages.get(id);
if (page) {
@ -469,6 +493,21 @@ export async function toggleNotebookFavorite(id: string): Promise<void> {
}
}
export async function reorderNotebook(id: string, direction: 'up' | 'down'): Promise<void> {
const nb = await db.notebooks.get(id);
if (!nb) return;
const siblings = await db.notebooks
.filter(n => !n.deletedAt)
.sortBy('sortOrder');
const idx = siblings.findIndex(n => n.id === id);
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
if (swapIdx < 0 || swapIdx >= siblings.length) return;
const other = siblings[swapIdx];
const ts = now();
await db.notebooks.update(id, { sortOrder: other.sortOrder, updatedAt: ts, version: nb.version + 1 });
await db.notebooks.update(other.id, { sortOrder: nb.sortOrder, updatedAt: ts, version: other.version + 1 });
}
export async function softDeleteNotebook(id: string): Promise<void> {
const nb = await db.notebooks.get(id);
if (nb) {

View File

@ -118,6 +118,14 @@ export class KaNoteDB extends Dexie {
if (nb.isFavorite === undefined) nb.isFavorite = false;
});
});
this.version(12).stores({
pages: '&id, deletedAt, isPrivate, isFavorite',
}).upgrade(tx => {
return tx.table('pages').toCollection().modify((p: Record<string, unknown>) => {
if (p.isFavorite === undefined) p.isFavorite = false;
});
});
}
}

View File

@ -29,6 +29,12 @@ export function notebooksForContext(contextId: string) {
);
}
export function favoritePages() {
return liveQuery(() =>
db.pages.filter(p => !p.deletedAt && !!p.isFavorite).sortBy('title')
);
}
export function allPages() {
return liveQuery(() =>
db.pages.filter(p => !p.deletedAt).sortBy('title')

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { softDeleteContext, toggleFavorite, upsertContext } from '$lib/db/repositories';
import { softDeleteContext, toggleFavorite, upsertContext, reorderContext } from '$lib/db/repositories';
import { newId } from '$lib/db/helpers';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@ -67,8 +67,12 @@
{#if companies.length > 0}
<div class="flex flex-col gap-2">
{#each companies as ctx (ctx.id)}
{#each companies as ctx, i (ctx.id)}
<div class="flex items-center gap-2">
<div class="flex flex-col">
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'up')} disabled={i === 0} title="Nach oben"></button>
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'down')} disabled={i === companies.length - 1} title="Nach unten"></button>
</div>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { softDeleteContext, toggleFavorite, upsertContext } from '$lib/db/repositories';
import { softDeleteContext, toggleFavorite, upsertContext, reorderContext } from '$lib/db/repositories';
import { newId } from '$lib/db/helpers';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@ -63,8 +63,12 @@
{#if meetings.length > 0}
<div class="flex flex-col gap-2">
{#each meetings as ctx (ctx.id)}
{#each meetings as ctx, i (ctx.id)}
<div class="flex items-center gap-2">
<div class="flex flex-col">
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'up')} disabled={i === 0} title="Nach oben"></button>
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'down')} disabled={i === meetings.length - 1} title="Nach unten"></button>
</div>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { liveQuery } from 'dexie';
import { db } from '$lib/db/schema';
import { softDeleteContext, toggleFavorite, upsertContext } from '$lib/db/repositories';
import { softDeleteContext, toggleFavorite, upsertContext, reorderContext } from '$lib/db/repositories';
import { newId } from '$lib/db/helpers';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import type { AgendaContext, ProjectMeta } from '@ka-note/shared';
@ -74,8 +74,12 @@
{#if activeProjects.length > 0}
<h2 class="mb-3 text-sm font-bold uppercase text-[#aaa]">Aktiv</h2>
<div class="mb-8 flex flex-col gap-2">
{#each activeProjects as ctx (ctx.id)}
{#each activeProjects as ctx, i (ctx.id)}
<div class="flex items-center gap-2">
<div class="flex flex-col">
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'up')} disabled={i === 0} title="Nach oben"></button>
<button class="text-[#555] hover:text-[#aaa] disabled:opacity-20 leading-none px-1" onclick={() => reorderContext(ctx.id, 'down')} disabled={i === activeProjects.length - 1} title="Nach unten"></button>
</div>
<button
class="rounded border border-[#444] bg-sidebar px-2.5 py-2.5 transition-colors hover:border-[#666] {ctx.isFavorite ? 'text-yellow-400' : 'text-[#555] hover:text-yellow-400/60'}"
onclick={() => toggleFavorite(ctx.id)}

View File

@ -1,11 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { allNotebooks, unassignedPages } from '$lib/stores/wiki';
import { createNotebook, createPage, toggleNotebookFavorite, upsertNotebook } from '$lib/db/repositories';
import { allNotebooks, unassignedPages, favoritePages } 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 SCOPE_KEY = 'wiki-scope';
let scope = $state<'business' | 'private'>(
@ -20,6 +21,10 @@
($unassigned$ ?? []).filter(p => p.isPrivate === (scope === 'private'))
);
const filteredFavoritePages = $derived(
($favoritePages$ ?? []).filter(p => p.isPrivate === (scope === 'private'))
);
let creatingNotebook = $state(false);
let newNotebookName = $state('');
@ -71,6 +76,24 @@
</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)}
<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}`)}
>
<span class="text-xs"></span>
<span>{page.title}</span>
</button>
{/each}
</div>
</section>
{/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">
@ -83,8 +106,12 @@
</div>
<div class="space-y-1">
{#each filteredNotebooks as nb (nb.id)}
{#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">
<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"
onclick={() => goto(`/wiki/notebook/${nb.id}`)}

View File

@ -2,7 +2,7 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { db } from '$lib/db/schema';
import { upsertPage, softDeletePage, getNotebooksForPage, getBacklinksForPage, assignPageToNotebook, removePageFromNotebook, getAllNotebooks } from '$lib/db/repositories';
import { upsertPage, softDeletePage, togglePageFavorite, getNotebooksForPage, getBacklinksForPage, assignPageToNotebook, removePageFromNotebook, getAllNotebooks } from '$lib/db/repositories';
import MarkdownEditor from '$lib/components/MarkdownEditor.svelte';
import RenderedMarkdown from '$lib/components/RenderedMarkdown.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@ -59,6 +59,7 @@
async function saveBody() {
const body = editorRef?.getMarkdown() ?? '';
await upsertPage({ id: pageId, body });
if (currentPage) currentPage = { ...currentPage, body };
}
async function switchToRead() {
@ -137,6 +138,11 @@
>
{editing ? '✓ Fertig' : '✎ Bearbeiten'}
</button>
<button
class="rounded px-2 py-1.5 text-base transition-colors {currentPage.isFavorite ? 'text-warning' : 'text-muted hover:text-warning'}"
onclick={async () => { await togglePageFavorite(pageId); currentPage = { ...currentPage!, isFavorite: !currentPage!.isFavorite }; }}
title={currentPage.isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
>★</button>
<button
class="rounded px-2 py-1.5 text-xs text-muted hover:bg-danger/20 hover:text-danger ml-auto"
onclick={() => confirmDelete = true}

Binary file not shown.

View File

@ -23,6 +23,7 @@ export interface ProjectMeta {
status: string;
owner: string;
links: string;
notes?: string;
}
export interface PersonMeta {
@ -91,6 +92,7 @@ export interface Page extends SyncEntity {
body: string;
isPrivate: boolean;
sortOrder: number;
isFavorite?: boolean;
}
export interface Notebook extends SyncEntity {