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:
Till JS 2026-04-25 13:57:08 +02:00
parent 95bedf4625
commit 21dbce6631
2 changed files with 223 additions and 49 deletions

View file

@ -612,6 +612,28 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
'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: {
description:
'Medien-Log — Bücher, Filme, Serien und Comics tracken. Status, Fortschritt, Bewertung.',

View file

@ -1,6 +1,10 @@
<!--
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
first ~160 chars of the current version so the card isn't empty for an
unstarted draft.
@ -23,8 +27,17 @@
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { decryptRecords } from '$lib/data/crypto';
import { toDraftVersion } from '../queries';
import { KIND_LABELS } from '../constants';
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 = $derived(drafts$.value);
@ -88,6 +101,18 @@
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
// mana:quick-action event channel that the app-registry dispatches
// from the card's kebab menu. Also fires when the user picks the
@ -105,37 +130,53 @@
</script>
<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
type="search"
class="search"
bind:value={searchQuery}
placeholder="Nach Titel oder Thema suchen…"
/>
<a href="/writing/styles" class="styles-link" title="Stile verwalten">🎨 Stile</a>
<button
type="button"
class="create-btn"
class:active={showCreate}
onclick={() => (showCreate = !showCreate)}
aria-expanded={showCreate}
>
{showCreate ? '× Schließen' : '+ Neuer Draft'}
</button>
</div>
<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>
{/if}
<a
href="/writing/styles"
class="styles-link"
title="Stile verwalten"
aria-label="Stile verwalten"
>
🎨
</a>
<button
type="button"
class="create-btn"
class:active={showCreate}
onclick={() => (showCreate = !showCreate)}
aria-expanded={showCreate}
>
{showCreate ? '× Schließen' : '+ Neuer Draft'}
</button>
</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}
<div class="inline-create">
<BriefingForm
@ -149,17 +190,39 @@
{#if drafts$.loading}
<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}
<div class="empty">
{#if drafts.length === 0}
<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}
<p class="muted">Keine Drafts passen zum aktuellen Filter.</p>
</div>
{:else}
<div class="grid">
@ -188,25 +251,31 @@
text-align: center;
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;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.search-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.styles-link {
padding: 0.45rem 0.75rem;
border-radius: 0.55rem;
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
padding: 0.4rem 0.55rem;
border-radius: 0.5rem;
border: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
background: transparent;
color: inherit;
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
text-decoration: none;
font-size: 0.85rem;
font-size: 0.95rem;
line-height: 1;
white-space: nowrap;
flex-shrink: 0;
}
@ -272,17 +341,100 @@
}
.empty {
max-width: 540px;
margin: 3rem auto;
margin: 2rem auto;
text-align: center;
}
.empty h2 {
margin: 0 0 0.5rem;
font-size: 1.2rem;
}
.empty p {
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
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 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));