feat(writing): print CSS + keyboard shortcuts

Two DetailView polish items.

Print / PDF (fixes M10's "Drucken / PDF" action):
- New <article class="print-target"> at the top of the route renders
  just the title + current version content. Hidden on screen, only
  visible under @media print so window.print() produces a clean
  manuscript instead of dumping the whole workbench chrome.
- :global(body > *) toggle suppresses the surrounding SvelteKit /
  workbench frame; the .shell + the per-card chrome are explicitly
  display:none in print. @page margin: 2cm gives a readable page
  with no further user setup.
- Body uses ui-serif so the printed prose looks like manuscript.

Keyboard shortcuts (DetailView document-level listener):
- ⌘G / Ctrl+G       → generate / re-generate (was: only the button)
- ⌘⇧S / Ctrl+Shift+S → save checkpoint
- ⌘Z / Ctrl+Z        → undo last refinement (only fires when refineUndo
                       is set; otherwise falls through to the textarea's
                       native undo as the user expects)

Buttons + the undo row carry the shortcut in their title attribute so
mouse-users discover them via tooltip.

i18n baseline +1 for DetailView (the new "(⌘Z)" tooltip suffix counts
as one additional German fragment per the validator's heuristic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-25 12:10:32 +02:00
parent 7e6fb5b6d1
commit bad935c258
2 changed files with 106 additions and 5 deletions

View file

@ -8,6 +8,7 @@
-->
<script lang="ts">
import { formatDateTime } from '$lib/i18n/format';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
import BriefingForm from '../components/BriefingForm.svelte';
@ -250,6 +251,38 @@
refineUndo = null;
}
// ─── Keyboard shortcuts ────────────────────────────────
// ⌘G / Ctrl+G → Generate (or "neu generieren")
// ⌘⇧S / Ctrl+Shift+S → Save checkpoint
// ⌘Z / Ctrl+Z → Undo last refinement (only when refineUndo is set)
// We listen at document level; modifier-based combos are safe even
// when the textarea has focus. ⌘Z without a refineUndo target falls
// through to the textarea's native undo, which the user expects.
onMount(() => {
function onKey(ev: KeyboardEvent) {
const mod = ev.metaKey || ev.ctrlKey;
if (!mod) return;
const key = ev.key.toLowerCase();
if (key === 'g' && !ev.shiftKey && !ev.altKey) {
ev.preventDefault();
void generate();
return;
}
if (key === 's' && ev.shiftKey && !ev.altKey) {
ev.preventDefault();
void saveCheckpoint();
return;
}
if (key === 'z' && !ev.shiftKey && !ev.altKey && refineUndo) {
ev.preventDefault();
void undoLastRefinement();
return;
}
}
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
});
const hasDraftContent = $derived((currentVersion?.content ?? '').trim().length > 0);
const kind = $derived(draft ? KIND_LABELS[draft.kind] : null);
@ -281,6 +314,19 @@
<a href="/writing">Zurück zur Übersicht</a>
</div>
{:else}
<!--
Print target — only visible when the user triggers Drucken/PDF
via the ExportMenu (which calls window.print()). The screen layout
is suppressed by @media print so the printed page is just title +
body, without the workbench chrome.
-->
<article class="print-target" aria-hidden="true">
<h1 class="print-title">{draft.title || draft.briefing.topic || 'Unbenannt'}</h1>
{#if currentVersion}
<div class="print-body">{currentVersion.content}</div>
{/if}
</article>
<div class="shell">
<header class="head">
<div class="title-row">
@ -391,8 +437,8 @@
onclick={generate}
disabled={generating}
title={hasDraftContent
? 'Kompletten Text neu generieren (überschreibt nicht — neue Version)'
: 'Ersten Entwurf aus dem Briefing generieren'}
? 'Kompletten Text neu generieren — neue Version (⌘G)'
: 'Ersten Entwurf aus dem Briefing generieren (⌘G)'}
>
{#if generating}
Schreibt…
@ -407,7 +453,7 @@
class="checkpoint"
onclick={saveCheckpoint}
disabled={saving}
title="Aktuellen Text als neue Version einfrieren"
title="Aktuellen Text als neue Version einfrieren (⌘⇧S)"
>
{saving ? 'Speichert…' : ' Checkpoint'}
</button>
@ -439,7 +485,12 @@
{/if}
{#if refineUndo && !refinement}
<div class="undo-row">
<button type="button" class="undo-btn" onclick={undoLastRefinement}>
<button
type="button"
class="undo-btn"
onclick={undoLastRefinement}
title="Letzte Auswahl-Verfeinerung rückgängig (⌘Z)"
>
↶ Rückgängig: {refineUndo.label}
</button>
</div>
@ -767,4 +818,54 @@
grid-template-columns: 1fr;
}
}
/* ─── Print / PDF — triggered by ExportMenu's window.print() ────
The screen layout (.shell + the rest of the SvelteKit app shell)
is hidden; only .print-target stays visible so the PDF reads
like a manuscript: serif body, full-width prose, no chrome.
`:global(...)` reaches through component boundaries to suppress
the route page + workbench wrapping that the user can't see
from inside this file. */
.print-target {
display: none;
}
@media print {
:global(body > *) {
visibility: hidden;
}
.print-target,
.print-target * {
visibility: visible;
}
.print-target {
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 0;
margin: 0;
background: white;
color: black;
font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
}
.print-title {
font-size: 1.8rem;
line-height: 1.2;
margin: 0 0 1.5rem;
font-weight: 600;
}
.print-body {
white-space: pre-wrap;
font-size: 1.05rem;
line-height: 1.65;
}
@page {
margin: 2cm;
}
.shell,
.empty {
display: none !important;
}
}
</style>

View file

@ -285,7 +285,7 @@
"apps/mana/apps/web/src/lib/modules/writing/components/StyleForm.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/components/VersionEditor.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/components/VersionHistory.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/views/DetailView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/writing/views/DetailView.svelte": 7,
"apps/mana/apps/web/src/lib/modules/writing/views/ListView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/writing/views/StylesView.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/+page.svelte": 1,