feat: F4.8c the documentation mode's image export, as Java's

ExportRun measures Java's print panel (GUIContainer.prepareExportPanel) and
image on four projects and an empty one: 10 to spare around, a light grey
footer 20 below the lowest element, 20 below it, a transparent background,
the documentation painted as in the design. The sidebar gains Java's
"Exportieren": the surface painted with Canvas 2D (an SVG with a
foreignObject taints the canvas in WebKit) and saved under the project's
name; Java's message when saving fails. Not adopted: Java's empty image of
an empty project (its footer overflows). The editor takes a projectName;
the capture export shares the download helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 11:54:09 +00:00
co-authored by Claude Opus 5.5
parent de03f6bb96
commit 7e68414bb2
20 changed files with 1803 additions and 25 deletions
@@ -2,6 +2,8 @@
// and „Gliederungsfläche“ with Java's icons. An item is dragged onto the
// surface, where it lands with its middle under the pointer, as Java's does;
// tapped, it lands in the middle of the view (Java's tap makes nothing).
// „Exportieren“ saves the surface as an image (F4.8c); if that fails, Java's
// „Fehler beim Speichern aufgetreten!“ shows below it.
import { useRef, useState, type CSSProperties } from 'react';
import type { DocumentationItemKind } from '@mdg-filius/core';
@@ -9,10 +11,13 @@ import { DRAG_THRESHOLD } from '../canvas/Canvas.js';
import { useTranslate } from '../i18n/i18n.js';
import addRectangle from '../assets/documentation/add_small.png';
import addText from '../assets/documentation/add_text_small.png';
import exportIcon from '../assets/documentation/download_small.png';
export interface DocumentationSidebarProps {
/** An item dropped at a point of the screen, or tapped (no point). */
readonly onItem: (kind: DocumentationItemKind, at: { readonly clientX: number; readonly clientY: number } | null) => void;
/** „Exportieren“: the surface saved as an image; rejected, it failed. */
readonly onExport: () => Promise<void>;
}
const ITEMS: readonly { readonly kind: DocumentationItemKind; readonly icon: string; readonly label: string }[] = [
@@ -39,12 +44,25 @@ const item: CSSProperties = {
touchAction: 'none',
};
export function DocumentationSidebar({ onItem }: DocumentationSidebarProps) {
export function DocumentationSidebar({ onItem, onExport }: DocumentationSidebarProps) {
const t = useTranslate();
const [ghost, setGhost] = useState<{ kind: DocumentationItemKind; x: number; y: number } | null>(null);
const press = useRef<{ kind: DocumentationItemKind; x: number; y: number; dragging: boolean } | null>(null);
/** The click the browser sends after a drag ends on the button: not a tap. */
const afterDrag = useRef(false);
const [exporting, setExporting] = useState(false);
const [failed, setFailed] = useState(false);
const exportImage = () => {
setExporting(true);
setFailed(false);
onExport().then(
() => setExporting(false),
() => {
setExporting(false);
setFailed(true);
},
);
};
const onPointerDown = (kind: DocumentationItemKind) => (event: React.PointerEvent<HTMLButtonElement>) => {
press.current = { kind, x: event.clientX, y: event.clientY, dragging: false };
@@ -106,6 +124,15 @@ export function DocumentationSidebar({ onItem }: DocumentationSidebarProps) {
{t(each.label)}
</button>
))}
<button type="button" style={{ ...item, cursor: exporting ? 'progress' : 'pointer' }} title={t('docu.exportHint')} disabled={exporting} onClick={exportImage}>
<img src={exportIcon} alt="" width={40} height={40} draggable={false} />
{t('docu.export')}
</button>
{failed ? (
<p role="alert" style={{ margin: 0, fontSize: 12, color: 'var(--danger)' }}>
{t('docu.saveFailed')}
</p>
) : null}
{ghost === null ? null : (
// The item's own size, its middle under the pointer, as Java drags it.
<div