mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(writing): smarter empty-state + help-content + de-emphasized Stile link
Three workbench-card UX fixes for the Schreiben module.
Smart empty-state (replaces the small "Noch keine Drafts" line):
- Hero block with icon + 1-line pitch + meta-line ("✨ 12 Textarten · 9
Stile · 7 Quellen-Typen · End-to-End-verschlüsselt"). Sells the
module's surface area before the user has anything saved.
- Six quick-start tiles (Blog · Essay · E-Mail · Social · Brief · Rede)
in a 3-col grid. Tile click → BriefingForm opens with that kind
pre-selected, skipping the Textart-dropdown for the most common cases.
Full 12-kind picker remains one click away inside the form.
- Search + KindTabs + status-filter + favorites-toggle now hide when
drafts.length === 0 — filtering nothing was visual noise. They reappear
the moment the first draft lands.
De-emphasized Stile link:
- Was a labeled "🎨 Stile" pill competing with "+ Neuer Draft" for
attention. Now a compact icon-only ghost button (just 🎨) with an
aria-label so screen readers still get it. Frees the action bar so
the primary CTA stands alone.
Module help (renders behind the ?-icon in the Workbench shell):
- New `writing` entry in MODULE_HELP with description + 9 features
(kinds / styles / references / refinement / versioning / visibility /
export / persona-linkage / token cost) + 5 tips covering keyboard
shortcuts, ✨-button, the Quellen-flow, drag-source, and custom
styles. Uses the existing auto-attach via app-registry's onMissing-
help fallback — no AppPage changes needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
95bedf4625
commit
21dbce6631
2 changed files with 223 additions and 49 deletions
|
|
@ -612,6 +612,28 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
|
||||||
'Lege deinen Heimatort als Standard fest',
|
'Lege deinen Heimatort als Standard fest',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
writing: {
|
||||||
|
description:
|
||||||
|
'KI-Ghostwriter für intentional produzierten Prosa-Text. Brief Thema, Stil und Quellen — ein fertiger Entwurf entsteht, den du iterativ verfeinerst.',
|
||||||
|
features: [
|
||||||
|
'12 Textarten: Blog, Essay, E-Mail, Social, Story, Brief, Rede, Bewerbung, Pressetext, Bio, …',
|
||||||
|
'9 eingebaute Schreibstile (Akademisch, Casual Blog, LinkedIn, Hemingway, Memoir, …) plus eigene Stile',
|
||||||
|
'Quellen verknüpfen aus 7 Modulen: Artikel, Notiz, Library, Kontext, Ziel, Bild, URL',
|
||||||
|
'Selection-Verfeinerung: Markiere Text → Kürzen / Erweitern / Ton ändern / Umschreiben / Übersetzen',
|
||||||
|
'Versionierung mit "Als Checkpoint speichern" + Wiederherstellen',
|
||||||
|
'Visibility: privat / Space / Unlisted-Link / öffentlich',
|
||||||
|
'Export: Markdown, PDF, "Als Artikel speichern"',
|
||||||
|
'Persona-Linkage: Agents pinnen einen Default-Stil',
|
||||||
|
'Token-Cost pro Generation in der Versionshistorie',
|
||||||
|
],
|
||||||
|
tips: [
|
||||||
|
'⌘G generiert · ⌘⇧S speichert Checkpoint · ⌘Z macht letzte Verfeinerung rückgängig',
|
||||||
|
'Klicke ✨ neben dem Titel-Feld — die KI schlägt einen Titel aus deinem Briefing vor',
|
||||||
|
'In der BriefingForm: "Quellen" → 7 Buttons. Nutze "Kontext" um den Space-Kontext-Doc anzuhängen.',
|
||||||
|
'Drafts sind ziehbar — auf andere Module droppen, sobald deren Drop-Targets ausgebaut sind',
|
||||||
|
'Eigene Stile unter "🎨 Stile" anlegen — die Beschreibung wird wörtlich an die KI übergeben',
|
||||||
|
],
|
||||||
|
},
|
||||||
library: {
|
library: {
|
||||||
description:
|
description:
|
||||||
'Medien-Log — Bücher, Filme, Serien und Comics tracken. Status, Fortschritt, Bewertung.',
|
'Medien-Log — Bücher, Filme, Serien und Comics tracken. Status, Fortschritt, Bewertung.',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<!--
|
<!--
|
||||||
Writing — ListView.
|
Writing — ListView.
|
||||||
Grid of drafts with KindTabs + status chips + search + "+ Neu" inline-create.
|
|
||||||
|
Two states:
|
||||||
|
- Has drafts → search + KindTabs + status filter + grid (the "I'm working" view)
|
||||||
|
- Empty → hero pitch + 6 quick-start kind tiles (the "What is this?" view)
|
||||||
|
|
||||||
Clicking a card routes to /writing/draft/[id]. The draft preview shows the
|
Clicking a card routes to /writing/draft/[id]. The draft preview shows the
|
||||||
first ~160 chars of the current version so the card isn't empty for an
|
first ~160 chars of the current version so the card isn't empty for an
|
||||||
unstarted draft.
|
unstarted draft.
|
||||||
|
|
@ -23,8 +27,17 @@
|
||||||
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||||
import { decryptRecords } from '$lib/data/crypto';
|
import { decryptRecords } from '$lib/data/crypto';
|
||||||
import { toDraftVersion } from '../queries';
|
import { toDraftVersion } from '../queries';
|
||||||
|
import { KIND_LABELS } from '../constants';
|
||||||
import type { Draft, DraftVersion, DraftKind, DraftStatus, LocalDraftVersion } from '../types';
|
import type { Draft, DraftVersion, DraftKind, DraftStatus, LocalDraftVersion } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick-start kinds for the empty-state hero. The plan covers 12
|
||||||
|
* kinds total but six fit nicely in a 2x3 / 3x2 grid and these are
|
||||||
|
* the ones a first-time user is most likely to recognise. The full
|
||||||
|
* 12-kind picker is one click away inside the BriefingForm.
|
||||||
|
*/
|
||||||
|
const QUICK_START_KINDS: DraftKind[] = ['blog', 'essay', 'email', 'social', 'letter', 'speech'];
|
||||||
|
|
||||||
const drafts$ = useAllDrafts();
|
const drafts$ = useAllDrafts();
|
||||||
const drafts = $derived(drafts$.value);
|
const drafts = $derived(drafts$.value);
|
||||||
|
|
||||||
|
|
@ -88,6 +101,18 @@
|
||||||
openDraft(d);
|
openDraft(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick-start tile click — opens the BriefingForm with the picked
|
||||||
|
* kind pre-selected. Sets `activeKind` so `presetKind()` returns it
|
||||||
|
* to BriefingForm via initialKind.
|
||||||
|
*/
|
||||||
|
function startWithKind(kind: DraftKind) {
|
||||||
|
activeKind = kind;
|
||||||
|
showCreate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmpty = $derived(drafts$.value.length === 0 && !drafts$.loading);
|
||||||
|
|
||||||
// Workbench "Neuer Draft" context-menu action. Uses the shared
|
// Workbench "Neuer Draft" context-menu action. Uses the shared
|
||||||
// mana:quick-action event channel that the app-registry dispatches
|
// mana:quick-action event channel that the app-registry dispatches
|
||||||
// from the card's kebab menu. Also fires when the user picks the
|
// from the card's kebab menu. Also fires when the user picks the
|
||||||
|
|
@ -105,37 +130,53 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="writing-shell">
|
<div class="writing-shell">
|
||||||
<div class="controls">
|
<!--
|
||||||
<div class="search-row">
|
Top action bar is always visible — even in the empty state — so the
|
||||||
|
primary CTA stays in the same place. In empty mode the search +
|
||||||
|
filter rows are suppressed because filtering nothing is noise; the
|
||||||
|
Stile-Link drops to a small ghost button beside the CTA.
|
||||||
|
-->
|
||||||
|
<div class="action-bar" class:compact={isEmpty}>
|
||||||
|
{#if !isEmpty}
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
class="search"
|
class="search"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
placeholder="Nach Titel oder Thema suchen…"
|
placeholder="Nach Titel oder Thema suchen…"
|
||||||
/>
|
/>
|
||||||
<a href="/writing/styles" class="styles-link" title="Stile verwalten">🎨 Stile</a>
|
{/if}
|
||||||
<button
|
<a
|
||||||
type="button"
|
href="/writing/styles"
|
||||||
class="create-btn"
|
class="styles-link"
|
||||||
class:active={showCreate}
|
title="Stile verwalten"
|
||||||
onclick={() => (showCreate = !showCreate)}
|
aria-label="Stile verwalten"
|
||||||
aria-expanded={showCreate}
|
>
|
||||||
>
|
🎨
|
||||||
{showCreate ? '× Schließen' : '+ Neuer Draft'}
|
</a>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
class="create-btn"
|
||||||
<KindTabs active={activeKind} {counts} onselect={(k) => (activeKind = k)} />
|
class:active={showCreate}
|
||||||
|
onclick={() => (showCreate = !showCreate)}
|
||||||
<div class="filter-row">
|
aria-expanded={showCreate}
|
||||||
<StatusFilter active={activeStatus} onselect={(s) => (activeStatus = s)} />
|
>
|
||||||
<label class="fav-toggle">
|
{showCreate ? '× Schließen' : '+ Neuer Draft'}
|
||||||
<input type="checkbox" bind:checked={showFavoritesOnly} />
|
</button>
|
||||||
<span>Nur Favoriten</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !isEmpty}
|
||||||
|
<div class="filter-stack">
|
||||||
|
<KindTabs active={activeKind} {counts} onselect={(k) => (activeKind = k)} />
|
||||||
|
<div class="filter-row">
|
||||||
|
<StatusFilter active={activeStatus} onselect={(s) => (activeStatus = s)} />
|
||||||
|
<label class="fav-toggle">
|
||||||
|
<input type="checkbox" bind:checked={showFavoritesOnly} />
|
||||||
|
<span>Nur Favoriten</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showCreate}
|
{#if showCreate}
|
||||||
<div class="inline-create">
|
<div class="inline-create">
|
||||||
<BriefingForm
|
<BriefingForm
|
||||||
|
|
@ -149,17 +190,39 @@
|
||||||
|
|
||||||
{#if drafts$.loading}
|
{#if drafts$.loading}
|
||||||
<p class="muted center">Lädt…</p>
|
<p class="muted center">Lädt…</p>
|
||||||
|
{:else if isEmpty && !showCreate}
|
||||||
|
<!-- Hero empty-state: the "What is this?" view. -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-icon" aria-hidden="true">📝</div>
|
||||||
|
<h2>Dein KI-Ghostwriter</h2>
|
||||||
|
<p class="hero-pitch">
|
||||||
|
Brief Thema, Stil und Quellen — ein fertiger Entwurf entsteht. Verfeinere ihn absatzweise
|
||||||
|
mit ⌘G zum Generieren, Markieren + Selection-Tools, oder direkt im Editor.
|
||||||
|
</p>
|
||||||
|
<p class="hero-meta">
|
||||||
|
✨ 12 Textarten · 9 Stile · 7 Quellen-Typen · End-to-End-verschlüsselt
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="quick-start">
|
||||||
|
<p class="quick-start-label">Schnellstart:</p>
|
||||||
|
<div class="quick-grid">
|
||||||
|
{#each QUICK_START_KINDS as kind (kind)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="quick-tile"
|
||||||
|
onclick={() => startWithKind(kind)}
|
||||||
|
title={`Neuer ${KIND_LABELS[kind].de}-Entwurf`}
|
||||||
|
>
|
||||||
|
<span class="quick-emoji" aria-hidden="true">{KIND_LABELS[kind].emoji}</span>
|
||||||
|
<span class="quick-label">{KIND_LABELS[kind].de}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{:else if filtered.length === 0}
|
{:else if filtered.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
{#if drafts.length === 0}
|
<p class="muted">Keine Drafts passen zum aktuellen Filter.</p>
|
||||||
<h2>Noch keine Drafts</h2>
|
|
||||||
<p>
|
|
||||||
Klick auf <strong>+ Neuer Draft</strong>, brief dem Ghostwriter Thema, Stil und Länge — M3
|
|
||||||
ergänzt die Generate-Funktion. Bis dahin kannst du Drafts manuell erstellen und editieren.
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="muted">Keine Drafts passen zum aktuellen Filter.</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
|
@ -188,25 +251,31 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
.controls {
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.action-bar.compact {
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.filter-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
.search-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.styles-link {
|
.styles-link {
|
||||||
padding: 0.45rem 0.75rem;
|
padding: 0.4rem 0.55rem;
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
|
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.85rem;
|
font-size: 0.95rem;
|
||||||
|
line-height: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -272,17 +341,100 @@
|
||||||
}
|
}
|
||||||
.empty {
|
.empty {
|
||||||
max-width: 540px;
|
max-width: 540px;
|
||||||
margin: 3rem auto;
|
margin: 2rem auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.empty h2 {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
.empty p {
|
.empty p {
|
||||||
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hero-icon {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.hero h2 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hero-pitch {
|
||||||
|
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
max-width: 480px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.hero-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted, rgba(0, 0, 0, 0.45));
|
||||||
|
margin: 0 0 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-start {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.quick-start-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.quick-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
.quick-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.85rem 0.5rem;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
|
||||||
|
background: var(--color-surface, rgba(255, 255, 255, 0.04));
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
background 0.15s ease,
|
||||||
|
transform 0.1s ease;
|
||||||
|
}
|
||||||
|
.quick-tile:hover {
|
||||||
|
border-color: #0ea5e9;
|
||||||
|
background: color-mix(in srgb, #0ea5e9 6%, transparent);
|
||||||
|
color: #0ea5e9;
|
||||||
|
}
|
||||||
|
.quick-tile:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.quick-emoji {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.quick-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.quick-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue