Replace the HTML preview with a worker-rendered PDF preview
Rendering moves into a module Web Worker behind a latest-wins RenderClient with a watchdog and a handshake-based main-thread fallback. The preview shows the real PDF through pdf.js at 2x with zoom, page indicator, audit issues popover and a focus modal. A4/Letter and margin controls are frozen into each invoice at issue and can be saved as the default (migration M3). react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB to 0.5 MB. The HTML twin and its styles are removed.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { Button, ContentSwitcher, InlineLoading, Select, SelectItem, Switch } from "@carbon/react";
|
||||
import { api } from "../lib/api";
|
||||
import type { Settings } from "../lib/types";
|
||||
import type { MarginPreset, PageId } from "../pdf/engine/geometry";
|
||||
import { parsePrefs, prefsToJson, type RenderPrefsV1 } from "../pdf/model/prefs";
|
||||
|
||||
const PAGES: { id: PageId; label: string }[] = [
|
||||
{ id: "A4", label: "A4" },
|
||||
{ id: "LETTER", label: "US Letter" },
|
||||
];
|
||||
|
||||
const MARGINS: { id: MarginPreset; label: string }[] = [
|
||||
{ id: "template", label: "Template default" },
|
||||
{ id: "narrow", label: "Narrow (0.5″)" },
|
||||
{ id: "normal", label: "Normal (0.75″)" },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
prefs: RenderPrefsV1;
|
||||
onChange: (prefs: RenderPrefsV1) => void;
|
||||
/** Live settings; `renderPrefs` is the saved default. */
|
||||
settings: Settings;
|
||||
/** Called after the default was saved so the app reloads its settings. */
|
||||
onSaved: () => void;
|
||||
disabled?: boolean;
|
||||
/** Where a TemplatePicker goes once there is more than one template. */
|
||||
templateSlot?: ReactNode;
|
||||
}
|
||||
|
||||
/** Page size and margins for the invoice being edited, plus "Save as default" for new invoices. */
|
||||
export default function PageSetupControls({ prefs, onChange, settings, onSaved, disabled, templateSlot }: Props) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const saved = prefsToJson(parsePrefs(settings.renderPrefs));
|
||||
const isDefault = prefsToJson(prefs) === saved;
|
||||
|
||||
const saveDefault = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.saveSettings({ ...settings, renderPrefs: prefsToJson(prefs) });
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="voiced-page-setup">
|
||||
<div className="voiced-page-setup__row">
|
||||
{templateSlot}
|
||||
<div className="voiced-page-setup__field">
|
||||
<span className="cds--label">Page size</span>
|
||||
<ContentSwitcher
|
||||
size="md"
|
||||
selectedIndex={Math.max(0, PAGES.findIndex((p) => p.id === prefs.pageSize))}
|
||||
onChange={(e) => !disabled && onChange({ ...prefs, pageSize: PAGES[e.index ?? 0].id })}
|
||||
>
|
||||
{PAGES.map((p) => (
|
||||
<Switch key={p.id} name={p.id} text={p.label} disabled={disabled} />
|
||||
))}
|
||||
</ContentSwitcher>
|
||||
</div>
|
||||
<div className="voiced-page-setup__field">
|
||||
<Select
|
||||
id="page-margins"
|
||||
labelText="Margins"
|
||||
size="md"
|
||||
disabled={disabled}
|
||||
value={prefs.margins}
|
||||
onChange={(e) => onChange({ ...prefs, margins: e.target.value as MarginPreset })}
|
||||
>
|
||||
{MARGINS.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id} text={m.label} />
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="voiced-page-setup__footer">
|
||||
{saving ? (
|
||||
<InlineLoading status="active" description="Saving…" />
|
||||
) : (
|
||||
<Button kind="tertiary" size="sm" disabled={isDefault} onClick={() => void saveDefault()}>
|
||||
{isDefault ? "Saved as default" : "Save as default"}
|
||||
</Button>
|
||||
)}
|
||||
<span className="voiced-muted voiced-page-setup__note">
|
||||
{error ?? "This choice is frozen into each invoice when it is issued."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user