mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
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:
parent
5c8faae4ea
commit
a295894ca6
19 changed files with 0 additions and 2241 deletions
|
|
@ -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>
|
||||
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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: '📄',
|
||||
context: '📚',
|
||||
prompt: '⚡',
|
||||
};
|
||||
</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 →
|
||||
</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] ?? '📄'}</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">📌</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
</BaseListView>
|
||||
|
|
@ -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'] },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
@ -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' },
|
||||
],
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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',
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 ? '★' : '☆'}
|
||||
</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>
|
||||
|
|
@ -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">← 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 ? '★' : '☆'}
|
||||
</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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">← 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 ? '★' : '☆'}
|
||||
</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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
|
|
@ -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 ? '★' : '☆'}
|
||||
</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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
|
|
@ -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} />
|
||||
Loading…
Add table
Add a link
Reference in a new issue