FacetDocs

Agent card

Everything most builds need, in the order you need it. The other pages go deeper, and the full documentation holds all of them in one file.

Commands

facet --doc f.facet build @screen.json --preview s.png   # build, then read every warning
facet --doc f.facet source Screen                        # a layer, in the language
facet --doc f.facet build @screen.json --sync            # change in place
facet --doc f.facet modify Card '{"fill": "@paper"}'     # one or two values
facet --doc f.facet tree Screen                          # ids, sizes, places
facet --doc f.facet inspect 42                           # every property of one layer
facet --doc f.facet shot Screen -o s.png --scale 2       # render any layer
facet --doc f.facet undo                                 # each call is one undo step
facet --doc f.facet close                                # save and stop the host

--doc and the path are two arguments. A file the app has open is edited live in the app; add --headless after the path for a host of your own. @file.json reads a file; inline JSON works too.

A build call

A node, a list of nodes, or {"kit": {...}, "nodes": [...]}. Send the kit once per file; it is saved with the file and every later call names it.

card-start.json≈ 398 tokens
{
  "kit": {
    "font": "Inter 15/22",
    "colors": {
      "ink": "#1A1423",
      "paper": "#FBF7F0",
      "rose": "#D9485F",
      "mute": "#6B6470"
    },
    "type": {"title": "Inter 22/28 700", "cap": "Inter 12/16 600 +0.4 caps"},
    "parts": {
      "chip": {
        "params": {"label": "", "tone": "@rose"},
        "node": {
          "row": [{"text": "{label}", "color": "{tone}"}],
          "padding": [2, 8],
          "cornerRadius": 8,
          "fill": "{tone}/12%"
        }
      }
    }
  },
  "nodes": [
    {
      "name": "Card",
      "width": 360,
      "padding": 16,
      "gap": 8,
      "fill": "@paper",
      "color": "@ink",
      "cornerRadius": 16,
      "column": [
        {"title": "Today"},
        {
          "row": [
            {"cap": "Raag Yaman"},
            {"space": "fill"},
            {"chip": "Evening"}
          ],
          "width": "fill",
          "align": "center"
        },
        {
          "text": "Long text wraps at the card's width and takes the height of its lines.",
          "width": "fill",
          "color": "@mute"
        },
        {"icon": "lucide:clock", "size": 16, "color": "@mute"}
      ]
    }
  ]
}
What card-start.json builds

A kit and a card in one call. Every later call names @ink, title and chip without sending them again.

Built by the docs build with the real CLI.

  • Strings are texts. Containers pass font, style and color down to every text inside.
  • Kit colors are variables: @ink, a tint @ink/40%, one value per mode with "modes": ["Light", "Dark"] and "mode": "Dark" on a layer.
  • Type presets are text styles: {"title": "Today"} is a text in title; "style": "cap" sets a container's default.
  • Parts take parameters: {"chip": "Evening"} (first parameter) or {"chip": {"label": "Late", "tone": "@mute"}}. {param} alone takes the value as it is (a node, a list); inside longer text it is text. Parts use parts and pass their parameters on: {"avatar": {"initials": "{initials}"}} inside member. A children parameter takes the children written at the use ("{children}"). A list parameter defaulting to [], written alone among children ({"row": [{"text": "{who}"}, {"space": "fill"}, "{badges}"]}), adds nothing when left out and its nodes when given: {"member": {"who": "Ben", "badges": [{"chip": "Blocked"}]}}.
  • Names: a key that is not a language key is a preset, then a part, then a component. Keys win, so never name a preset or part name, text, size, row or any other key. A preset and a part cannot share a name. {"kit": {"parts": {"chip": null}}} removes one.
  • Font shorthand: "Family Size/Line Weight" plus italic, tracking +0.4 or -2%, a case caps. Weights are 100 to 900 in hundreds, or light, medium, semibold, bold and the rest. "17/26" keeps the family; "600" changes only the weight. Any Google Font downloads on first use. Change a preset in the kit and every text in it follows at once.

Layout

WriteFor
{"row": [...]}, {"column": [...]}auto layout; gap, padding (number, [v, h], [t, r, b, l]), align (cross: start, center, end, stretch), justify (start, center, end, space-between)
{"grid": [...], "columns": 2}rows of columns ([120, "fill"] sizes them)
{"stack": [...]}children overlap, centered; place each with at
{"ring": [...], "r": 80}children around a circle
"width", "height"a number, "fill" (the room the flow leaves), "hug" (default for containers); "size": 40 or [w, h]
{"space": "fill"}, {"space": 12}push siblings apart, or a fixed gap
"at"[x, y], [x, y, "center"], "bottom right", ["top right", -8, 8] (x right, y down), {"angle": 90, "r": 60}; takes the layer out of the flow
"grow": 1take the leftover room along the flow

Never compute positions: at with a place word, an anchor or an angle does it.

Look

fill (a color, "none", or a gradient {"linear": ["@rose", "@ink"], "angle": 160}), "$image": "photo:latte art" or a path for a picture, stroke and strokeWidth, cornerRadius (number or [tl, tr, br, bl]), opacity, effects ({"type": "drop-shadow", "x": 0, "y": 4, "blur": 12, "color": "#0000001A"}), overflow: "hidden", rotation. Texts: text, font, style, color, textAlign, maxLines: 1 (one line with an ellipsis), "type": "paragraph" with "runs": ["Due ", {"chip": "Fri"}, " at noon."] for chips inside a sentence.

{"icon": "lucide:bell", "size": 20, "color": "@mute"}: any Iconify set; each name becomes a component in an Icons frame, every use an instance. {"draw": [{"circle": [20, 20, 18], "stroke": "@rose", "weight": 3}], "size": 40} draws shapes (circle, ellipse, rect, line, poly, arc, path).

Repeat

{"each": ["Mon", "Tue", "Wed"], "do": {"chip": "{.}"}}: {.} the item, {field} a field of an object item, {i} its index, {#} its number. {"each": 5, "do": ...} repeats a count.

Components

card-set.json≈ 283 tokens
{
  "nodes": [
    {
      "set": "Plan",
      "props": {"Price": "$0", "Note": "Billed monthly"},
      "variants": {"Tier": ["Free", "Pro"]},
      "template": {
        "name": "Plan",
        "width": 240,
        "padding": 16,
        "gap": 6,
        "fill": "#FFFFFF",
        "column": [
          {"title": "{Tier} plan"},
          {"text": "{Price}", "name": "Price"},
          {"text": "{Note}", "name": "Note", "width": "fill"},
          {"chip": "Popular", "only": "Pro"}
        ]
      },
      "x": 0,
      "y": 0
    },
    {
      "name": "Plans",
      "x": 0,
      "y": 260,
      "gap": 12,
      "row": [
        {"Plan": {"Tier": "Free"}},
        {
          "Plan": {"Tier": "Pro", "Note": "Billed yearly"},
          "name": "Pro card"
        }
      ]
    }
  ]
}
What card-set.json builds

A set with a property per text, a variant property shown in a text, a layer only one variant holds, and two instances in the same call.

Built by the docs build with the real CLI · built after card-start.json.

  • "{Prop}" binds a text to a property; "visible": "{Show}" binds visibility to a boolean one. "{Tier} plan" names a variant property, so each variant shows its own value.
  • only keeps a layer to some variants. A variant's map changes it: {"Tier": {"Free": {}, "Pro": {"fill": "#F3EFF7", "props": {"Price": "$12"}}}}.
  • A prop every variant sets for itself cannot be set by instances; the build warns.
  • Give exposed texts "width": "fill" (or a number) so longer values wrap rather than widen the component.
  • Instances: {"Plan": {"Tier": "Pro", "Note": "Billed yearly"}, "name": "Pro card"}. Variant property keys choose the variant; the rest set properties. One call can hold the set and the screen that uses it.
  • Behavior on a set ("behavior": {"start", "states", "axes", "plays", "targets"}) is stored as a feature named after the set (plan); facet chart plan checks it. See Behavior.

Changing what is there

ChangeDo
a value or twomodify NODE '{...}'; $nodes reaches named descendants: {"$nodes": {"Title": {"text": "Tue"}}}
a treeedit the JSON you built it from, then build --sync: matched layers keep ids and instances; the reply counts what changed
a component setbuild @set.json --replace: instances follow to the new variants and keep what they set
add one layerbuild @row.json --parent ID appends it last; it takes the font and color the parent's texts share

Name the layers you will change, or give repeated ones a key: sync matches by key or name, and same-named siblings in order. source prints a layer in the language with parts expanded; for a screen built from parts, keep editing your own JSON.

Checks

Every write verifies itself and reports, by layer, what did not happen: NOT_APPLIED, INPUT_IGNORED (a key not read, with the nearest one), LAYOUT_OVERFLOW, LAYOUT_OCCLUDED, TEXT_TRUNCATED, FONT_SUBSTITUTED, TOKEN_FALLBACK. Fix each one; the app shows them as badges. State what must hold instead of looking: "expect": {"lines": 1}, {"fits": true}, {"noOverlap": true}, {"width": {"max": 320}}; a miss is EXPECT_FAILED.

Look at the --preview PNG when the result matters visually. It renders the top-level layers the call built or synced, whole, not the page.

Traps

  • Frames have no fill until you give one; "fill": "none" removes one (null is not a value).
  • "fill" width needs a row, column or grid parent; a top-level layer gets a number.
  • A text in a stack or with at keeps its own width; give it a width to wrap.
  • Overlapping top-level frames hide each other: put component sets to the side ("x": 1400).
  • "♈︎" draws a symbol as text; without U+FE0E some symbols draw as color emoji.