chore: drop legacy context module files (companion to acb737e25)

Companion deletion sweep — acb737e25 removed all the *registry refs*
to the legacy `context` module, but its source files were still on
disk on main (because the original deletion in d3e2e73ca on the
articles-bulk-import branch was bundled with unrelated photon /
broadcast-rename work and never landed on main). Dropping them now
so the consolidation is self-contained:

- apps/mana/apps/web/src/lib/modules/context/ — entire module dir
- apps/mana/apps/web/src/routes/(app)/context/ — page routes
- apps/mana/apps/web/src/lib/components/dashboard/widgets/ContextDocsWidget.svelte
- apps/mana/apps/web/src/lib/i18n/locales/context/{de,en,es,fr,it}.json
- packages/shared-branding/src/logos/ContextLogo.svelte

Verified: svelte-check + tsc --noEmit both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-29 00:23:10 +02:00
parent 5c8faae4ea
commit a295894ca6
19 changed files with 0 additions and 2241 deletions

View file

@ -1,72 +0,0 @@
<script lang="ts">
/**
* ContextDocsWidget - Recent documents and spaces (local-first)
*/
import { _ } from 'svelte-i18n';
import { useRecentDocuments, useSpaces } from '$lib/data/cross-app-queries';
const docs = useRecentDocuments(5);
const spaces = useSpaces();
function getSpaceName(spaceId: string | null | undefined): string {
if (!spaceId) return '';
const space = (spaces.value ?? []).find((s) => s.id === spaceId);
return space?.name ?? '';
}
function formatDate(dateStr?: string): string {
if (!dateStr) return '';
return new Date(dateStr).toLocaleDateString('de-DE', { day: 'numeric', month: 'short' });
}
const typeIcons: Record<string, string> = {
text: '📝',
context: '📋',
prompt: '💡',
};
</script>
<div>
<div class="mb-3">
<h3 class="flex items-center gap-2 text-lg font-semibold">
<span>📝</span>
{$_('dashboard.widgets.context.title')}
</h3>
</div>
{#if docs.loading}
<div class="space-y-2">
{#each Array(3) as _}
<div class="h-8 animate-pulse rounded bg-surface-hover"></div>
{/each}
</div>
{:else if (docs.value ?? []).length === 0}
<div class="py-6 text-center">
<div class="mb-2 text-3xl">📝</div>
<p class="text-sm text-muted-foreground">{$_('dashboard.widgets.context.empty')}</p>
</div>
{:else}
<div class="space-y-1">
{#each docs.value ?? [] as doc (doc.id)}
<a
href="/context/documents/{doc.id}"
class="flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover"
>
<span>{typeIcons[doc.type ?? 'text'] ?? '📄'}</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{doc.title}</p>
{#if getSpaceName(doc.contextSpaceId)}
<p class="truncate text-xs text-muted-foreground">
{getSpaceName(doc.contextSpaceId)}
</p>
{/if}
</div>
<span class="flex-shrink-0 text-xs text-muted-foreground">
{formatDate(doc.updatedAt)}
</span>
</a>
{/each}
</div>
{/if}
</div>

View file

@ -1,87 +0,0 @@
{
"app": {
"name": "Context"
},
"common": {
"back": "Zurück",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"create": "Erstellen",
"edit": "Bearbeiten",
"loading": "Lade...",
"search": "Suchen",
"confirm": "Bestätigen",
"close": "Schließen",
"pin": "Anheften",
"unpin": "Lösen"
},
"nav": {
"home": "Übersicht",
"spaces": "Spaces",
"documents": "Dokumente",
"settings": "Einstellungen"
},
"spaces": {
"title": "Spaces",
"create": "Neuen Space erstellen",
"empty": "Noch keine Spaces vorhanden",
"name": "Name",
"description": "Beschreibung",
"deleteConfirm": "Alle Dokumente in diesem Space werden ebenfalls gelöscht.",
"searchPlaceholder": "Spaces durchsuchen..."
},
"documents": {
"title": "Dokumente",
"create": "Neues Dokument",
"empty": "Keine Dokumente vorhanden",
"deleteConfirm": "Das Dokument wird unwiderruflich gelöscht.",
"searchPlaceholder": "Dokumente durchsuchen...",
"types": {
"all": "Alle",
"text": "Text",
"context": "Kontext",
"prompt": "Prompt"
},
"editor": {
"titlePlaceholder": "Titel...",
"contentPlaceholder": "Schreibe deinen Text in Markdown...",
"preview": "Vorschau",
"edit": "Bearbeiten",
"words": "Wörter",
"saving": "Speichert...",
"saved": "Gespeichert",
"unsaved": "Ungespeichert",
"tags": "Tags",
"addTag": "Tag hinzufügen..."
}
},
"settings": {
"title": "Einstellungen"
},
"messages": {
"saved": "Gespeichert",
"deleted": "Gelöscht",
"error": "Ein Fehler ist aufgetreten",
"created": "Erstellt"
},
"home": {
"page_title_html": "Context - Mana",
"title": "Context",
"subtitle": "Dein Wissensmanagement Hub",
"stat_spaces": "Spaces",
"stat_documents": "Dokumente",
"stat_words": "Wörter",
"stat_split_label": "Text/Kontext/Prompt",
"action_spaces": "Spaces",
"action_all_documents": "Alle Dokumente",
"section_pinned": "Angeheftete Spaces",
"section_recent": "Zuletzt bearbeitet",
"action_show_all": "Alle anzeigen",
"badge_pinned": "Angeheftet",
"empty_title": "Noch keine Dokumente",
"empty_hint": "Erstelle deinen ersten Space und beginne mit dem Schreiben.",
"empty_action": "Ersten Space erstellen",
"confirm_delete_doc": "Dokument wirklich löschen?"
}
}

View file

@ -1,87 +0,0 @@
{
"app": {
"name": "Context"
},
"common": {
"back": "Back",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"create": "Create",
"edit": "Edit",
"loading": "Loading...",
"search": "Search",
"confirm": "Confirm",
"close": "Close",
"pin": "Pin",
"unpin": "Unpin"
},
"nav": {
"home": "Overview",
"spaces": "Spaces",
"documents": "Documents",
"settings": "Settings"
},
"spaces": {
"title": "Spaces",
"create": "Create new space",
"empty": "No spaces yet",
"name": "Name",
"description": "Description",
"deleteConfirm": "All documents in this space will also be deleted.",
"searchPlaceholder": "Search spaces..."
},
"documents": {
"title": "Documents",
"create": "New document",
"empty": "No documents yet",
"deleteConfirm": "The document will be permanently deleted.",
"searchPlaceholder": "Search documents...",
"types": {
"all": "All",
"text": "Text",
"context": "Context",
"prompt": "Prompt"
},
"editor": {
"titlePlaceholder": "Title...",
"contentPlaceholder": "Write your text in Markdown...",
"preview": "Preview",
"edit": "Edit",
"words": "Words",
"saving": "Saving...",
"saved": "Saved",
"unsaved": "Unsaved",
"tags": "Tags",
"addTag": "Add tag..."
}
},
"settings": {
"title": "Settings"
},
"messages": {
"saved": "Saved",
"deleted": "Deleted",
"error": "An error occurred",
"created": "Created"
},
"home": {
"page_title_html": "Context - Mana",
"title": "Context",
"subtitle": "Your knowledge management hub",
"stat_spaces": "Spaces",
"stat_documents": "Documents",
"stat_words": "Words",
"stat_split_label": "Text/Context/Prompt",
"action_spaces": "Spaces",
"action_all_documents": "All documents",
"section_pinned": "Pinned spaces",
"section_recent": "Recently edited",
"action_show_all": "Show all",
"badge_pinned": "Pinned",
"empty_title": "No documents yet",
"empty_hint": "Create your first space and start writing.",
"empty_action": "Create first space",
"confirm_delete_doc": "Really delete document?"
}
}

View file

@ -1,87 +0,0 @@
{
"app": {
"name": "Context"
},
"common": {
"back": "Atrás",
"cancel": "Cancelar",
"save": "Guardar",
"delete": "Eliminar",
"create": "Crear",
"edit": "Editar",
"loading": "Cargando...",
"search": "Buscar",
"confirm": "Confirmar",
"close": "Cerrar",
"pin": "Fijar",
"unpin": "Desfijar"
},
"nav": {
"home": "Resumen",
"spaces": "Espacios",
"documents": "Documentos",
"settings": "Ajustes"
},
"spaces": {
"title": "Espacios",
"create": "Crear nuevo espacio",
"empty": "Aún no hay espacios",
"name": "Nombre",
"description": "Descripción",
"deleteConfirm": "Todos los documentos en este espacio también serán eliminados.",
"searchPlaceholder": "Buscar espacios..."
},
"documents": {
"title": "Documentos",
"create": "Nuevo documento",
"empty": "Aún no hay documentos",
"deleteConfirm": "El documento será eliminado permanentemente.",
"searchPlaceholder": "Buscar documentos...",
"types": {
"all": "Todos",
"text": "Texto",
"context": "Contexto",
"prompt": "Prompt"
},
"editor": {
"titlePlaceholder": "Título...",
"contentPlaceholder": "Escribe tu texto en Markdown...",
"preview": "Vista previa",
"edit": "Editar",
"words": "Palabras",
"saving": "Guardando...",
"saved": "Guardado",
"unsaved": "Sin guardar",
"tags": "Etiquetas",
"addTag": "Añadir etiqueta..."
}
},
"settings": {
"title": "Ajustes"
},
"messages": {
"saved": "Guardado",
"deleted": "Eliminado",
"error": "Ha ocurrido un error",
"created": "Creado"
},
"home": {
"page_title_html": "Context - Mana",
"title": "Context",
"subtitle": "Tu hub de gestión del conocimiento",
"stat_spaces": "Spaces",
"stat_documents": "Documentos",
"stat_words": "Palabras",
"stat_split_label": "Texto/Contexto/Prompt",
"action_spaces": "Spaces",
"action_all_documents": "Todos los documentos",
"section_pinned": "Spaces fijados",
"section_recent": "Editados recientemente",
"action_show_all": "Mostrar todos",
"badge_pinned": "Fijado",
"empty_title": "Aún no hay documentos",
"empty_hint": "Crea tu primer Space y comienza a escribir.",
"empty_action": "Crear primer Space",
"confirm_delete_doc": "¿Eliminar realmente el documento?"
}
}

View file

@ -1,87 +0,0 @@
{
"app": {
"name": "Context"
},
"common": {
"back": "Retour",
"cancel": "Annuler",
"save": "Enregistrer",
"delete": "Supprimer",
"create": "Créer",
"edit": "Modifier",
"loading": "Chargement...",
"search": "Rechercher",
"confirm": "Confirmer",
"close": "Fermer",
"pin": "Épingler",
"unpin": "Détacher"
},
"nav": {
"home": "Vue d'ensemble",
"spaces": "Espaces",
"documents": "Documents",
"settings": "Paramètres"
},
"spaces": {
"title": "Espaces",
"create": "Créer un nouvel espace",
"empty": "Pas encore d'espaces",
"name": "Nom",
"description": "Description",
"deleteConfirm": "Tous les documents de cet espace seront également supprimés.",
"searchPlaceholder": "Rechercher des espaces..."
},
"documents": {
"title": "Documents",
"create": "Nouveau document",
"empty": "Pas encore de documents",
"deleteConfirm": "Le document sera définitivement supprimé.",
"searchPlaceholder": "Rechercher des documents...",
"types": {
"all": "Tous",
"text": "Texte",
"context": "Contexte",
"prompt": "Prompt"
},
"editor": {
"titlePlaceholder": "Titre...",
"contentPlaceholder": "Écrivez votre texte en Markdown...",
"preview": "Aperçu",
"edit": "Modifier",
"words": "Mots",
"saving": "Enregistrement...",
"saved": "Enregistré",
"unsaved": "Non enregistré",
"tags": "Tags",
"addTag": "Ajouter un tag..."
}
},
"settings": {
"title": "Paramètres"
},
"messages": {
"saved": "Enregistré",
"deleted": "Supprimé",
"error": "Une erreur est survenue",
"created": "Créé"
},
"home": {
"page_title_html": "Context - Mana",
"title": "Context",
"subtitle": "Ton hub de gestion des connaissances",
"stat_spaces": "Spaces",
"stat_documents": "Documents",
"stat_words": "Mots",
"stat_split_label": "Texte/Contexte/Prompt",
"action_spaces": "Spaces",
"action_all_documents": "Tous les documents",
"section_pinned": "Spaces épinglés",
"section_recent": "Modifiés récemment",
"action_show_all": "Tout afficher",
"badge_pinned": "Épinglé",
"empty_title": "Pas encore de documents",
"empty_hint": "Crée ton premier space et commence à écrire.",
"empty_action": "Créer le premier space",
"confirm_delete_doc": "Vraiment supprimer le document ?"
}
}

View file

@ -1,87 +0,0 @@
{
"app": {
"name": "Context"
},
"common": {
"back": "Indietro",
"cancel": "Annulla",
"save": "Salva",
"delete": "Elimina",
"create": "Crea",
"edit": "Modifica",
"loading": "Caricamento...",
"search": "Cerca",
"confirm": "Conferma",
"close": "Chiudi",
"pin": "Fissa",
"unpin": "Sgancia"
},
"nav": {
"home": "Panoramica",
"spaces": "Spazi",
"documents": "Documenti",
"settings": "Impostazioni"
},
"spaces": {
"title": "Spazi",
"create": "Crea nuovo spazio",
"empty": "Nessuno spazio ancora",
"name": "Nome",
"description": "Descrizione",
"deleteConfirm": "Tutti i documenti in questo spazio verranno eliminati.",
"searchPlaceholder": "Cerca spazi..."
},
"documents": {
"title": "Documenti",
"create": "Nuovo documento",
"empty": "Nessun documento ancora",
"deleteConfirm": "Il documento verrà eliminato definitivamente.",
"searchPlaceholder": "Cerca documenti...",
"types": {
"all": "Tutti",
"text": "Testo",
"context": "Contesto",
"prompt": "Prompt"
},
"editor": {
"titlePlaceholder": "Titolo...",
"contentPlaceholder": "Scrivi il tuo testo in Markdown...",
"preview": "Anteprima",
"edit": "Modifica",
"words": "Parole",
"saving": "Salvataggio...",
"saved": "Salvato",
"unsaved": "Non salvato",
"tags": "Tag",
"addTag": "Aggiungi tag..."
}
},
"settings": {
"title": "Impostazioni"
},
"messages": {
"saved": "Salvato",
"deleted": "Eliminato",
"error": "Si è verificato un errore",
"created": "Creato"
},
"home": {
"page_title_html": "Context - Mana",
"title": "Context",
"subtitle": "Il tuo hub di gestione della conoscenza",
"stat_spaces": "Spaces",
"stat_documents": "Documenti",
"stat_words": "Parole",
"stat_split_label": "Testo/Contesto/Prompt",
"action_spaces": "Spaces",
"action_all_documents": "Tutti i documenti",
"section_pinned": "Spaces fissati",
"section_recent": "Modificati di recente",
"action_show_all": "Mostra tutti",
"badge_pinned": "Fissato",
"empty_title": "Nessun documento",
"empty_hint": "Crea il tuo primo Space e inizia a scrivere.",
"empty_action": "Crea il primo Space",
"confirm_delete_doc": "Eliminare davvero il documento?"
}
}

View file

@ -1,112 +0,0 @@
<!--
Context — Workbench ListView
Spaces and recent documents.
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords, encryptRecord } from '$lib/data/crypto';
import { BaseListView } from '@mana/shared-ui';
import { documentTable } from './collections';
import type { LocalContextSpace, LocalDocument } from './types';
async function handleCreateDocument() {
const id = crypto.randomUUID();
const row: LocalDocument = {
id,
contextSpaceId: null,
title: 'Neues Dokument',
content: '# Neues Dokument\n\n',
type: 'text',
shortId: null,
pinned: false,
metadata: null,
};
await encryptRecord('documents', row);
await documentTable.add(row);
goto(`/context/documents/${id}`);
}
const spacesQuery = useLiveQueryWithDefault(async () => {
const all = await db.table<LocalContextSpace>('contextSpaces').toArray();
return all.filter((s) => !s.deletedAt);
}, [] as LocalContextSpace[]);
const documentsQuery = useLiveQueryWithDefault(async () => {
const all = await db.table<LocalDocument>('documents').toArray();
const visible = all.filter((d) => !d.deletedAt);
return decryptRecords('documents', visible);
}, [] as LocalDocument[]);
const spaces = $derived(spacesQuery.value);
const documents = $derived(documentsQuery.value);
const pinnedSpaces = $derived(spaces.filter((s) => s.pinned));
const recentDocs = $derived(
[...documents].sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')).slice(0, 15)
);
const typeIcons: Record<string, string> = {
text: '&#128196;',
context: '&#128218;',
prompt: '&#9889;',
};
</script>
<BaseListView items={recentDocs} getKey={(d) => d.id} emptyTitle="Keine Dokumente">
{#snippet toolbar()}
<div class="flex items-center justify-between gap-2">
<a
href="/context/documents"
class="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
Alle Dokumente &rarr;
</a>
<button
type="button"
onclick={handleCreateDocument}
class="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
<span aria-hidden="true">+</span>
Neues Dokument
</button>
</div>
{/snippet}
{#snippet header()}
<span>{spaces.length} Spaces</span>
<span>{documents.length} Dokumente</span>
{/snippet}
{#snippet listHeader()}
{#if pinnedSpaces.length > 0}
<h3 class="mb-2 text-xs font-medium text-muted-foreground">Angepinnte Spaces</h3>
{#each pinnedSpaces as space (space.id)}
<div class="mb-1 min-h-[44px] rounded-md px-3 py-2 transition-colors hover:bg-muted/50">
<p class="text-sm font-medium text-foreground">{space.name}</p>
{#if space.description}
<p class="truncate text-xs text-muted-foreground/70">{space.description}</p>
{/if}
</div>
{/each}
{/if}
<h3 class="mb-2 mt-3 text-xs font-medium text-muted-foreground">Zuletzt bearbeitet</h3>
{/snippet}
{#snippet item(doc)}
<a
href="/context/documents/{doc.id}"
class="flex min-h-[44px] items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted/50"
>
<span class="text-sm">{@html typeIcons[doc.type] ?? '&#128196;'}</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm text-foreground/90">{doc.title || 'Unbenannt'}</p>
</div>
{#if doc.pinned}
<span class="text-xs text-muted-foreground/70">&#128204;</span>
{/if}
</a>
{/snippet}
</BaseListView>

View file

@ -1,52 +0,0 @@
/**
* Context module collection accessors and guest seed data.
*
* Uses table names from the unified DB: contextSpaces, documents.
*/
import { db } from '$lib/data/database';
import type { LocalContextSpace, LocalDocument } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const contextSpaceTable = db.table<LocalContextSpace>('contextSpaces');
export const documentTable = db.table<LocalDocument>('documents');
// ─── Guest Seed ────────────────────────────────────────────
const DEMO_SPACE_ID = 'demo-workspace';
export const CONTEXT_GUEST_SEED = {
contextSpaces: [
{
id: DEMO_SPACE_ID,
name: 'Mein Workspace',
description: 'Beispiel-Space zum Kennenlernen von Context.',
pinned: true,
prefix: 'W',
},
],
documents: [
{
id: 'doc-welcome',
contextSpaceId: DEMO_SPACE_ID,
title: 'Willkommen bei Context',
content:
'Context ist dein KI-gestütztes Dokumenten-Management. Erstelle Texte, sammle Kontexte und nutze KI-Prompts.\n\nMelde dich an, um deine Dokumente zu synchronisieren.',
type: 'text' as const,
shortId: 'WD1',
pinned: true,
metadata: { tags: ['einführung'], wordCount: 22 },
},
{
id: 'doc-prompt',
contextSpaceId: DEMO_SPACE_ID,
title: 'Beispiel-Prompt',
content: 'Fasse den folgenden Text in 3 Stichpunkten zusammen:\n\n{text}',
type: 'prompt' as const,
shortId: 'WP1',
pinned: false,
metadata: { tags: ['vorlage'] },
},
],
};

View file

@ -1,14 +0,0 @@
/**
* Context module barrel exports.
*/
export { contextSpaceTable, documentTable, CONTEXT_GUEST_SEED } from './collections';
export * from './queries';
export type {
LocalContextSpace,
LocalDocument,
DocumentType,
DocumentMetadata,
Space,
Document,
} from './types';

View file

@ -1,10 +0,0 @@
import type { ModuleConfig } from '$lib/data/module-registry';
export const contextModuleConfig: ModuleConfig = {
appId: 'context',
tables: [
{ name: 'contextSpaces', syncName: 'spaces' },
{ name: 'documents' },
{ name: 'documentTags' },
],
};

View file

@ -1,158 +0,0 @@
/**
* Reactive Queries & Pure Helpers for Context module.
*
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
* (local writes, sync updates, other tabs). Components call these hooks
* at init time; no manual fetch/refresh needed.
*/
import { useScopedLiveQuery } from '$lib/data/scope/use-scoped-live-query.svelte';
import { db } from '$lib/data/database';
import { scopedForModule } from '$lib/data/scope';
import { decryptRecords } from '$lib/data/crypto';
import type { LocalContextSpace, LocalDocument, Space, Document, DocumentType } from './types';
// ─── Type Converters ──────────────────────────────────────
/** Convert LocalContextSpace (IndexedDB) to shared Space type. */
export function toSpace(local: LocalContextSpace): Space {
return {
id: local.id,
name: local.name,
description: local.description ?? null,
user_id: 'local',
created_at: local.createdAt ?? new Date().toISOString(),
settings: local.settings ?? null,
pinned: local.pinned,
prefix: local.prefix,
};
}
/** Convert LocalDocument (IndexedDB) to shared Document type. */
export function toDocument(local: LocalDocument): Document {
return {
id: local.id,
title: local.title,
content: local.content,
type: local.type,
space_id: local.contextSpaceId ?? null,
user_id: 'local',
created_at: local.createdAt ?? new Date().toISOString(),
updated_at: local.updatedAt ?? new Date().toISOString(),
metadata: local.metadata ?? null,
short_id: local.shortId ?? undefined,
pinned: local.pinned,
};
}
// ─── Live Query Hooks (call during component init) ────────
/** All spaces, sorted by name. Auto-updates on any change. */
export function useAllSpaces() {
return useScopedLiveQuery(async () => {
const locals = await scopedForModule<LocalContextSpace, string>(
'context',
'contextSpaces'
).toArray();
return locals
.filter((s) => !s.deletedAt)
.map(toSpace)
.sort((a, b) => a.name.localeCompare(b.name));
}, [] as Space[]);
}
/** All documents, sorted by updated_at desc. Auto-updates on any change. */
export function useAllDocuments() {
return useScopedLiveQuery(async () => {
const locals = await scopedForModule<LocalDocument, string>('context', 'documents').toArray();
const visible = locals.filter((d) => !d.deletedAt);
const decrypted = await decryptRecords('documents', visible);
return decrypted
.map(toDocument)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
}, [] as Document[]);
}
/** Documents for a specific context-space. Auto-updates on any change. */
export function useSpaceDocuments(contextSpaceId: string) {
return useScopedLiveQuery(async () => {
const locals = await db
.table<LocalDocument>('documents')
.where('contextSpaceId')
.equals(contextSpaceId)
.toArray();
const visible = locals.filter((d) => !d.deletedAt);
const decrypted = await decryptRecords('documents', visible);
return decrypted
.map(toDocument)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
}, [] as Document[]);
}
// ─── Pure Helper Functions (for $derived) ─────────────────
/** Get pinned spaces from a list. */
export function getPinnedSpaces(spaces: Space[]): Space[] {
return spaces.filter((s) => s.pinned);
}
/** Filter documents by type, search query, and tags. */
export function filterDocuments(
documents: Document[],
options: {
typeFilter?: DocumentType | 'all';
searchQuery?: string;
tagFilter?: string[];
}
): Document[] {
let filtered = documents;
if (options.typeFilter && options.typeFilter !== 'all') {
filtered = filtered.filter((d) => d.type === options.typeFilter);
}
if (options.searchQuery?.trim()) {
const q = options.searchQuery.toLowerCase();
filtered = filtered.filter(
(d) => d.title.toLowerCase().includes(q) || d.content?.toLowerCase().includes(q)
);
}
if (options.tagFilter && options.tagFilter.length > 0) {
filtered = filtered.filter((d) =>
options.tagFilter!.some((tag) => d.metadata?.tags?.includes(tag))
);
}
return filtered;
}
/** Compute document stats from a list. */
export function getDocumentStats(documents: Document[]) {
return {
total: documents.length,
text: documents.filter((d) => d.type === 'text').length,
context: documents.filter((d) => d.type === 'context').length,
prompt: documents.filter((d) => d.type === 'prompt').length,
totalWords: documents.reduce((sum, d) => sum + (d.metadata?.word_count || 0), 0),
};
}
/** Get all unique tags from documents. */
export function getAllDocumentTags(documents: Document[]): string[] {
const tags = new Set<string>();
documents.forEach((d) => {
d.metadata?.tags?.forEach((t) => tags.add(t));
});
return Array.from(tags).sort();
}
/** Find a space by ID. */
export function findSpaceById(spaces: Space[], id: string): Space | undefined {
return spaces.find((s) => s.id === id);
}
/** Find a document by ID. */
export function findDocumentById(documents: Document[], id: string): Document | undefined {
return documents.find((d) => d.id === id);
}

View file

@ -1,19 +0,0 @@
/**
* Ucontext Tags Uses shared global tags + module-specific junction table.
*/
import { db } from '$lib/data/database';
import { createTagLinkOps } from '@mana/shared-stores';
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
} from '@mana/shared-stores';
export const documentTagOps = createTagLinkOps({
table: () => db.table('documentTags'),
entityIdField: 'documentId',
});

View file

@ -1,79 +0,0 @@
/**
* Context module types for the unified Mana app.
*/
import type { BaseRecord } from '@mana/local-store';
// ─── Document Types ────────────────────────────────────────
export type DocumentType = 'text' | 'context' | 'prompt';
export interface DocumentMetadata {
tags?: string[];
word_count?: number;
token_count?: number;
parent_document?: string;
version?: number;
generation_type?: 'summary' | 'continuation' | 'rewrite' | 'ideas';
model_used?: string;
prompt_used?: string;
original_title?: string;
version_history?: Array<{
id: string;
title: string;
type: string;
created_at: string;
is_original: boolean;
}>;
[key: string]: unknown;
}
// ─── Local DB Types (IndexedDB) ────────────────────────────
export interface LocalContextSpace extends BaseRecord {
name: string;
description?: string | null;
settings?: Record<string, unknown> | null;
pinned: boolean;
prefix: string;
}
export interface LocalDocument extends BaseRecord {
contextSpaceId?: string | null;
title: string;
content: string;
type: DocumentType;
shortId?: string | null;
pinned: boolean;
metadata?: {
tags?: string[];
wordCount?: number;
} | null;
}
// ─── Shared / View Types ───────────────────────────────────
export interface Space {
id: string;
name: string;
description: string | null;
user_id: string;
created_at: string;
settings: Record<string, unknown> | null;
pinned: boolean;
prefix?: string;
}
export interface Document {
id: string;
title: string;
content: string | null;
type: DocumentType;
space_id: string | null;
user_id: string;
created_at: string;
updated_at: string;
metadata: DocumentMetadata | null;
short_id?: string;
pinned?: boolean;
}

View file

@ -1,205 +0,0 @@
<script lang="ts">
import { formatDate } from '$lib/i18n/format';
import { Folder, FileText, Plus } from '@mana/shared-icons';
import {
useAllSpaces,
useAllDocuments,
getPinnedSpaces,
getDocumentStats,
} from '$lib/modules/context/queries';
import { documentTable } from '$lib/modules/context/collections';
import { RoutePage } from '$lib/components/shell';
import { _ } from 'svelte-i18n';
const allSpaces = useAllSpaces();
const allDocuments = useAllDocuments();
const spaces = $derived(allSpaces.value);
const documents = $derived(allDocuments.value);
const pinnedSpaces = $derived(getPinnedSpaces(spaces));
const stats = $derived(getDocumentStats(documents));
const recentDocs = $derived(documents.slice(0, 6));
async function handleDeleteDoc(id: string) {
if (!confirm($_('context.home.confirm_delete_doc'))) return;
await documentTable.delete(id);
}
async function handleTogglePinDoc(id: string) {
const doc = documents.find((d) => d.id === id);
if (doc) {
await documentTable.update(id, { pinned: !doc.pinned });
}
}
</script>
<svelte:head>
<title>{$_('context.home.page_title_html')}</title>
</svelte:head>
<RoutePage appId="context">
<div class="mx-auto max-w-5xl">
<header class="mb-8">
<h1 class="text-2xl font-bold">{$_('context.home.title')}</h1>
<p class="mt-1 text-sm opacity-60">{$_('context.home.subtitle')}</p>
</header>
<!-- Stats -->
<div class="mb-8 grid grid-cols-2 gap-4 md:grid-cols-4">
<div
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">{$_('context.home.stat_spaces')}</div>
</div>
<div
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">{$_('context.home.stat_documents')}</div>
</div>
<div
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">{$_('context.home.stat_words')}</div>
</div>
<div
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">{$_('context.home.stat_split_label')}</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mb-8 flex gap-3">
<a
href="/context/spaces"
class="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
>
<Folder size={16} />
{$_('context.home.action_spaces')}
</a>
<a
href="/context/documents"
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} />
{$_('context.home.action_all_documents')}
</a>
</div>
<!-- Pinned Spaces -->
{#if pinnedSpaces.length > 0}
<section class="mb-8">
<h2 class="mb-4 text-lg font-semibold">{$_('context.home.section_pinned')}</h2>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{#each pinnedSpaces as space}
<a
href="/context/spaces/{space.id}"
class="rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-center gap-3">
<span
class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-100 text-lg font-bold text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300"
>
{space.prefix || space.name[0]?.toUpperCase() || 'S'}
</span>
<div>
<h3 class="font-semibold">{space.name}</h3>
{#if space.description}
<p class="text-xs opacity-60 line-clamp-1">{space.description}</p>
{/if}
</div>
</div>
</a>
{/each}
</div>
</section>
{/if}
<!-- Recent Documents -->
{#if recentDocs.length > 0}
<section>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold">{$_('context.home.section_recent')}</h2>
<a href="/context/documents" class="text-sm text-indigo-600 hover:underline"
>{$_('context.home.action_show_all')}</a
>
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{#each recentDocs as doc}
<a
href="/context/documents/{doc.id}"
class="group rounded-xl border border-border-strong bg-white p-4 transition-colors hover:shadow-md dark:border-border dark:bg-card"
>
<div class="flex items-start justify-between">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span
class="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase {doc.type ===
'text'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: doc.type === 'context'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300'}"
>
{doc.type}
</span>
{#if doc.pinned}
<span class="text-xs opacity-40">{$_('context.home.badge_pinned')}</span>
{/if}
</div>
<h3 class="mt-1 truncate font-semibold">{doc.title}</h3>
{#if doc.content}
<p class="mt-0.5 truncate text-xs opacity-50">
{doc.content.slice(0, 100)}
</p>
{/if}
</div>
<button
onclick={(e) => {
e.preventDefault();
handleTogglePinDoc(doc.id);
}}
class="ml-2 rounded p-1 opacity-0 transition-opacity hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
title={doc.pinned ? $_('context.common.unpin') : $_('context.common.pin')}
>
{doc.pinned ? '&#9733;' : '&#9734;'}
</button>
</div>
<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-muted px-1.5 py-0.5 dark:bg-muted">{tag}</span>
{/each}
{/if}
<span class="ml-auto">
{formatDate(new Date(doc.updated_at))}
</span>
</div>
</a>
{/each}
</div>
</section>
{:else}
<div
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">{$_('context.home.empty_title')}</h3>
<p class="mt-1 text-sm opacity-40">
{$_('context.home.empty_hint')}
</p>
<a
href="/context/spaces"
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
>
<Plus size={16} />
{$_('context.home.empty_action')}
</a>
</div>
{/if}
</div>
</RoutePage>

View file

@ -1,279 +0,0 @@
<script lang="ts">
import { formatDate } from '$lib/i18n/format';
import { goto } from '$app/navigation';
import { Plus, MagnifyingGlass, FileText } from '@mana/shared-icons';
import {
useAllDocuments,
filterDocuments,
getDocumentStats,
getAllDocumentTags,
} from '$lib/modules/context/queries';
import { documentTable } from '$lib/modules/context/collections';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
import { RoutePage } from '$lib/components/shell';
let searchQuery = $state('');
let typeFilter = $state<DocumentType | 'all'>('all');
let tagFilter = $state<string[]>([]);
let deleteTarget = $state<string | null>(null);
const allDocuments = useAllDocuments();
const documents = $derived(allDocuments.value);
const stats = $derived(getDocumentStats(documents));
const allTags = $derived(getAllDocumentTags(documents));
const filteredDocuments = $derived(
filterDocuments(documents, { typeFilter, searchQuery, tagFilter })
);
const typeFilters: { value: DocumentType | 'all'; label: string }[] = [
{ value: 'all', label: 'Alle' },
{ value: 'text', label: 'Text' },
{ value: 'context', label: 'Kontext' },
{ value: 'prompt', label: 'Prompt' },
];
async function handleCreateDocument() {
const id = crypto.randomUUID();
const row: LocalDocument = {
id,
contextSpaceId: null,
title: 'Neues Dokument',
content: '# Neues Dokument\n\n',
type: 'text',
shortId: null,
pinned: false,
metadata: null,
};
await encryptRecord('documents', row);
await documentTable.add(row);
goto(`/context/documents/${id}`);
}
async function handleDeleteDoc(id: string) {
await documentTable.delete(id);
deleteTarget = null;
}
async function handleTogglePin(id: string) {
const doc = documents.find((d) => d.id === id);
if (doc) {
await documentTable.update(id, { pinned: !doc.pinned });
}
}
function toggleTag(tag: string) {
if (tagFilter.includes(tag)) {
tagFilter = tagFilter.filter((t) => t !== tag);
} else {
tagFilter = [...tagFilter, tag];
}
}
function resetFilters() {
searchQuery = '';
typeFilter = 'all';
tagFilter = [];
}
</script>
<svelte:head>
<title>Dokumente - Context - Mana</title>
</svelte:head>
<RoutePage appId="context" backHref="/context">
<div class="mx-auto max-w-4xl">
<div class="mb-6 flex items-center justify-between">
<div>
<div class="flex items-center gap-3">
<a href="/context" class="text-sm opacity-60 hover:opacity-100">&larr; Context</a>
<h1 class="text-2xl font-bold">Dokumente</h1>
</div>
<p class="mt-1 text-sm opacity-60">
{stats.total} Dokumente, {stats.totalWords.toLocaleString()} Woerter
</p>
</div>
<button
class="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
onclick={handleCreateDocument}
>
<Plus size={16} />
Neues Dokument
</button>
</div>
<!-- Filters -->
<div class="mb-6 flex items-center gap-4">
<div class="flex gap-2">
{#each typeFilters as filter}
<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-muted dark:hover:bg-muted'}"
onclick={() => (typeFilter = filter.value)}
>
{filter.label}
{#if filter.value === 'all'}
<span class="ml-1 opacity-60">{stats.total}</span>
{:else if filter.value === 'text'}
<span class="ml-1 opacity-60">{stats.text}</span>
{:else if filter.value === 'context'}
<span class="ml-1 opacity-60">{stats.context}</span>
{:else if filter.value === 'prompt'}
<span class="ml-1 opacity-60">{stats.prompt}</span>
{/if}
</button>
{/each}
</div>
<div class="relative ml-auto max-w-xs flex-1">
<MagnifyingGlass size={14} class="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-40" />
<input
type="text"
bind:value={searchQuery}
placeholder="Dokumente durchsuchen..."
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>
<!-- Tags filter -->
{#if allTags.length > 0}
<div class="mb-4 flex flex-wrap gap-2">
{#each allTags as tag}
<button
class="rounded-full px-2 py-1 text-xs transition-colors {tagFilter.includes(tag)
? 'bg-indigo-600 text-white'
: 'bg-muted opacity-60 hover:opacity-100 dark:bg-muted'}"
onclick={() => toggleTag(tag)}
>
{tag}
</button>
{/each}
</div>
{/if}
<!-- Document list -->
{#if filteredDocuments.length > 0}
<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-border-strong bg-white p-4 transition-colors 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">
<div class="flex items-center gap-2">
<span
class="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase {doc.type ===
'text'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: doc.type === 'context'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300'}"
>
{doc.type}
</span>
{#if doc.pinned}
<span class="text-xs opacity-40">Angeheftet</span>
{/if}
</div>
<h3 class="mt-1 truncate font-semibold">{doc.title}</h3>
{#if doc.content}
<p class="mt-0.5 truncate text-xs opacity-50">
{doc.content.slice(0, 100)}
</p>
{/if}
</a>
<div
class="ml-2 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"
>
<button
onclick={() => handleTogglePin(doc.id)}
class="rounded p-1 hover:bg-muted dark:hover:bg-muted"
title={doc.pinned ? 'Loslassen' : 'Anheften'}
>
{doc.pinned ? '&#9733;' : '&#9734;'}
</button>
<button
onclick={() => (deleteTarget = doc.id)}
class="rounded p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
title="Loeschen"
>
&times;
</button>
</div>
</div>
<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-muted px-1.5 py-0.5 dark:bg-muted">{tag}</span>
{/each}
{/if}
<span class="ml-auto">
{formatDate(new Date(doc.updated_at))}
</span>
</div>
</div>
{/each}
</div>
{:else if searchQuery || typeFilter !== 'all' || tagFilter.length > 0}
<div class="py-12 text-center">
<p class="opacity-60">Keine Dokumente gefunden</p>
<button class="mt-2 text-sm text-indigo-600 hover:underline" onclick={resetFilters}>
Filter zuruecksetzen
</button>
</div>
{:else}
<div
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>
<p class="mt-1 max-w-md text-sm opacity-40">
Dokumente enthalten dein Wissen, Kontext-Referenzen und AI-Prompts.
</p>
<button
class="mt-4 flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
onclick={handleCreateDocument}
>
<Plus size={16} />
Erstes Dokument erstellen
</button>
</div>
{/if}
</div>
<!-- Delete Confirmation -->
{#if deleteTarget}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={() => (deleteTarget = null)}
onkeydown={(e) => e.key === 'Escape' && (deleteTarget = null)}
tabindex="-1"
role="presentation"
>
<div
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
onclick={(e) => e.stopPropagation()}
role="none"
>
<h3 class="text-lg font-semibold">Dokument loeschen?</h3>
<p class="mt-2 text-sm opacity-60">Das Dokument wird unwiderruflich geloescht.</p>
<div class="mt-4 flex justify-end gap-2">
<button
onclick={() => (deleteTarget = null)}
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>
<button
onclick={() => deleteTarget && handleDeleteDoc(deleteTarget)}
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
Loeschen
</button>
</div>
</div>
</div>
{/if}
</RoutePage>

View file

@ -1,246 +0,0 @@
<script lang="ts">
import { formatDate } from '$lib/i18n/format';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ArrowLeft, Trash } from '@mana/shared-icons';
import { useAllDocuments, findDocumentById } from '$lib/modules/context/queries';
import { documentTable } from '$lib/modules/context/collections';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType } from '$lib/modules/context/types';
import { RoutePage } from '$lib/components/shell';
let showDeleteConfirm = $state(false);
let saving = $state(false);
let docId = $derived($page.params.id || '');
const allDocuments = useAllDocuments();
const doc = $derived(findDocumentById(allDocuments.value, docId) ?? null);
// Local editing state
let editTitle = $state('');
let editContent = $state('');
let editType = $state<DocumentType>('text');
let editTags = $state('');
let initialized = $state(false);
// Initialize edit state from document
$effect(() => {
if (doc && !initialized) {
editTitle = doc.title;
editContent = doc.content || '';
editType = doc.type;
editTags = doc.metadata?.tags?.join(', ') || '';
initialized = true;
}
});
// Reset when navigating to a different document
$effect(() => {
if (docId) {
initialized = false;
}
});
async function handleSave() {
if (!doc) return;
saving = true;
const tags = editTags
.split(',')
.map((t) => t.trim())
.filter(Boolean);
const wordCount = editContent.split(/\s+/).filter(Boolean).length;
const diff: Record<string, unknown> = {
title: editTitle,
content: editContent,
type: editType,
metadata: {
...(doc.metadata || {}),
tags,
word_count: wordCount,
},
};
await encryptRecord('documents', diff);
await documentTable.update(docId, diff);
saving = false;
}
async function handleDelete() {
await documentTable.delete(docId);
if (doc?.space_id) {
goto(`/context/spaces/${doc.space_id}`);
} else {
goto('/context/documents');
}
}
// Auto-save on content change (debounced)
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
function scheduleAutoSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(handleSave, 1500);
}
const typeOptions: { value: DocumentType; label: string }[] = [
{ value: 'text', label: 'Text' },
{ value: 'context', label: 'Kontext' },
{ value: 'prompt', label: 'Prompt' },
];
</script>
<svelte:head>
<title>{doc?.title || 'Dokument'} - Context - Mana</title>
</svelte:head>
<RoutePage appId="context" backHref="/context/documents" title="Dokument">
<div class="mx-auto max-w-4xl pb-24">
{#if !doc}
<div class="py-12 text-center opacity-60">Lade Dokument...</div>
{:else}
<!-- Breadcrumb -->
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2 text-sm">
{#if doc.space_id}
<a
href="/context/spaces/{doc.space_id}"
class="flex items-center gap-1 opacity-60 hover:opacity-100"
>
<ArrowLeft size={14} />
Zurueck zum Space
</a>
{:else}
<a
href="/context/documents"
class="flex items-center gap-1 opacity-60 hover:opacity-100"
>
<ArrowLeft size={14} />
Alle Dokumente
</a>
{/if}
</div>
<div class="flex items-center gap-2">
{#if saving}
<span class="text-xs opacity-40">Speichert...</span>
{/if}
<button
onclick={handleSave}
class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
>
Speichern
</button>
<button
class="rounded-lg p-2 opacity-60 transition-colors hover:bg-red-50 hover:text-red-600 hover:opacity-100 dark:hover:bg-red-900/20"
onclick={() => (showDeleteConfirm = true)}
title="Dokument loeschen"
>
<Trash size={18} />
</button>
</div>
</div>
<!-- Editor -->
<div
class="rounded-xl border border-border-strong bg-white p-6 dark:border-border dark:bg-card"
>
<!-- Title -->
<input
type="text"
bind:value={editTitle}
oninput={scheduleAutoSave}
placeholder="Dokumenttitel"
class="mb-4 w-full border-none bg-transparent text-2xl font-bold outline-none placeholder:opacity-30"
/>
<!-- Type + Tags bar -->
<div
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}
<button
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors {editType ===
opt.value
? 'bg-indigo-600 text-white'
: 'bg-muted opacity-60 hover:opacity-100 dark:bg-muted'}"
onclick={() => {
editType = opt.value;
scheduleAutoSave();
}}
>
{opt.label}
</button>
{/each}
</div>
<div class="h-4 w-px bg-muted dark:bg-muted"></div>
<input
type="text"
bind:value={editTags}
oninput={scheduleAutoSave}
placeholder="Tags (komma-getrennt)"
class="flex-1 border-none bg-transparent text-sm outline-none placeholder:opacity-30"
/>
</div>
<!-- Content -->
<textarea
bind:value={editContent}
oninput={scheduleAutoSave}
rows="20"
placeholder="Schreibe hier..."
class="w-full resize-none border-none bg-transparent font-mono text-sm leading-relaxed outline-none placeholder:opacity-30"
></textarea>
</div>
<!-- Document metadata -->
<div class="mt-4 flex items-center gap-4 text-xs opacity-40">
{#if doc.short_id}
<span>ID: {doc.short_id}</span>
{/if}
{#if doc.metadata?.word_count}
<span>{doc.metadata.word_count} Woerter</span>
{/if}
<span>
Erstellt: {formatDate(new Date(doc.created_at))}
</span>
<span>
Aktualisiert: {formatDate(new Date(doc.updated_at))}
</span>
</div>
{/if}
</div>
<!-- Delete Confirmation -->
{#if showDeleteConfirm}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={() => (showDeleteConfirm = false)}
onkeydown={(e) => e.key === 'Escape' && (showDeleteConfirm = false)}
tabindex="-1"
role="presentation"
>
<div
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
onclick={(e) => e.stopPropagation()}
role="none"
>
<h3 class="text-lg font-semibold">Dokument loeschen?</h3>
<p class="mt-2 text-sm opacity-60">Das Dokument wird unwiderruflich geloescht.</p>
<div class="mt-4 flex justify-end gap-2">
<button
onclick={() => (showDeleteConfirm = false)}
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>
<button
onclick={handleDelete}
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
Loeschen
</button>
</div>
</div>
</div>
{/if}
</RoutePage>

View file

@ -1,262 +0,0 @@
<script lang="ts">
import { formatDate } from '$lib/i18n/format';
import { Plus, MagnifyingGlass } from '@mana/shared-icons';
import { useAllSpaces } from '$lib/modules/context/queries';
import { contextSpaceTable } from '$lib/modules/context/collections';
import type { LocalContextSpace } from '$lib/modules/context/types';
import { RoutePage } from '$lib/components/shell';
let searchQuery = $state('');
let showCreateForm = $state(false);
let newName = $state('');
let newDescription = $state('');
let newPrefix = $state('');
let deleteTarget = $state<string | null>(null);
const allSpaces = useAllSpaces();
const spaces = $derived(allSpaces.value);
const filteredSpaces = $derived(
searchQuery.trim()
? spaces.filter(
(s) =>
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
: spaces
);
async function handleCreate() {
if (!newName.trim()) return;
const prefix = newPrefix.trim() || newName.trim()[0]?.toUpperCase() || 'S';
await contextSpaceTable.add({
id: crypto.randomUUID(),
name: newName.trim(),
description: newDescription.trim() || null,
pinned: false,
prefix,
} satisfies LocalContextSpace);
newName = '';
newDescription = '';
newPrefix = '';
showCreateForm = false;
}
async function handleTogglePin(id: string) {
const space = spaces.find((s) => s.id === id);
if (space) {
await contextSpaceTable.update(id, { pinned: !space.pinned });
}
}
async function handleDelete(id: string) {
await contextSpaceTable.delete(id);
deleteTarget = null;
}
const inputClass =
'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>
<title>Spaces - Context - Mana</title>
</svelte:head>
<RoutePage appId="context" backHref="/context">
<div class="mx-auto max-w-4xl">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/context" class="text-sm opacity-60 hover:opacity-100">&larr; Context</a>
<h1 class="text-2xl font-bold">Spaces</h1>
</div>
<button
class="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
onclick={() => (showCreateForm = !showCreateForm)}
>
<Plus size={16} />
Neuer Space
</button>
</div>
<!-- Create Form -->
{#if showCreateForm}
<div
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">
<div>
<label for="space-name" class="mb-1 block text-sm font-medium">Name</label>
<input
id="space-name"
type="text"
bind:value={newName}
placeholder="Mein Workspace"
class={inputClass}
onkeydown={(e) => e.key === 'Enter' && handleCreate()}
/>
</div>
<div>
<label for="space-desc" class="mb-1 block text-sm font-medium"
>Beschreibung (optional)</label
>
<textarea
id="space-desc"
bind:value={newDescription}
rows="2"
placeholder="Worum geht es in diesem Space?"
class="{inputClass} resize-none"
></textarea>
</div>
<div>
<label for="space-prefix" class="mb-1 block text-sm font-medium"
>Prefix (optional)</label
>
<input
id="space-prefix"
type="text"
bind:value={newPrefix}
placeholder="W"
maxlength="3"
class="{inputClass} max-w-[100px]"
/>
</div>
<div class="flex justify-end gap-2">
<button
onclick={() => (showCreateForm = false)}
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>
<button
onclick={handleCreate}
disabled={!newName.trim()}
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
>
Erstellen
</button>
</div>
</div>
</div>
{/if}
<!-- Search -->
<div class="relative mb-6">
<MagnifyingGlass size={16} class="absolute left-3 top-1/2 -translate-y-1/2 opacity-40" />
<input
type="text"
bind:value={searchQuery}
placeholder="Spaces durchsuchen..."
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>
{#if filteredSpaces.length > 0}
<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-border-strong bg-white p-4 transition-colors 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">
<div class="flex items-center gap-3">
<span
class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-100 text-lg font-bold text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300"
>
{space.prefix || space.name[0]?.toUpperCase() || 'S'}
</span>
<div>
<h3 class="font-semibold">{space.name}</h3>
{#if space.description}
<p class="text-xs opacity-60 line-clamp-2">{space.description}</p>
{/if}
</div>
</div>
</a>
<div
class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"
>
<button
onclick={() => handleTogglePin(space.id)}
class="rounded p-1.5 hover:bg-muted dark:hover:bg-muted"
title={space.pinned ? 'Loslassen' : 'Anheften'}
>
{space.pinned ? '&#9733;' : '&#9734;'}
</button>
<button
onclick={() => (deleteTarget = space.id)}
class="rounded p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
title="Loeschen"
>
&times;
</button>
</div>
</div>
<div class="mt-3 text-xs opacity-40">
Erstellt: {formatDate(new Date(space.created_at))}
</div>
</div>
{/each}
</div>
{:else if searchQuery}
<div class="py-12 text-center">
<p class="opacity-60">Keine Spaces gefunden fuer "{searchQuery}"</p>
</div>
{:else}
<div
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>
<p class="mt-1 max-w-md text-sm opacity-40">
Spaces helfen dir, dein Wissen zu organisieren. Erstelle deinen ersten Space, um
loszulegen.
</p>
<button
class="mt-4 flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
onclick={() => (showCreateForm = true)}
>
<Plus size={16} />
Ersten Space erstellen
</button>
</div>
{/if}
</div>
<!-- Delete Confirmation -->
{#if deleteTarget}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={() => (deleteTarget = null)}
onkeydown={(e) => e.key === 'Escape' && (deleteTarget = null)}
tabindex="-1"
role="presentation"
>
<div
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl dark:bg-card"
onclick={(e) => e.stopPropagation()}
role="none"
>
<h3 class="text-lg font-semibold">Space loeschen?</h3>
<p class="mt-2 text-sm opacity-60">
Alle Dokumente in diesem Space werden ebenfalls geloescht. Diese Aktion kann nicht
rueckgaengig gemacht werden.
</p>
<div class="mt-4 flex justify-end gap-2">
<button
onclick={() => (deleteTarget = null)}
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>
<button
onclick={() => deleteTarget && handleDelete(deleteTarget)}
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
Loeschen
</button>
</div>
</div>
</div>
{/if}
</RoutePage>

View file

@ -1,285 +0,0 @@
<script lang="ts">
import { formatDate } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Plus, ArrowLeft, PencilSimple, Check, X, MagnifyingGlass } from '@mana/shared-icons';
import {
useAllSpaces,
useSpaceDocuments,
filterDocuments,
getDocumentStats,
findSpaceById,
} from '$lib/modules/context/queries';
import { contextSpaceTable, documentTable } from '$lib/modules/context/collections';
import { encryptRecord } from '$lib/data/crypto';
import type { DocumentType, LocalDocument } from '$lib/modules/context/types';
import { RoutePage } from '$lib/components/shell';
let editingName = $state(false);
let editName = $state('');
let editDescription = $state('');
let deleteTarget = $state<string | null>(null);
let searchQuery = $state('');
let typeFilter = $state<DocumentType | 'all'>('all');
let spaceId = $derived($page.params.id || '');
const allSpaces = useAllSpaces();
const spaceDocs = $derived(useSpaceDocuments(spaceId));
const space = $derived(findSpaceById(allSpaces.value, spaceId) ?? null);
const documents = $derived(spaceDocs.value);
const stats = $derived(getDocumentStats(documents));
const filteredDocuments = $derived(filterDocuments(documents, { typeFilter, searchQuery }));
$effect(() => {
if (space && !editingName) {
editName = space.name;
editDescription = space.description || '';
}
});
async function handleCreateDocument() {
const id = crypto.randomUUID();
const row: LocalDocument = {
id,
contextSpaceId: spaceId,
title: 'Neues Dokument',
content: '# Neues Dokument\n\n',
type: 'text',
shortId: null,
pinned: false,
metadata: null,
};
await encryptRecord('documents', row);
await documentTable.add(row);
goto(`/context/documents/${id}`);
}
function startEdit() {
editingName = true;
editName = space?.name || '';
editDescription = space?.description || '';
}
async function saveEdit() {
if (!space) return;
await contextSpaceTable.update(space.id, {
name: editName,
description: editDescription || null,
});
editingName = false;
}
function cancelEdit() {
editingName = false;
editName = space?.name || '';
editDescription = space?.description || '';
}
async function handleDeleteDoc(id: string) {
if (!confirm('Dokument wirklich loeschen?')) return;
await documentTable.delete(id);
}
async function handleTogglePinDoc(id: string) {
const doc = documents.find((d) => d.id === id);
if (doc) {
await documentTable.update(id, { pinned: !doc.pinned });
}
}
const typeFilters: { value: DocumentType | 'all'; label: string }[] = [
{ value: 'all', label: 'Alle' },
{ value: 'text', label: 'Text' },
{ value: 'context', label: 'Kontext' },
{ value: 'prompt', label: 'Prompt' },
];
</script>
<svelte:head>
<title>{space?.name || 'Space'} - Context - Mana</title>
</svelte:head>
<RoutePage appId="context" backHref="/context/spaces" title="Space">
<div class="mx-auto max-w-4xl">
<!-- Breadcrumb -->
<div class="mb-4 flex items-center gap-2 text-sm">
<a href="/context/spaces" class="flex items-center gap-1 opacity-60 hover:opacity-100">
<ArrowLeft size={14} />
Spaces
</a>
<span class="opacity-40">/</span>
<span class="font-medium">{space?.name || '...'}</span>
</div>
{#if !space}
<div class="py-12 text-center opacity-60">Lade...</div>
{:else}
<!-- Space Header -->
<div
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-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-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
class="flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
onclick={saveEdit}
>
<Check size={14} /> Speichern
</button>
<button
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
</button>
</div>
</div>
{:else}
<div class="flex items-start justify-between">
<div>
<h1 class="text-xl font-bold">{space.name}</h1>
{#if space.description}
<p class="mt-1 text-sm opacity-60">{space.description}</p>
{/if}
<div class="mt-3 flex gap-4 text-xs opacity-50">
<span>{stats.total} Dokumente</span>
<span>{stats.totalWords.toLocaleString()} Woerter</span>
</div>
</div>
<button
class="rounded-lg p-2 opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
onclick={startEdit}
title={$_('common.edit')}
>
<PencilSimple size={18} />
</button>
</div>
{/if}
</div>
<!-- Toolbar -->
<div class="mb-4 flex items-center justify-between gap-4">
<div class="flex gap-2">
{#each typeFilters as filter}
<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-muted dark:hover:bg-muted'}"
onclick={() => (typeFilter = filter.value)}
>
{filter.label}
</button>
{/each}
</div>
<div class="flex items-center gap-2">
<div class="relative">
<MagnifyingGlass
size={14}
class="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-40"
/>
<input
type="text"
bind:value={searchQuery}
placeholder={$_('common.search')}
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
class="flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
onclick={handleCreateDocument}
>
<Plus size={14} />
Neues Dokument
</button>
</div>
</div>
<!-- Documents -->
{#if filteredDocuments.length > 0}
<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-border-strong bg-white p-4 transition-colors 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">
<div class="flex items-center gap-2">
<span
class="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase {doc.type ===
'text'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: doc.type === 'context'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300'}"
>
{doc.type}
</span>
{#if doc.pinned}
<span class="text-xs opacity-40">Angeheftet</span>
{/if}
</div>
<h3 class="mt-1 truncate font-semibold">{doc.title}</h3>
{#if doc.content}
<p class="mt-0.5 truncate text-xs opacity-50">
{doc.content.slice(0, 100)}
</p>
{/if}
</a>
<div
class="ml-2 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"
>
<button
onclick={() => handleTogglePinDoc(doc.id)}
class="rounded p-1 hover:bg-muted dark:hover:bg-muted"
title={doc.pinned ? 'Loslassen' : 'Anheften'}
>
{doc.pinned ? '&#9733;' : '&#9734;'}
</button>
<button
onclick={() => handleDeleteDoc(doc.id)}
class="rounded p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
title="Loeschen"
>
&times;
</button>
</div>
</div>
<div class="mt-2 text-xs opacity-40">
{formatDate(new Date(doc.updated_at))}
</div>
</div>
{/each}
</div>
{:else}
<div
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
class="mt-4 flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 mx-auto"
onclick={handleCreateDocument}
>
<Plus size={16} />
Erstes Dokument erstellen
</button>
</div>
{/if}
{/if}
</div>
</RoutePage>

View file

@ -1,13 +0,0 @@
<script lang="ts">
import AppLogo from '../AppLogo.svelte';
interface Props {
size?: number;
color?: string;
class?: string;
}
let { size = 55, color, class: className = '' }: Props = $props();
</script>
<AppLogo app="context" {size} {color} class={className} />