Documentation

Boceto is a tiny DSL for hand-drawn wireframes. Write a few lines inside a fenced markdown code block, get a styled diagram. This page documents every construct in the language.

The DSL has seven keywords. That's all of it:

KeywordWhat it does
elementDraw a UI element (one of 83 built-in types) or call a component.
textShorthand for a text label.
arrowConnect two elements by id.
row / colOpen a layout container that arranges its children.
componentDefine a reusable composite.
endClose the most recent row, col, or component block.

File embedding

Boceto content lives inside markdown fenced code blocks. The info string starts with boceto and may include an optional page name after a colon:

```boceto:Login Screen
element navbar 0 0 600 44 "MyApp"
element heading 100 80 400 28 "Welcome back"
```

Coordinates are pixels in page-space. Top-left is (0, 0). Multiple boceto blocks in the same document become multiple pages of one diagram.

Your first diagram

Show source
element navbar 0 0 900 44 "MyApp"
element heading 280 90 340 32 "Welcome back" fontSize=28
element label 280 130 340 22 "Sign in to continue"
element input 280 170 340 36 "Email"
element primary-button 280 210 340 36 "Sign In"

element

The core statement. Draws one UI element.

element TYPE[#id] X Y W H "label" ["note"] [key=value ...]
  • TYPE — one of 83 element types (see Elements).
  • #id — optional named identifier (used by arrow to reference this element).
  • X Y W H — pixel coordinates and size. Required positionally, even inside a row/col (the container will override X and Y; write 0 0).
  • "label" — quoted text displayed on the element. Use "" if blank.
  • "note" — optional sticky-note annotation.
  • key=value — type-specific attributes (e.g. fontSize=18, rows=5) and layout attrs when inside a flex container.

text

Shorthand for a label element with implicit sizing.

text X Y "content" [w=N h=N fontSize=N ...]

Equivalent to element label X Y 200 24 "content" with the same attributes.

arrow

Connect two elements by id. The arrow draws from the midpoint of the source's bottom edge to the midpoint of the target's top edge.

arrow FROM_ID TO_ID ["optional label"]
Show source
element button#a 100 40 120 40 "Start"
element box#b 380 100 200 80 "Process"
element button#c 660 40 120 40 "Done"
arrow a b
arrow b c "completes"

Attributes (key=value)

Any element accepts trailing key=value tokens. Unknown attributes are preserved verbatim — implementations may consume them, others pass them through. Common attributes:

AttributeMeaning
idNamed id (alternative to TYPE#id shorthand).
fontSizeOverride text size in pixels.
type-specifice.g. rows, cols on table; progress on progress.
flex attrsSee child flex attributes when inside a row/col.

Named IDs

Two equivalent forms; the shorthand is preferred for round-trip diffs.

element button#save 0 0 100 30 "Save"
element button 0 0 100 30 "Save" id=save

IDs must match [A-Za-z][A-Za-z0-9_-]*. Without an id, the parser auto-generates an opaque one.

Text rendering

Every text-bearing element honors four generic attributes that control how its label fits inside the bounding box: overflow, align, fontSize, and minFontSize. Quoted labels also accept \n for hard line breaks. Defaults are sensible per element (a button ellipsizes; a heading wraps) — these attributes only matter when you want to override the default.

Multi-line labels (\n)

Inside a quoted token, \n is interpreted as a newline. Renderers like textarea, alert, and chat-bubble respect those line breaks before word-wrapping each segment. \t is also recognised for tabs; \" and \\ remain literal quote and backslash.

overflow

Four modes. ellipsis trims to a single line with a trailing . wrap word-wraps onto multiple lines, capped by the box height. clip hard-clips at the box edge. shrink binary-searches a smaller font size between minFontSize (default 9) and fontSize until the label fits.

textAlign

textAlign=left | center | right overrides the per-element default horizontal alignment. In wrap mode, every line uses the same anchor. (The attribute is named textAlign rather than align to avoid collision with the flex container's cross-axis align attribute, which uses a different value set.)

fontSize + minFontSize

fontSize sets the requested point size (it is the upper bound when overflow=shrink). minFontSize caps how small shrink is allowed to go; the default is 9. Both attributes are ignored on modes that aren't shrink (except fontSize, which always sets the requested size).

Layout: row and col

A row arranges children left → right. A col arranges them top → bottom. The grammar mirrors element:

row[#id] X Y W|auto H|auto [attrs...]
  <child statements>
end

col[#id] X Y W|auto H|auto [attrs...]
  <child statements>
end
Show source
row 20 20 860 80 gap=12 align=middle
  element button 0 0 100 36 "Save"
  element button 0 0 100 36 "Cancel"
  element button 0 0 100 36 "Reset"
end
Children write 0 0 for X/Y. The grammar requires positional tokens, but the container overrides X/Y. Width and height become the child's preferred size.

Distribution: justify and align

justify spreads children along the main axis; align positions them on the cross axis.

Show source
row 20 20 860 60 justify=between align=middle
  element button 0 0 100 36 "Back"
  element heading 0 0 200 28 "Settings"
  element primary-button 0 0 100 36 "Save"
end

Valid justify: start, middle, end, between, around, evenly. Valid align: start, middle, end, stretch.

Sizing: auto, grow, min/max

Container auto

Use auto for W or H to shrink-wrap the container around its content.

Show source
row 20 20 auto auto padding=12 gap=8 align=middle
  element heading 0 0 200 28 "Toolbar"
  element button 0 0 80 32 "Save"
  element button 0 0 80 32 "Cancel"
end

Child grow

A child with grow=1 takes any leftover main-axis space. Two siblings with grow=1 each split the remainder evenly.

Show source
row 20 20 860 44 gap=8 align=middle
  element button 0 0 80 32 "Back"
  element input 0 0 0 32 "Search…" grow=1
  element button 0 0 80 32 "Filter"
end
0 means "no preferred size". A child with w=0 grow=1 reads as "I have no width preference; give me the remaining space." An actual zero-sized element wouldn't render, so the overload is unambiguous.

Constraints: min-w / max-w / min-h / max-h

Guardrails on a child's size. min-w is essential for wrap-as-grid layouts — it sets the threshold at which children break to the next line.

Wrap as grid

wrap=wrap on a row pushes overflowing children to a new line. Combined with min-w + grow=1 on the children, you get a responsive grid without leaving the row syntax.

Show source
row 20 20 860 180 gap=12 wrap=wrap align=start
  element card 0 0 0 80 "" grow=1 min-w=180
  element card 0 0 0 80 "" grow=1 min-w=180
  element card 0 0 0 80 "" grow=1 min-w=180
  element card 0 0 0 80 "" grow=1 min-w=180
  element card 0 0 0 80 "" grow=1 min-w=180
end

Elements as containers

Any element type can host children via the same block-form syntax as composites. A trailing : on the element line opens a children block, closed by end. The element's chrome (border, header, etc.) renders normally, and the children render inside the content rect.

Two modes, same trigger rule as composite shells:

  • Flex container — set direction=row|col; children flow with full gap / padding / justify / align / wrap support.
  • Absolute body — omit direction; children render at their declared local (x, y) inside the content rect.
Show source
element box 0 0 400 auto "" direction=col padding=12 gap=8 align=stretch :
  element heading 0 0 0 24 "In a box"
  element label 0 0 0 18 "With a flex layout"
end

Element types that draw header chrome (card, modal, window-frame, browser-frame, phone-frame) get an intrinsic content inset added to your padding so children land below the chrome automatically — no manual padding-top=40 trick needed:

Show source
element modal 80 20 400 auto "Confirm action" direction=col padding=12 gap=12 align=stretch :
  element label 0 0 0 18 "Are you sure you want to continue?"
  row 0 0 0 36 gap=8 justify=end
    element button 0 0 100 32 "Cancel"
    element primary-button 0 0 100 32 "Confirm"
  end
end

Generic border + shadow

Any element accepts optional border and shadow attributes — independent of block-form children.

  • border=true — 1.5px default stroke. border=3 — 3px stroke. border=#3b82c4 — colored stroke. border=false — none (default).
  • shadow=true — default drop shadow. shadow=12 — 12px blur. shadow=false — none (default).
Show source
element box 40 40 220 100 "Plain"
element box 280 40 220 100 "Bordered" border=#3b82c4 shadow=8
element button 540 40 100 36 "Floating" shadow=14

Alignment tips

A few rules that keep diagrams looking sharp:

  • Stacked sections should share their padding. If a header uses padding=12 and a body uses padding=16, their titles sit at different left edges. Pick one padding value per layout.
  • Use align=stretch when children should fill the cross axis. The default cross-axis behavior doesn't stretch.
  • Use 0 for "flex decides". Pairs with grow, basis, or align=stretch.
  • Use auto for "size to content". A container with h=auto grows to fit the tallest child. A call site auto auto uses the component's declared default if set.

component

Define a reusable composite once and reference it many times — like a function whose parameters fill in labels and attribute values.

component NAME[(p1, p2, ...)] [layout-attrs] [size-defaults]
  <body statements>
end

The body contains the same statements as a page: element, text, arrow, row, col, and other component references. Inside the body, $name and ${name} are placeholders for parameters; they're substituted at every call site.

To reference a component, use element NAME ...:

Show source
component UserCard(name, role)
  element card 0 0 240 80 ""
  element avatar 8 8 40 40 ""
  element heading 56 12 180 24 "$name"
  element label 56 38 180 18 "$role"
end

element UserCard 20 20 240 80 "" name="Jane Doe" role="Admin"
element UserCard 280 20 240 80 "" name="John" role="User"

Responsive shells

Add direction=row or direction=col to the component header and the body becomes flex-laid-out inside an implicit shell sized to the call site. The same component can then be dropped at a fixed size, into a row with grow=1, or into a wrap-grid — and the body re-flows every time.

Show source
component Panel(title) direction=col align=stretch padding=16 gap=8 min-w=200 max-w=400
  element heading 0 0 0 24 "$title"
  element box 0 0 0 0 "" grow=1
end

# fixed
element Panel 20 20 280 120 "" title="Fixed"

# grow inside a row
row 20 160 860 120 gap=12 align=stretch
  element box 0 0 200 0 "" id=sidebar
  element Panel 0 0 0 0 "" grow=1 title="Grows"
end

# wrap grid
row 20 300 860 40 gap=12 wrap=wrap
  element Panel 0 0 0 0 "" grow=1 title="A"
  element Panel 0 0 0 0 "" grow=1 title="B"
  element Panel 0 0 0 0 "" grow=1 title="C"
end

The component header accepts every row/col attribute (gap, padding, align, justify, wrap) plus size defaults (w, h, min-w, min-h, max-w, max-h) and per-instance flex defaults (grow, shrink, basis, align-self).

Call-site values override component defaults. Use auto at the call site to fall back to the default; use 0 at the call site to defer to the parent container's layout.

Absolute-body components

Omit direction on the header and the component keeps an absolute body: body coordinates are relative to the instance origin, and the body does not re-flow when the instance is resized. Useful for pixel-precise widgets you want identical wherever they land.

Slots — composite children

A component body may include slot markers that designate where call-site children render. A trailing : on the element <Composite> ... line opens a children block, closed by end.

Two flavors:

  • Anonymous default slot — declared with bare slot; filled by the call site's bare children.
  • Named slots — declared with slot NAME; filled by slot NAME … end sub-blocks at the call site.
Show source
component Card(title) direction=col align=stretch padding=12 gap=8 w=300
  slot header
  element heading 0 0 0 24 "$title"
  slot
end

element Card 20 20 320 auto "" title="Members" :
  slot header
    element label 0 0 0 16 "Showing 3 of 24"
  end
  element label 0 0 0 18 "Alice"
  element label 0 0 0 18 "Bob"
  element label 0 0 0 18 "Mei"
end

Rules:

  • The single-line call form (no :) is still valid — slots default to empty.
  • Passing bare children when the component has no anonymous slot is a parse error.
  • Passing slot NAME the component doesn't declare is a parse error.
  • A slot name may appear at most once per call site.
  • Built-in element types (element box, element button, …) do not accept children — only composite references do.

Common patterns

App shell — fixed chrome, flexible main

Header and Footer declare h=N; Main uses grow=1 to fill the rest. The shared padding across all three is what makes their content edges line up.

Show source
component Header(title) direction=row align=middle padding=16 gap=8 h=56
  element heading 0 0 0 28 "$title" grow=1
  element button 0 0 80 32 "Action"
end

component Footer() direction=row align=middle padding=16 h=44
  element label 0 0 0 16 "© 2026" grow=1
  element label 0 0 120 16 "v1.0.0"
end

component Main() direction=col align=stretch padding=16 gap=12 grow=1
  element heading 0 0 0 24 "Welcome"
  element box 0 0 0 0 "" grow=1
end

component AppShell(title) direction=col align=stretch
  element Header 0 0 0 0 "" title="$title"
  element Main 0 0 0 0 ""
  element Footer 0 0 0 0 ""
end

element AppShell 20 20 860 340 "" title="My App"

Chrome sized to content (h=auto)

Drop h=auto on the header and it grows with its tallest child.

Show source
component DynHeader(title) direction=row align=middle padding=16 gap=8 h=auto
  element heading 0 0 0 32 "$title" grow=1
  element button 0 0 100 40 "Save"
  element button 0 0 100 40 "Cancel"
end

component Main() direction=col align=stretch padding=16 gap=12 grow=1
  element heading 0 0 0 24 "Content area"
  element box 0 0 0 0 "" grow=1
end

component Shell(title) direction=col align=stretch
  element DynHeader 0 0 0 0 "" title="$title"
  element Main 0 0 0 0 ""
end

element Shell 20 20 860 240 "" title="Sized to content"

Sidebar + main, two columns

Show source
component Sidebar() direction=col align=stretch padding=16 gap=8 w=200
  element heading 0 0 0 20 "Menu"
  element button 0 0 0 32 "Home"
  element button 0 0 0 32 "Settings"
  element button 0 0 0 32 "Profile"
end

component Main() direction=col align=stretch padding=16 gap=8 grow=1
  element heading 0 0 0 24 "Dashboard"
  element box 0 0 0 0 "" grow=1
end

component TwoCol() direction=row align=stretch
  element Sidebar 0 0 0 0 ""
  element Main 0 0 0 0 ""
end

element TwoCol 20 20 860 220 ""

Modal — fixed width, content-driven height

Show source
component Modal(title) direction=col align=stretch padding=16 gap=12 w=400 h=auto
  element heading 0 0 0 24 "$title"
  element label 0 0 0 18 "Are you sure you want to continue?"
  element input 0 0 0 36 "Reason (optional)"
  row 0 0 0 36 gap=8 justify=end
    element button 0 0 100 32 "Cancel"
    element primary-button 0 0 100 32 "Confirm"
  end
end

element box 0 0 900 280 "" id=backdrop
element Modal 250 40 400 0 "" title="Confirm action"

Web components

Boceto ships two framework-agnostic custom elements you can drop into any HTML page. <boceto-view> is a read-only renderer; <boceto-edit> is an interactive editor. Both share the same code attribute as the source of truth — set it once, hand-edit it later, or wire it up to the editor and watch the markup update on every commit.

Install

Two equivalent forms — pick whichever fits your build:

// ESM — bundler or modern browser with an importmap.
import '@boceto/view/auto'
import '@boceto/edit/auto'

// Or via plain <script> tags pointing at the pre-built bundles
// (each bundle inlines its own copy of @boceto/core + yoga-layout):
<script type="module" src="./assets/boceto-view.js"></script>
<script type="module" src="./assets/boceto-edit.js"></script>

The /auto entry point is tree-shake-safe; it only registers the custom element with customElements.define. If you want manual control over the tag name, import the constructor and call defineBocetoView() / defineBocetoEdit() yourself (each accepts an optional tag override).

<boceto-view> — read-only renderer

Paints a Boceto document onto a <canvas> inside a shadow root. Re-renders whenever any observed attribute changes; emits boceto-render when a paint completes.

<boceto-view width="600" height="200" code='```boceto
element navbar 0 0 600 44 "MyApp"
element heading 100 80 400 28 "Hello, Boceto"
```'></boceto-view>

Attributes

AttributeTypeMeaning
codestringInline DSL source. Overrides slot text content.
srcURLFetched on connect/attribute change if code is absent.
widthintegerCanvas pixel width. Default 860.
heightintegerCanvas pixel height. Default 600.
pageid / name / indexWhich page to render in a multi-page doc.
fitfixed | contentcontent grows the canvas to encompass every element; width / height become minimums. Default fixed.
paddingintegerBreathing room around content when fit="content". Default 16.

Events

EventDetailFires on
boceto-render{ doc, page }Every successful paint.

Auto-fit example

Set fit="content" when the doc's size is data-driven or you don't know the bounding box in advance. The canvas grows automatically:

<boceto-view width="200" height="100" fit="content" padding="20" code='...'></boceto-view>

Loading from a URL

<boceto-view src="./scenes/login.boceto" width="600" height="320"></boceto-view>

<boceto-edit> — interactive editor

A thin DOM adapter around a framework-agnostic BocetoEditor controller. Click / drag / resize on the canvas, multi-select, undo / redo, right-click for a context menu, double-click to rename. Selection state, geometry mutations, and history are all routed through the controller, which is exposed as el.editor for programmatic use.

<boceto-edit width="600" height="280" code='...'></boceto-edit>

Attributes

AttributeTypeMeaning
codestringSource of truth. External writes call editor.setCode(); commits emit change.
srcURLFetched on connect if code is absent.
width / heightintegerInitial canvas size. Once the host gets a non-zero CSS size, it tracks that instead (drag a parent with resize: both to grow the canvas live).
pageid / name / indexActive page.
readonlybooleanSuppresses every mutation (pointer / keyboard / programmatic).
modeselectReserved for forward compatibility. Only select in v0.2.

Events

EventDetailFires on
change{ code }User commit (drag release, resize, delete, undo, redo, programmatic mutation).
select{ ids: string[] }Selection set changes.
page{ pageId }Active page changes.

External setAttribute('code', ...) does not emit change — it's a source-of-truth reset, not a user action.

Pointer + keyboard reference

GestureAction
Click an itemSelect (replace).
Shift / ⌘-clickAdd to or toggle selection.
Drag selectedMove (one history entry per drag).
Drag a handleResize.
Drag empty canvasRubber-band select.
Double-clickInline label edit.
Right-clickContext menu: Bring to Front / Forward / Send Backward / to Back / Duplicate / Delete.
⌘KOpen the element palette (<boceto-palette>) when bound to the editor.
⌘] / ⌘⇧]Bring forward / to front.
⌘[ / ⌘⇧[Send backward / to back.
Backspace / DeleteRemove selection.
⌘DDuplicate.
Arrow keys (Shift = ×10)Nudge selection.
⌘Z / ⌘⇧ZUndo / redo.

Programmatic API: el.editor

The custom element exposes its BocetoEditor controller as el.editor. Drive the doc from JavaScript — useful for toolbars, AI-generated edits, or any UI sitting alongside the canvas:

const el = document.querySelector('boceto-edit')

el.addEventListener('change', e => save(e.detail.code))

// Selection + mutation
el.editor.addElement('button', 200, 100, { label: 'Save' })
el.editor.move(['p0e0'], 10, 0)
el.editor.bringToFront(['p0e0'])

// History
el.editor.undo()
el.editor.redo()

// Pages
el.editor.addPage('Settings')
el.editor.setPage('Settings')

Headless: BocetoEditor without DOM

The controller is exported separately for use in React hooks, Svelte actions, tests, or anywhere a canvas isn't around. Same API as el.editor:

import { BocetoEditor } from '@boceto/edit'

const ed = new BocetoEditor({
  code: '```boceto\\nelement box 0 0 100 50 "Hi"\\n```',
})
ed.on('change', ({ code }) => persist(code))
ed.move(['p0e0'], 10, 0)
const updated = ed.code   // serialised on demand

@boceto/react — React wrappers

React wrappers around the custom elements. Props pass through as attributes; events fan back out via onRender / onChange. BocetoEditFull bundles the editor with its floating palette and inspector — use it when you want a working authoring surface (the bare BocetoEdit renders only the canvas).

import { BocetoView, BocetoEditFull } from '@boceto/react'

<BocetoView code={code} width={600} height={320} />
<BocetoEditFull code={code} onChange={setCode} readOnly={isLocked} />

// Custom layout — wire palette + inspector by a shared id:
import { BocetoEdit, BocetoPalette, BocetoInspector } from '@boceto/react'

<BocetoEdit id="ed-1" code={code} onChange={setCode} />
<BocetoPalette for="ed-1" />
<BocetoInspector for="ed-1" />

The editor also exposes the new imports prop introduced by @boceto/tiptap, so a React-hosted editor can resolve components defined in a sibling fence when embedded inside a doc-aware host.

@boceto/vue — Vue 3 wrappers

defineComponent-based wrappers that ship as plain runtime components (no SFC build step). Use v-model:code on <BocetoEdit> for two-way binding. The boceto-render CustomEvent surfaces as Vue's render event.

import { BocetoView, BocetoEdit } from '@boceto/vue'

<BocetoView :code="code" :width="600" :height="320" @render="onRender" />
<BocetoEdit v-model:code="code" :readOnly="isLocked" />

@boceto/svelte — Svelte wrappers

Source-only .svelte components — your Svelte tooling compiles them. Compatible with Svelte 4 and Svelte 5. Use bind:code for two-way binding; on:render and on:change surface the custom-element events.

<script>
  import { BocetoView, BocetoEdit } from '@boceto/svelte'
  let code = 'element navbar 0 0 600 44 "MyApp"'
</script>

<BocetoView {code} width={600} height={320} on:render={(e) => console.log(e.detail.doc)} />
<BocetoEdit bind:code readOnly={isLocked} />

@boceto/tiptap — TipTap node + extension

A TipTap node (bocetoBlock) and an extension (BocetoContext) for embedding Boceto blocks inside any TipTap editor. The extension broadcasts the doc-level Boceto source to every node view, so a component defined in one fenced block is resolvable from any other block in the same document — without merging them into a single fence.

This is the cross-block context the markdown plugins (@boceto/remark, @boceto/markdown-it) get for free, ported into a TipTap editor that stores each block as an atomic node.

import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { BocetoBlock, BocetoContext, BOCETO_ICON_SVG } from '@boceto/tiptap'
import { withReactNodeView } from '@boceto/tiptap/react'

const editor = useEditor({
  extensions: [StarterKit, withReactNodeView(BocetoBlock), BocetoContext],
  content: /* markdown / HTML with one or more ```boceto fences */,
})

// Toolbar button:
<button onClick={() => editor?.chain().focus().insertBocetoBlock().run()}>
  <span dangerouslySetInnerHTML={{ __html: BOCETO_ICON_SVG }} />
</button>

Heads-up: the built-in React node view mounts the full editing trio — <boceto-edit>, <boceto-palette>, <boceto-inspector>. If you swap in your own node view, mount all three; @boceto/react's wrapper only renders <boceto-edit>, so authoring without the palette + inspector is unusable.

Under the hood, BocetoContext populates editor.storage.bocetoContext.source with every block's fenced DSL joined together. The node view subtracts its own code and passes the rest to <boceto-view> / <boceto-edit> via the new imports attribute. Runnable demo: examples/tiptap-demo.

AI assistance

Boceto ships two complementary pieces for AI coding assistants: the skill (markdown rules that trigger the agent and teach the non-negotiables) and the MCP server (runtime tools the agent calls — lint, fix, render-svg, recipe + element catalog). Install both together with one command.

Install — npx boceto add mcp

npx boceto add mcp                       # auto-detect AI client; install MCP + matching skill
npx boceto add mcp claude-code           # explicit (claude-code | cursor | claude-desktop)
npx boceto add skill <target>            # skill only — for assistants without MCP support
npx boceto check                         # what's installed where

add mcp writes the mcpServers.boceto entry into your AI client's config and co-installs the matching skill (the skill is what triggers the agent to call the MCP tools — MCP alone leaves it with tools it doesn't know about). Auto-detects when exactly one MCP-client config exists; otherwise prompts. Atomic JSON merge — never clobbers other entries. --skip-skill to install MCP only.

MCP client (add mcp)Skill goes to
claude-code.claude/skills/boceto/ (project)
cursor.cursor/rules/boceto.mdc (project)
claude-desktop~/.claude/skills/boceto/ (user)

For assistants without a standardised MCP config (Aider, GitHub Copilot, OpenAI Codex / AGENTS.md, Gemini CLI, Cline, Windsurf, …): use boceto add skill <target> for the same set of targets the old @boceto/skill install command supported. Run boceto list skill for the full set.

Manual MCP install (edit JSON yourself)
{
  "mcpServers": {
    "boceto": {
      "command": "npx",
      "args": ["-y", "@boceto/mcp"]
    }
  }
}

Drop into ~/.claude.json, ~/.cursor/mcp.json, or the OS-specific Claude Desktop config and restart your client.

MCP server — @boceto/mcp

Closes the runtime loop: generate DSL → boceto_lint → fix → boceto_render_svg → iterate. Element catalog and recipes surface as on-demand tools so the agent doesn't carry the whole canon in its context.

Tools

ToolWhat it does
boceto_parseParse DSL → { ok, doc } or { ok: false, error: { line, message } }.
boceto_lintIssues with line / column / endColumn / severity / fix, plus a parse-clean fixed source.
boceto_fixReturns the autofixed source directly.
boceto_render_svgRender a page to a self-contained SVG (Yoga flex applied; deterministic).
boceto_list_elementsEvery element type, grouped by category, with default sizes.
boceto_describe_elementOne element's attribute schema and defaults.
boceto_list_recipesAll recipes (mockups + shells) with slug + summary.
boceto_read_recipeThe full markdown body of one recipe by slug.

The MCP server also exposes the skill canon + formal spec as resources under boceto://skill/*, boceto://references/*, boceto://recipes/index.md, and boceto://spec/boceto-spec.md — readable by any resource-aware client.

AI Skill — @boceto/skill

Trimmed teaching layer: SKILL.md (always loaded) + three reference files (grammar.md, layout.md, component-doc-pattern.md). The element catalog and recipes live behind the MCP tools above — the skill is small on purpose, so the agent keeps it in context. When asked for a composite component, the agent returns literate markdown: ## heading + description + fenced definition + example usage + Used in cross-reference.

Non-negotiables the skill teaches: six positional slots on every element line, four positional slots on every row / col line, canonical types only (hallucination map for the common substitutes), components-first authoring, definition-once per page, the literate output pattern, and the three documented parser limitations.

Try it

"Sketch a wireframe for a settings page with three sections."
"Add a search bar at the top of this `​```boceto` block."
"Mock up a Spotify-like mobile player wrapped in a phone-frame."
"Define a reusable pricing-card composite and place three tiers in a row."

Updating — re-run npx boceto add mcp --force (or npx boceto add skill <target> --force) to refresh. npx always fetches the latest published CLI on each invocation. --from-git pulls unreleased changes from the main branch. boceto check compares installed vs npm-latest.

Linter — @boceto/lint

A standalone linter + auto-fixer for Boceto source. Designed to catch the failure modes humans and AI assistants hit most often: missing label slots, invented element types, fractional coords, mismatched align vs textAlign, unclosed blocks, unterminated quoted strings, and structural parse errors — with character-precise line + column ranges so editors can underline the exact offending token.

The linter is line-oriented and stateless: it accepts a source string and returns a typed LintReport with { issues, fixed }. The fixed output is the source rewritten with every available autofix applied, in one pass. Cross-checks against @boceto/core's real parse() as a final step, so anything the rules miss still gets surfaced as a parse-error issue with fence-aware line numbers (parser-reported "line 3 in the block" is translated back to the actual file line).

Rules

RuleSeverityAutofixWhat it catches
missing-label error 5 positional slots, the 5th is unquoted — splices "" after H so chrome elements (chart-bar, status-bar, home-indicator, divider, fab, …) become parser-valid.
missing-coord error 5 slots, the 5th is a quoted label. One of X/Y/W/H was dropped, NOT the label — no autofix because the linter can't guess which coord was meant. Message tells the author what to do.
wrong-arity error Fewer than 5 positional slots on an element line.
invented-type error 16+ common AI hallucinations mapped to their real types: Frame → box, Stack → col, NavBar → navbar, Heading2 → heading, icon-button → button, PhoneFrame → phone-frame, …
unknown-type warning Element type not in the catalog and not in the canonical hallucination map. Suggests the closest known type by edit distance.
bad-coord error Fractional or negative coords — rounds to integer, clamps below 0.
align-vs-textAlign warning align=left|center|right on a non-container element — renames to textAlign= (since flex align uses start|middle|end|stretch).
unclosed-block error row / col / component / element-as-container block opened but never closed with end.
unterminated-string warning A label or attr opens with " but never closes before end-of-line. The parser silently accepts this, but it's almost always a typo — appends a closing ".
parse-error error Final cross-check against @boceto/core's real parser. Reports the original error with the line mapped back to its absolute position in the source (not relative to the inside of a markdown fence).

Usage

npm install @boceto/lint (or pnpm add, yarn add, …). Then:

import { lint, applyFixes } from '@boceto/lint'

const source = `\`\`\`boceto
element Frame 0 0 600 400
element chart-bar 232 110 600 260
\`\`\``

const report = lint(source)
//  report.issues:       LintIssue[] with line + column + endColumn + fix
//  report.fixed:        source rewritten with every autofix applied
//  report.errorCount, .warningCount, .infoCount

console.log(report.fixed)
// => `\`\`\`boceto
//     element box 0 0 600 400 ""
//     element chart-bar 232 110 600 260 ""
//     \`\`\``

// Or apply autofixes from a custom issue subset:
const partial = applyFixes(source, report.issues.filter(i => i.rule === 'invented-type'))

Each LintIssue carries:

interface LintIssue {
  rule: string                  // 'missing-label' | 'invented-type' | …
  severity: 'error' | 'warning' | 'info'
  line: number                  // 1-based, absolute in the source
  column: number                // 1-based, points at the offending token
  endColumn: number             // 1-based, exclusive end of the span
  message: string               // human-readable, safe for inline display
  fix?: {
    line: number
    newLine: string             // the replacement line (full text)
    label: string               // short label for UI buttons
  }
}

In the playground

The playground wires the linter into CodeMirror via @codemirror/lint — every issue gets an inline red / yellow underline at the exact offending span, a gutter marker, and a hover tooltip with a one-click Apply fix action. The full issue list also renders in a panel below the editor with a Fix all button that applies every available autofix in a single dispatch (one undo entry).

MCP server

The linter is the foundation of @boceto/mcp, the MCP server that lets any AI client call boceto_lint, boceto_fix, boceto_render_svg, and friends directly. See AI assistance → MCP server above for the install snippet and the full tool / resource list.

Reference: row / col attributes

AttributeValuesDefault
gapnon-negative integer (px)0
paddingnon-negative integer (px)0
alignstart · middle · end · stretchmiddle (row) / start (col)
justifystart · middle · end · between · around · evenlystart
wrapnowrap · wrap · wrap-reversenowrap
min-w / min-h / max-w / max-hnon-negative integer (px)unset

Reference: child flex attributes

Applies to any element, text, or component reference that is a direct child of a row / col. Ignored outside a flex container.

AttributeValuesDefault
grownon-negative number0
shrinknon-negative number1
basisnon-negative integer (px) or autoauto
align-selfauto · start · middle · end · stretchauto
min-w / min-h / max-w / max-hnon-negative integer (px)unset

Reference: component header attributes

GroupAttributeNotes
Shelldirectionrow · col. Presence enables shell mode.
gap / paddingPixels.
justifySee row/col reference.
alignSee above.
wrapSee above.
(omit)Component becomes absolute-body.
Size defaultw, hPixels or auto. Used when call site doesn't supply.
min-w / min-h / max-w / max-hPixels.
Child-flex defaultgrowNon-negative number.
shrinkNon-negative number.
basisPixels or auto.
align-selfSee child reference.

For the formal grammar, see the spec.