Copy everything below the line and paste it to an LLM, with your project description added at the end.
You are authoring a Yatate document project: a self-contained folder that
turns a fill-out form into a generated PDF. On submit, the app runs static
validation (form.json rules) → validate.js → process.js (declares
pages, assembles layout blocks) → renders.
Files you produce (one fenced code block each, see "Output format" below):
project.ypf- requiredname, plus page size/background that a blank-canvasaddPage()call falls back to for any ofwidth/height/backgroundColorit omits (real page geometry still comes entirely fromprocess.js'saddPage()calls, below).form.json- the fields a user fills out.overlay.canvas(and optionally other.canvaslayout partials, each with a mandatory"layout": "canvas"and aplacementslist) - blocks drawn onto a page from field values.process.js- required, no fallback. Declares every output page viaaddPage({width, height, backgroundColor})(blank canvas) oraddPage({pdf, pdfPage})(a page of a real project-relative PDF file) - page number = call order, first call is page 1 - then composites blocks onto them viaoverlay("overlay.canvas")(and any other partial, each optionally targeting a page number). ZeroaddPage()calls is a hard error. A variable-length page (e.g. a receipt) can callsetPageHeight(page, padding), once, after every block is placed, to trim that page down to its actual content height plus padding - the trim point is each block's own declaredheight, not what it actually renders at, so give a grid overlay block (or a wrappedtextblock) a generousheightapproximating its real expected content, or the page gets trimmed too short and clips it.validate.js(optional) - cross-field validation only: anything a single field's ownrequired/min/max/min_length/max_length/regexinform.jsoncan already express, put there instead - don't duplicate it here. Two globals:inputs(same submitted-values map, mutable - see below) anderrors, a plain object, not an array (errors.push(...)throws) - assignerrors["field_id"] = "message"for a specific field, orerrors[""] = "message"(or an array of strings) for a general error that isn't any one field's fault. Leaveerrorsuntouched when there's nothing to report; the script's return value is ignored, onlyerrors's final state matters.inputsis mutable here, same read/write contractprocess.jshas (see below) - assigning a new or existing property is how a computed value gets both validated and used, carrying forward intoprocess.js/every block exactly like a directly-submitted field, as long as the script reports no errors.presets.json/autocomplete.json(optional, examples below).
Other process.js host functions (all optional, beyond addPage/
overlay/setPageHeight above - reach for these before reinventing
something they already do):
inputs.uid/inputs.timestamp- a unique id / reset datetime, both auto-generated whenever the form is reset (not fields you declare).setPageScale(page, x, y)/setDocScale(x, y)- resize a document for print (yomitted isx): scales the whole page about its upper-left corner and the output page size with it. Document and page scales multiply. To rotate or shift, use anoverlayblock'stransform.- A
gridoverlay block that hasrepeating_group_idis expanded once per record automatically whenoverlay()places it (no call); it stays pending until the script ends, sosetCells(grid, getCount(grid) + 1, [...])can append a totals row.getBlockInfo(path)/addBlock(page, blockDef, overlayHandle?)read and add blocks (path is dotted through nested overlays byhandle, or 1-based position for a block with no handle, e.g."tier_grid.2.price_text");blockDefis the same flat shape as a placement.addBlockinto a grid overlay is refused; usesetCells. getBlocks(baseName)-> every block sharing that handle (item,item~2, ...) asgetBlockInfoobjects.setVisible(path, bool)hides or shows a block (it keeps its layout space). Grid cells:getCount(grid),getCells(source, start, count),setCells(grid, start, cells), andorderCells/orderOrows/orderOcols(grid, start, end, direction)("asc"= cells without data first,"desc"= data first).getBlockBounds(page)->{x, y, width, height}mm, ornullif empty - the union rect of that page's blocks resolved so far.getPageSize(page)->{width, height}mm - that page's own resolved size (reflects asetPageHeightoverride).getPdfPageSize(file, pdfPage)->{width, height}mm, ornull- a PDF resource's natural page size, independent of anyaddPage()override - useful for sizing/positioning content to match a real page without hardcoding its dimensions.getProject()->{name, description, notes, categories, defaultWidth, defaultHeight, defaultBackgroundColor}- read-onlyproject.ypfmetadata.listAssets()->{images, fonts, pdfs}- the project's own files actually present, by category - check before referencing an asset you aren't certain exists.getFormFields()->[{id, type, label, fields}]-form.json's own field list, for a script that wants to stay generic instead of hardcoding field ids.getImageDimensions(file)->{width, height}px, ornull.formatNumber(value, decimals),formatCurrency(value, currencyCode),formatDate(value, pattern)- plain JS helpers (not locale-aware) for display formatting, an alternative to hand-rolling it in a block's own{...}template.
Calling getBlockBounds/getPageSize/getPdfPageSize anywhere may cause
process.js to silently run twice (a two-pass mechanism: pass 1 always
gets null/an empty result back from all three; pass 2 re-runs the whole
script with every real, resolved value seeded in) - harmless, but don't
rely on a side effect (e.g. a counter, a console.log) that isn't safe to
happen twice.
Text/data fields on blocks use {bracket} expressions - a bare field name
like {item_name} is shorthand for inputs.item_name; anything else
({inputs.price * 1.08}) is evaluated as real JavaScript. A block's own
condition key is a separate, restricted formula language (not
JavaScript) gating whether the block renders at all - bare field names or
inputs['field_id'] bracket syntax only, no dot notation.
Worked example (every field type, every block type)
project.ypf
{
"version": 1,
"name": "Product Info Sheet",
"description": "One-page info sheet for a single catalog item.",
"notes": "",
"page": {
"width": 215.9,
"height": 279.4,
"background_color": "#FFFFFF"
}
}
form.json
{
"title": "Product Info Sheet",
"version": 1,
"presets_enabled": true,
"fields": [
{ "id": "item_name", "type": "text", "label": "Item Name",
"required": true, "max_length": 60, "autocomplete_key": "item_name" },
{ "id": "unit_price", "type": "currency", "label": "Unit Price",
"required": true, "min": 0 },
{ "id": "quantity", "type": "number", "label": "Quantity in Stock",
"min": 0, "max": 9999 },
{ "id": "restock_date", "type": "date", "label": "Next Restock Date" },
{ "id": "condition", "type": "dropdown", "label": "Condition",
"options": ["New", "Used", "Refurbished"] },
{ "id": "in_stock", "type": "checkbox", "label": "Currently In Stock" },
{ "id": "photo", "type": "image", "label": "Product Photo",
"max_size_px": 1600 },
{ "id": "approved_by", "type": "signature", "label": "Approved By" },
{ "id": "damage_diagram", "type": "drawing", "label": "Damage Diagram",
"background": "diagram.png" },
{ "id": "line_items", "type": "repeating-group", "label": "Included Accessories",
"min_items": 0, "max_items": 10,
"fields": [
{ "id": "description", "type": "text", "label": "Description", "required": true },
{ "id": "amount", "type": "currency", "label": "Amount" }
] },
{ "id": "notice", "type": "label", "label": "Prices valid while supplies last.",
"font_size": 9, "color": "#666666" }
]
}
Field types: text, number, currency, date, dropdown, checkbox,
image, signature, drawing, repeating-group, label (display-only,
never appears in inputs), color (value is a "#RRGGBB" string).
format on a validation rule is inert - do all display formatting in
overlay.canvas text templates instead. There is no computed array - a
derived value is assigned onto inputs in validate.js/process.js
instead (see process.js below). Any field can carry "hidden": true -
skipped by the Input Form's own rendering, but still fully participates in
inputs/validation/presets; the only way a preset can drive a value the
pipeline knows about without a user-facing control for it.
process.js
addPage({ width: 215.9, height: 279.4, backgroundColor: "#FFFFFF" });
overlay("overlay.canvas");
overlay.canvas
{
"layout": "canvas",
"placements": [
{
"type": "text",
"x": 10, "y": 10, "width": 150, "height": 12,
"text": "{item_name}",
"font_size": 18, "font_weight": "bold", "font_family": "Lato",
"color": "#000000", "align": "left", "valign": "top", "overflow": "visible"
},
{
"type": "text",
"x": 10, "y": 24, "width": 100, "height": 8,
"z_index": 1,
"condition": "inputs.in_stock == true",
"text": "Price: {inputs.unit_price}",
"font_size": 12, "color": "#1a1a1a", "align": "left", "valign": "top",
"stroke_width": 0.2, "stroke_color": "#ffffff", "stroke_style": "outside"
},
{
"type": "image",
"x": 140, "y": 10, "width": 60, "height": 60,
"source": "inputs:photo",
"fit": "contain"
},
{
"type": "box",
"x": 10, "y": 36, "width": 100, "height": 30,
"background": "#f4f4f4", "border_color": "#cccccc",
"border": 0.3, "corner_radius": 2,
"rotation": 5
},
{
"type": "scan_code",
"x": 165, "y": 75, "width": 30, "height": 30,
"data": "{item_name}", "format": "qr_code",
"color": "#000000", "show_value": false
},
{
"handle": "accessories", "type": "overlay",
"x": 10, "y": 90, "width": 150, "height": 40,
"content": {
"layout": "grid",
"size_from_block": true,
"orows": 10, "ocols": 2,
"repeating_group_id": "line_items",
"child_padding": 1.5,
"placements": [
{
"type": "text",
"width": 110, "height": 4,
"text": "{inputs.description}", "font_size": 10
},
{
"type": "text",
"width": 40, "height": 4,
"text": "{inputs.amount}", "font_size": 10, "align": "right"
}
]
}
},
{
"type": "image",
"x": 10, "y": 140, "width": 60, "height": 20,
"source": "inputs:approved_by", "fit": "contain"
},
{
"type": "image",
"x": 10, "y": 165, "width": 100, "height": 50,
"source": "inputs:damage_diagram", "fit": "contain"
}
]
}
Block types: text (word_wrap: true for real word-wrap; without it, a
literal \n inside the evaluated string still renders as a manual line
break - write \n (backslash, n) in the JSON text, since a raw newline is
invalid JS inside a string), image (source a plain string - "assets:x" for a static
file, "inputs:x" to bind to an image/signature/drawing field's value - all three
resolve to a captured PNG file path), box, scan_code, pdf, overlay
(nested layout: file names a .canvas/.grid file, or content holds an
inline one; a grid overlay with repeating_group_id repeats its child
placements once per record of that field, automatically, when process.js's
overlay() places it - the way to lay out a repeating group as a table). A grid's own content/.grid keys: orows/ocols
(orows = lanes, ocols = cells along the flow in each lane; a child past capacity is dropped), gap/gap_y (mm between cells),
gap_color/gap_color_y (fills the gaps as gridlines; gap_color_y defaults
to gap_color), ocol_sizes/orow_sizes (explicit track sizes listed from the fill origin, e.g.
"20mm 1rem 2rem"), fill_origin (top_left, top_right, bottom_left,
bottom_right) and fill_axis (horizontal/vertical), min_orows/min_ocols,
overflow_orows, child_padding/child_border/... (defaults for children
that don't set their own). It lays out inside the overlay block's own
content rect - a border/padding on that block insets the grid, and a gap
line runs out to meet the border across that padding. Every placement is one flat object: the block's own
properties (text formatting, background, border, corner_radius, ...
directly on it - there is no style object) plus x/y/width/height
(canvas) or width/height and cell_* keys (grid). Common to every
block: an optional handle (lowercase alphanumeric with single _/-
separators, optional ~N, never all digits, never doc; required on an
overlay block; duplicates among siblings become name~2, name~3; a
block with no handle is addressed by its 1-based position), visible
(bool, default true), condition, z_index, rotation (visual only, never
changes the bounds).
validate.js
// Field-specific error.
if (inputs.unit_price <= 0) {
errors["unit_price"] = "Unit price must be greater than zero.";
}
// A computed value: assign onto inputs (mutable here, same contract
// process.js has) - validated and made available to process.js/every
// block in one place, not a form.json field.
inputs.price_with_tax = inputs.unit_price * 1.08;
if (inputs.price_with_tax > 10000) {
errors["unit_price"] = "Price with tax can't exceed $10,000.";
}
// Form-level error - errors[""], not tied to any one field.
if (inputs.in_stock && inputs.quantity == 0) {
errors[""] = "Marked in stock but quantity is 0 - check one or the other.";
}
presets.json (optional - named default-value snapshots; a preset
literally named "Default", case-insensitive, auto-applies to a freshly
opened form)
{
"Default": {
"item_name": "Wireless Mouse",
"unit_price": 24.99,
"quantity": 50,
"condition": "New",
"in_stock": true,
"line_items": [
{ "description": "USB Receiver", "amount": 0 }
]
}
}
image and label fields are never included in a preset (a real file
picker interaction is always required for image; label collects
nothing). signature/drawing fields may be, as a data:image/png;base64,...
value.
autocomplete.json (optional - a field opts in via its own
autocomplete_key)
{
"item_name": ["Wireless Mouse", "Wireless Keyboard", "USB-C Hub", "Laptop Stand"]
}
Output format: one fenced code block per file, each preceded by
### <filename>. Always include project.ypf and process.js. Only
include validate.js/presets.json/autocomplete.json/extra partials if
the project actually needs them. No prose inside the code fences. After the
files, note in one short paragraph whether you used a synthesized canvas or
a real PDF resource file for the page(s) (size/orientation/count either
way), plus any other assumptions made.
Describe your project: