This skill should be used when the user asks to edit a PDF, rotate/reorder/delete PDF pages, build a Word or .docx editor, build a spreadsheet or .xlsx editor, edit cells or formulas in the browser, capture a signature, add e-signature, sign a PDF, stamp a signature onto a PDF, or save/export/upload an edited document in a Svelte 5 app using extend-ui-svelte (PdfEditor, DocxEditor, XlsxEditor, ESignature).
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Four editing components from extend-ui-svelte, a Svelte 5 port of extend-hq/ui. For install, SSR rules, the PDF/DOCX/XLSX engines, and the parent-controlled dark-mode contract, read the core skill first: ../extend-ui-svelte/SKILL.md. This skill covers what happens after the document is on screen: editing it, reading the changes back, and saving the result.
| Need | Component | What it can actually edit |
|---|---|---|
| Rotate, reorder, delete PDF pages | PdfEditor (alias PDFEditor) | Page-level edits only. No annotations, forms, redaction, text edits, or signing. |
| Light rich-text edit of a .docx | DocxEditor (alias DocxEditorPreview) | Paragraphs, H1–H3, bold, italic, bulleted and numbered lists. Export is lossy. |
| Edit cells and formulas in a workbook | XlsxEditor (alias XlsxEditorPreview) | Values, formulas, bold/align/fill (fill and style are not written on export), all sheets. |
| Draw a signature and stamp it on a PDF | ESignature (alias ESignatureBlock) | One drawn signature on one page, placed by numeric coordinates. |
If the user needs something outside the "actually edit" column (PDF annotations, tracked changes, number formats, multiple signers), say so up front. Those are known gaps against upstream React, not missing configuration. See "Parity gaps" below.
// Components: package root
import { PdfEditor, DocxEditor, XlsxEditor, ESignature } from 'extend-ui-svelte';
// Helpers and types: per-component subpath (package.json "./components/*")
import { exportEditedPdf, type EditedPdfPage } from 'extend-ui-svelte/components/pdf-editor';
import { exportDocxBytes, DOCX_MIME_TYPE } from 'extend-ui-svelte/components/docx-editor';
import { sheetsToXlsxBytes, type XlsxCellChange } from 'extend-ui-svelte/components/xlsx-editor';
import { embedSignaturePng, resolvePlacement } from 'extend-ui-svelte/components/e-signature';The root barrel exports only the components and PdfEditorProps/PDFEditorProps/ESignatureProps. Every helper comes from the subpath. With the shadcn-svelte registry copy-in, import the same names from the copied folder, e.g. $lib/components/pdf-editor/index.js.
<script lang="ts">
import { PdfEditor, DocxEditor, XlsxEditor, ESignature } from 'extend-ui-svelte';
let isDark = $state(false);
</script>
<PdfEditor src="/files/contract.pdf" {isDark} onSave={({ buffer, fileName }) => console.log(fileName, buffer.byteLength)} />
<!-- isDark and onIsDarkChange are REQUIRED props on DocxEditor and XlsxEditor -->
<DocxEditor src="/files/memo.docx" {isDark} onIsDarkChange={(v) => (isDark = v)} onExport={(bytes) => console.log(bytes.byteLength)} />
<XlsxEditor src="/files/budget.xlsx" {isDark} onIsDarkChange={(v) => (isDark = v)} onCellChange={(c) => console.log(c)} />
<ESignature file="/files/nda.pdf" fileName="nda" {isDark} onSignedDownload={(name) => console.log(name)} />None of these components exposes a bindable document value or an imperative handle. Changes come out through callbacks, and export runs from the component's own toolbar button. Plan the save path around that.
PdfEditor
src is a URL, ArrayBuffer, Uint8Array, Blob, or File. onDocumentLoadSuccess({ numPages, documentId, fileName }) or onDocumentLoadError(message).onPagesChange(pages) with the full EditedPdfPage[] ({ sourcePage, rotation }, in the new order, deleted pages left out).onSave({ buffer: ArrayBuffer, fileName }).showDownload={false}, keep the latest pages from onPagesChange, and call exportEditedPdf(sourceBytes, { pages }) yourself (it returns a Uint8Array). Setting showDownload={false} also turns off Cmd/Ctrl+S.DocxEditor
src is typed string (a URL). For a local File, pass URL.createObjectURL(file), or let the user pick one with the built-in Upload button.contenteditable surface. There is no change callback. The editor's HTML is internal state.onExport(bytes: Uint8Array) and then downloads <fileName>.docx. Both always happen.XlsxEditor
data (ArrayBuffer | Uint8Array) takes precedence over src (URL, fetched on the client). Changing either one resets every edit.onCellChange({ sheet, row, col, value }). Row and column are zero-based. For a formula, value is the text including =, for example "=SUM(A1:A3)".onExport(bytes, fileName). Bold, alignment, and fill are dropped on export.ESignature
file (or the alias src) is a URL string, fetched on the client. Use URL.createObjectURL(file) for local files.onSignatureChange(hasSignature) fires after each stroke and after Clear.positionX, positionY, signatureWidth, signatureHeight, and scale are $bindable.<name>-signed.pdf and calls onSignedDownload(fileName). No callback returns the signed bytes. To upload a signed PDF, build the flow with the helpers instead (next section).Every export path ends in a Uint8Array or ArrayBuffer. Wrap it in a Blob with the right MIME type:
const MIME = {
pdf: 'application/pdf',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // DOCX_MIME_TYPE
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
};
async function upload(bytes: Uint8Array | ArrayBuffer, name: string, type: string) {
const body = new FormData();
body.append('file', new Blob([bytes as BlobPart], { type }), name);
const res = await fetch('/api/documents', { method: 'POST', body });
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
}onSave (download already happened) or exportEditedPdf (no download).onExport (the download still fires).onExport (the download still fires). For a download-free save, the component offers nothing. Rebuild sheets yourself and call sheetsToXlsxBytes(sheets) (see references/xlsx-editor.md).+server.ts that returns the stored bytes with Content-Type and Content-Disposition: attachment.There are two routes:
<ESignature file={url} /> handles drawing, placing, previewing, and downloading. Choose it when a download is the end of the flow.signature_pad directly for capture and the helpers for stamping:import SignaturePad from 'signature_pad';
import { dataUrlToBytes, defaultPlacementForPage, embedSignaturePng, resolvePlacement }
from 'extend-ui-svelte/components/e-signature';
const png = dataUrlToBytes(pad.toDataURL('image/png')); // pad: SignaturePad
const box = defaultPlacementForPage(612, 792); // or your own x/y/width/height
const placement = resolvePlacement({ ...box, page: 1, scale: 1, pageCount });
const signed = await embedSignaturePng(pdfBytes, png, placement); // Uint8ArrayCoordinates are PDF points from the bottom-left corner (pdf-lib convention), not CSS pixels from the top-left. A Letter page is 612×792 pt. Load signature_pad inside onMount with await import('signature_pad'), because it needs a canvas. pdf-lib and the sign-pdf helpers are safe during SSR. With pnpm or another strict installer, add signature_pad and pdf-lib as direct dependencies before importing them in app code. Full flow: examples/sign-and-upload-pdf.svelte.
All four take isDark and never flip it themselves (enforced by the parity suite). The core skill covers the contract. Per editor:
DocxEditor and XlsxEditor require isDark and onIsDarkChange in their TS props. Their toolbars show a dark-mode switch that calls onIsDarkChange(!isDark).PdfEditor and ESignature render no switch. onIsDarkChange is accepted and never called.onMount or $effect, so server renders show only the empty or loading shell. No {#if browser} guard is needed.onPagesChange/onCellChange, before navigating away. Add a beforeNavigate or beforeunload guard if edits matter.src or data reference resets the editor. Do not create a fresh Uint8Array on every render (for example, inside $derived from a changing object).class and className are both accepted and merged on every component.src/lib/parity/port-parity.test.ts)defaultMode, annotationAuthor, colorPresets, features, toolbarActions, selectionActions, persistSignatures, stampManifests, signatureFontsStylesheetUrl, resolveScrollAreaViewport, renderPageOverlay, onModeChange, onAnnotationEvent, onAnnotationsChange, onFormValuesChange, onRedactionsApplied, onCapture, onToast, onSelectionAction. It has no imperative handle and no multi-page selection.showToolbar, readOnly, onExport for DOCX; data, rowHeight, height, overscan, onCellChange, onExport for XLSX). Features are narrower: see the references.file. Fields, recipients, the draggable placement overlay, and signing states are not ported.references/pdf-editor.md, references/docx-editor.md, references/xlsx-editor.md, references/e-signature.md cover each component's full props table, callbacks, exported helpers and types, keyboard behavior, a realistic example, and gotchas.examples/sign-and-upload-pdf.svelte: custom signature capture, stamping with pdf-lib, then download and upload.examples/edit-spreadsheet-save-to-api.svelte: XlsxEditor with change tracking and onExport saving to an API.32dbcd7
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.