diff --git a/.agents/skills/docs-write-guide/SKILL.md b/.agents/skills/docs-write-guide/SKILL.md new file mode 100644 index 00000000..c3ec81df --- /dev/null +++ b/.agents/skills/docs-write-guide/SKILL.md @@ -0,0 +1,44 @@ +--- +name: write-guide +description: | + Generates technical guides that teach real-world use cases through progressive examples. + + **Auto-activation:** User asks to write, create, or draft a guide or tutorial. Also use when converting feature documentation, API references, or skill knowledge into step-by-step learning content. + + **Input sources:** API documentation, existing code examples, or user-provided specifications. +--- + +# Writing Guides + +## Goal + +Produce a technical guide that teaches a real-world use case through progressive examples. Concepts are introduced only when the reader needs them. + +Each guide solves **one specific problem**. Not a category of problems. If the outline has 5+ steps or covers multiple approaches, split it. + +### Workflow + +1. **Research**: Read existing docs for context and linking opportunities. +2. **Plan**: Outline sections. Verify scope. Each step needs a friction point and resolution. +3. **Write**: Follow the template above. Apply the rules below. +4. **Review**: Re-read the rules, verify, then present. + +## Rules + +1. **Progressive disclosure.** Start with the smallest working example. Introduce complexity only when the example breaks. Name concepts at the moment of resolution, after the reader has felt the problem. Full loop: working → new requirement → something breaks → explain why → name the fix → apply → verify with proof → move on. +2. **One friction point per step.** If a step has multiple friction points, split it. +3. **No em dashes.** Use periods, commas, or parentheses instead. +4. **Mechanical, observable language.** Describe what happens, not how it feels. +5. **No selling, justifying, or comparing.** No "the best way," no historical context, no framework comparisons. + +| Don't | Do | +| ---------------------------------------------------- | -------------------------------------------------------- | +| "creates friction in the pipeline" | "blocks the response" | +| "needs dynamic information" | "depends on request-time data" | +| "requires dynamic processing" | "output can't be known ahead of time" | +| "The component blocks the response — causing delays" | "The component blocks the response. This causes delays." | + + +## References + +We write docs on `apps/web/content/docs/**/*`, any reference point or example can be found there. Any new docs should also be added there. diff --git a/packages/ducjs/src/restore/restoreElements.ts b/packages/ducjs/src/restore/restoreElements.ts index c1547111..f58d2ad2 100644 --- a/packages/ducjs/src/restore/restoreElements.ts +++ b/packages/ducjs/src/restore/restoreElements.ts @@ -751,12 +751,36 @@ const restoreElement = ( case "doc": { const docElement = element as DucDocElement; + // Migration: old format stored the compiled PDF on fileId and the Typst + // source in referencedFileIds. New format stores the Typst source on + // fileId and the PDF cache in referencedFileIds with a doc_pdf_cache_ prefix. + let migratedFileId = (isValidString(docElement.fileId) as ExternalFileId) || null; + let migratedReferencedFileIds = docElement.referencedFileIds ?? []; + + if (migratedFileId && typeof migratedFileId === "string" && migratedFileId.startsWith("doc_pdf_")) { + const typstSourceId = migratedReferencedFileIds.find( + (rid) => typeof rid === "string" && rid.startsWith("doc_typst_source_"), + ); + if (typstSourceId) { + // Move old PDF cache to referencedFileIds with new prefix + const elementId = docElement.id ?? ""; + const newCacheId = `doc_pdf_cache_${elementId}` as ExternalFileId; + migratedReferencedFileIds = migratedReferencedFileIds.filter( + (rid) => rid !== typstSourceId, + ); + if (!migratedReferencedFileIds.includes(newCacheId)) { + migratedReferencedFileIds = [...migratedReferencedFileIds, newCacheId]; + } + migratedFileId = typstSourceId as ExternalFileId; + } + } + return restoreElementWithProperties( element, { text: isValidString(docElement.text), - fileId: (isValidString(docElement.fileId) as ExternalFileId) || null, - referencedFileIds: docElement.referencedFileIds || [], + fileId: migratedFileId, + referencedFileIds: migratedReferencedFileIds, gridConfig: restoreDocumentGridConfig(docElement.gridConfig), }, localState diff --git a/packages/ducjs/src/types/elements/typeChecks.ts b/packages/ducjs/src/types/elements/typeChecks.ts index cfd6effd..92187a27 100644 --- a/packages/ducjs/src/types/elements/typeChecks.ts +++ b/packages/ducjs/src/types/elements/typeChecks.ts @@ -3,33 +3,34 @@ import { assertNever } from "../../utils"; import { Bounds, LineSegment, TuplePoint } from "../geometry.types"; import type { MarkNonNullable } from "../utility.types"; import type { - DucArrowElement, - DucBindableElement, - DucDocElement, - DucElbowArrowElement, - DucElement, - DucElementType, - DucEllipseElement, - DucEmbeddableElement, - DucFlowchartNodeElement, - DucFrameElement, - DucFrameLikeElement, - DucFreeDrawElement, - DucIframeLikeElement, - DucImageElement, - DucLinearElement, - DucNonSelectionElement, - DucPdfElement, - DucPlotElement, - DucPointBinding, - DucPolygonElement, - DucTableElement, - DucTextContainer, - DucTextElement, - DucTextElementWithContainer, - FixedPointBinding, - InitializedDucImageElement, - NonDeleted + DucArrowElement, + DucBindableElement, + DucDocElement, + DucElbowArrowElement, + DucElement, + DucElementType, + DucEllipseElement, + DucEmbeddableElement, + DucFlowchartNodeElement, + DucFrameElement, + DucFrameLikeElement, + DucFreeDrawElement, + DucIframeLikeElement, + DucImageElement, + DucLinearElement, + DucNonSelectionElement, + DucPdfElement, + DucPlotElement, + DucPointBinding, + DucPolygonElement, + DucTableElement, + DucTextContainer, + DucTextElement, + DucTextElementWithContainer, + ExternalFileId, + FixedPointBinding, + InitializedDucImageElement, + NonDeleted } from "./"; export const isInitializedImageElement = ( @@ -58,6 +59,35 @@ export const isPdfLikeElement = ( return !!element && (element.type === "pdf" || element.type === "doc"); }; +/** + * Resolves the file ID used for rendering a PDF-like element. + * + * For `pdf` elements, this is simply `element.fileId`. + * For `doc` elements, the `fileId` holds the Typst source file, and the + * compiled PDF cache is stored in `referencedFileIds` with a + * `doc_pdf_cache_` prefix. This function returns that cache file ID, or + * falls back to `element.fileId` for backward compatibility with older + * drawings where `fileId` was the PDF itself. + */ +const DOC_PDF_CACHE_FILE_ID_PREFIX = "doc_pdf_cache_"; + +export const getRenderablePdfFileId = ( + element: DucElement, +): ExternalFileId | null => { + if (element.type === "pdf") { + return element.fileId; + } + if (element.type === "doc") { + const cacheId = `${DOC_PDF_CACHE_FILE_ID_PREFIX}${element.id}` as ExternalFileId; + if (element.referencedFileIds?.includes(cacheId)) { + return cacheId; + } + // Fallback for legacy drawings where fileId was the PDF + return element.fileId; + } + return null; +}; + export const isEmbeddableElement = ( element: DucElement | null | undefined, ): element is DucEmbeddableElement => { @@ -284,9 +314,9 @@ export const isDucElement = ( case "model": case "plot": case "pdf": - { - return true; - } + { + return true; + } default: { assertNever(type, null); return false; diff --git a/packages/ducrs/src/serialize.rs b/packages/ducrs/src/serialize.rs index 24dbd554..d6ed4ce6 100644 --- a/packages/ducrs/src/serialize.rs +++ b/packages/ducrs/src/serialize.rs @@ -1112,7 +1112,7 @@ fn write_element_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String fn write_doc_referenced_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> { let mut stmt = tx.prepare_cached( - "INSERT INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)" + "INSERT OR IGNORE INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)" )?; for (i, fid) in file_ids.iter().enumerate() { stmt.execute(params![element_id, fid, i as i32])?;