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 - required name, plus page size/background that a blank-canvas addPage() call falls back to for any of width/height/ backgroundColor it omits (real page geometry still comes entirely from process.js's addPage() calls, below).
  • form.json - the fields a user fills out.
  • overlay.canvas (and optionally other .canvas layout partials, each with a mandatory "layout": "canvas" and a placements list) - blocks drawn onto a page from field values.
  • process.js - required, no fallback. Declares every output page via addPage({width, height, backgroundColor}) (blank canvas) or addPage({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 via overlay("overlay.canvas") (and any other partial, each optionally targeting a page number). Zero addPage() calls is a hard error. A variable-length page (e.g. a receipt) can call setPageHeight(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 declared height, not what it actually renders at, so give a grid overlay block (or a wrapped text block) a generous height approximating 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 own required/min/max/min_length/max_length/regex in form.json can already express, put there instead - don't duplicate it here. Two globals: inputs (same submitted-values map, mutable - see below) and errors, a plain object, not an array (errors.push(...) throws) - assign errors["field_id"] = "message" for a specific field, or errors[""] = "message" (or an array of strings) for a general error that isn't any one field's fault. Leave errors untouched when there's nothing to report; the script's return value is ignored, only errors's final state matters. inputs is mutable here, same read/write contract process.js has (see below) - assigning a new or existing property is how a computed value gets both validated and used, carrying forward into process.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 (y omitted is x): 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 an overlay block's transform.
  • A grid overlay block that has repeating_group_id is expanded once per record automatically when overlay() places it (no call); it stays pending until the script ends, so setCells(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 by handle, or 1-based position for a block with no handle, e.g. "tier_grid.2.price_text"); blockDef is the same flat shape as a placement. addBlock into a grid overlay is refused; use setCells.
  • getBlocks(baseName) -> every block sharing that handle (item, item~2, ...) as getBlockInfo objects. 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), and orderCells/orderOrows/orderOcols (grid, start, end, direction) ("asc" = cells without data first, "desc" = data first).
  • getBlockBounds(page) -> {x, y, width, height} mm, or null if empty - the union rect of that page's blocks resolved so far.
  • getPageSize(page) -> {width, height} mm - that page's own resolved size (reflects a setPageHeight override).
  • getPdfPageSize(file, pdfPage) -> {width, height} mm, or null - a PDF resource's natural page size, independent of any addPage() override - useful for sizing/positioning content to match a real page without hardcoding its dimensions.
  • getProject() -> {name, description, notes, categories, defaultWidth, defaultHeight, defaultBackgroundColor} - read-only project.ypf metadata.
  • 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, or null.
  • 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: