Files
Voiced/src/components/PageSetupControls.tsx
T
xavierk aa7a407944 Add the Neutral template family, template picker and text-only logo allowances
Linea (the vendor's original format.pdf design), Monogram and Serenity are
rebuilt from the reference PDFs as one parametric family on the engine, with
the user's logo in the measured slot, a home for every GST field and
A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and
a generated Ms Madi outline for Serenity's vertical word. Text-only logo
placeholders (Monolith, Tangerine) use a doc-derived allowance box. A
Carbon TemplatePicker with committed thumbnails opens from the page
setup controls.
2026-10-04 07:27:12 +05:30

131 lines
4.6 KiB
TypeScript

import { useState } from "react";
import { Button, ContentSwitcher, InlineLoading, Select, SelectItem, Switch, Toggle } from "@carbon/react";
import { Catalog } from "@carbon/icons-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";
import { catalogEntry } from "../pdf/templates/catalog";
import TemplatePicker from "./TemplatePicker";
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;
}
/** 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 }: Props) {
const [saving, setSaving] = useState(false);
const [pickerOpen, setPickerOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const saved = prefsToJson(parsePrefs(settings.renderPrefs));
const isDefault = prefsToJson(prefs) === saved;
const template = catalogEntry(prefs.templateId);
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">
<div className="voiced-page-setup__field">
<span className="cds--label">Template</span>
<Button kind="tertiary" size="md" renderIcon={Catalog} disabled={disabled} onClick={() => setPickerOpen(true)}>
{template.name}
</Button>
</div>
<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>
{template.logoOptIn ? (
<div className="voiced-page-setup__row voiced-page-setup__extra">
<Toggle
id="template-logo-show"
size="sm"
labelText="Logo"
labelA="Hidden"
labelB="Shown above the title"
toggled={prefs.logo?.show === true}
disabled={disabled}
onToggle={(on) => onChange({ ...prefs, logo: { ...prefs.logo, show: on } })}
/>
</div>
) : null}
<TemplatePicker
open={pickerOpen}
onClose={() => setPickerOpen(false)}
selectedId={template.id}
onSelect={(id) => onChange({ ...prefs, templateId: id })}
isDefault={isDefault}
saving={saving}
error={error}
onSaveDefault={() => void saveDefault()}
disabled={disabled}
/>
<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>
);
}