[
  {
    "description": "What Jx Suite is, how to install Jx Studio, and where to go first — whether you build visually or want the format underneath.",
    "markdown": "\n# Start here\n\nJx Suite is a visual site builder (**Jx Studio**), a JSON-native component format (**the Jx framework**), and a compiler that prerenders every page — working on plain files you own and publish with git. Sites that need more than pages get a server tier too: databases, signed-in users, and server functions compile into a small worker alongside the prebuilt output.\n\nPick your path:\n\n- **Build visually.** [Install Jx Studio](/docs/start/install) and follow [Your first project](/docs/start/first-project) to go from starter template to published site.\n- **Understand the format.** The [Framework](/docs/framework) section documents the JSON documents Studio writes for you — components, state, reactivity, styling, and how a site compiles.\n- **Extend it.** The [Extending](/docs/extending) section covers writing extensions, embedding Studio, and the backend protocol.\n",
    "section": "Start here",
    "slug": "start",
    "title": "Start here",
    "url": "https://jxsuite.com/docs/start/"
  },
  {
    "description": "Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.",
    "markdown": "\n# Get Studio\n\nJx Studio is a desktop application. There is no hosted, sign-in version — you run it on your own machine, against your own files.\n\n## Download the app\n\nGrab an installer for your platform from the [latest release](https://github.com/jxsuite/jx/releases/latest):\n\n| Platform              | Download                                                                                                           |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| macOS (Apple Silicon) | [`JxStudio.dmg`](https://github.com/jxsuite/jx/releases/latest/download/stable-macos-arm64-JxStudio.dmg)           |\n| macOS (Intel)         | [`JxStudio.dmg`](https://github.com/jxsuite/jx/releases/latest/download/stable-macos-x64-JxStudio.dmg)             |\n| Windows (x64)         | [`JxStudio.msi`](https://github.com/jxsuite/jx/releases/latest/download/JxStudio.msi)                              |\n| Linux (x64)           | [`JxStudio.tar.gz`](https://github.com/jxsuite/jx/releases/latest/download/stable-linux-x64-JxStudio-Setup.tar.gz) |\n\nThe macOS builds are notarized, so they open without a Gatekeeper prompt. The Windows installer is not yet code-signed — SmartScreen will warn on first run; choose **More info → Run anyway**. The [Download page](/download) has the same links plus checksums and release notes.\n\nOnce installed, open Studio and either **create a new project**, **open an existing folder**, or **clone a repository** — see [Your first project](/docs/start/first-project).\n\n## Updating\n\nStudio checks your project's `@jxsuite/*` dependencies against the version it ships with and offers to update them when they drift, and prompts when a newer release of the app itself is available.\n\n## For developers: scaffolding from a terminal\n\nThe visual editor only runs as the desktop app above — there's no way to install or serve Studio itself from a command line. If you'd rather generate a project's files from a terminal before opening them in Studio, see [CLI commands](/docs/framework/build/cli) for `bun create @jxsuite` and the `jx` CLI.\n\n## Next\n\nReady to build something? Continue to **[Your first project](/docs/start/first-project)**.\n",
    "section": "Start here",
    "slug": "start/install",
    "title": "Install Jx Studio",
    "url": "https://jxsuite.com/docs/start/install/"
  },
  {
    "description": "Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.",
    "markdown": "\n# Your first project\n\nIn about ten minutes you'll create a site in Jx Studio, design a page, add a bit of interactivity, and publish it to git — all without hand-writing a line of JSON. (If you _do_ want to see the underlying format, every page here has a counterpart under **Format & Reference**.)\n\n## 1. Get Studio\n\nDownload the desktop app for [macOS, Windows, or Linux](/docs/start/install) — there's no hosted Studio to sign into, it runs on your machine, against your files. Full details are in **[Install Jx Studio](/docs/start/install)**.\n\n## 2. Create a project\n\nIn Studio, choose **New Project**. Give it a name, choose the **Location** to create it in — **Browse…** opens your system's folder picker — name the folder, set your production URL, and pick a **deployment adapter** — Static, Cloudflare Pages, Node, or Bun. **Static** suits a site that's purely pages and content; if it will have a database, sign-ins, or server functions, pick one of the others — those hosts can run the small worker Jx builds for them. You can change this later in [project settings](/docs/studio/projects/settings).\n\nThen pick a **template**. Start from **Blank** for an empty project, or clone one of the [starter sites](/templates) — a restaurant, shop, portfolio, SaaS landing, blog, and more. Each one is a complete, themed site (pages, components, content, and images) you can reshape into your own. Studio copies it in as plain files and opens it on the canvas.\n\n![Jx Studio New Project dialog — template gallery, name, directory, production URL, and deployment adapter picker](../images/new-project-modal.png)\n\nAlready have a Jx project? Use **Open Project** to point Studio at a folder on disk, or **Clone** to pull one from git. (Developers who'd rather scaffold from a terminal can use `bun create @jxsuite` instead — see [CLI commands](/docs/framework/build/cli) — then open the result in Studio.)\n\n## 3. Manage, edit, design\n\nStudio gives you a surface for each part of the job. Start in **[Manage](/docs/studio/projects/browse)** to see your project — pages, components, content, and media — with live previews.\n\n![Jx Studio Manage Files modal with live previews of every project file](../images/mode-manage.png)\n\nOpen a content page and switch to **[Edit](/docs/studio/editing)** to write inline — click any text and type, use slash commands for blocks, fill in frontmatter on the side. Open a component and switch to **[Design](/docs/studio/design)** for the visual canvas: a live preview at every breakpoint, and a full CSS inspector for spacing, type, color, and hover states.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../images/mode-design.png)\n\n## 4. Add an interaction\n\nWebsites do things. In Studio, interactivity comes from three panels working together — no separate \"code mode\" required:\n\n- **State** — declare a value or a computed one (a counter, a total, a fetched list).\n- **Events** — bind a handler to a click, input, or submit with the structured expression editor.\n- **Code** — drop into the Monaco editor when a handler needs real JavaScript.\n\n![Jx Studio editing a component state function in the Monaco code editor](../images/mode-script.png)\n\nAdd a `count` to state, a button, and an `onclick` that increments it — you've built a reactive component. See **[Script & logic](/docs/studio/logic)** for the full toolkit.\n\n## 5. Commit and publish\n\nWhen you're happy, open **Source Control**. Review your changes, write a message, and **commit and sync** — or, for a brand-new project, **Publish to GitHub** and Studio creates the repository for you.\n\n![Jx Studio commit box — write a message and commit-and-sync straight from the Source Control panel](../images/git-commit.png)\n\nHere's the one boundary worth knowing: **Studio publishes code; it doesn't build or deploy the site.** It commits and pushes, and it sets the deploy adapter you chose in step 2. Your host takes it from there — building the site (`bunx jx build`) on every push and serving the `dist/` output from a CDN. If you picked one of the server-capable adapters in step 2, the build emits a small worker beside those files as well — that's the piece that runs a database, sign-ins, or server functions. See **[Git & publish](/docs/studio/publish)** for the full flow.\n\n## What's next\n\n- **Using Studio:** [Manage](/docs/studio/projects/browse) · [Edit](/docs/studio/editing) · [Design](/docs/studio/design) · [Script & logic](/docs/studio/logic) · [Git & publish](/docs/studio/publish)\n- **For developers:** [Site architecture](/docs/framework/site) · [Component model](/docs/framework/concepts/components) · [Spec overview](/docs/framework)\n",
    "section": "Start here",
    "slug": "start/first-project",
    "title": "Your first project",
    "url": "https://jxsuite.com/docs/start/first-project/"
  },
  {
    "description": "A guided map of the Jx Studio workspace — toolbar, activity bar, panels, canvas, tabs, and status bar, with links to each surface's guide.",
    "markdown": "\n# A tour of Jx Studio\n\nEverything in Jx Studio happens in one window. This page is your map: what each region is called, what it does, and where to read more. If you've used another visual builder, the layout will feel familiar — a live canvas in the middle, panels on either side, and a toolbar on top.\n\n![The Jx Studio workspace with the canvas in the center, panels on both sides, and the toolbar across the top](../images/hero.png)\n\n## The toolbar\n\nThe top row holds project-wide actions: **Open Project** (with a dropdown of recent projects), **Manage**, **Publish**, **Save**, and **Undo**/**Redo**, plus a search field and the mode switcher. Read more in **[The workspace](/docs/studio/interface)**.\n\n## The mode switcher\n\nOn the right side of the toolbar, five buttons switch how the canvas presents the open file: **Edit**, **Design**, **Grid**, **Code**, and **Stylebook**. Only the modes that make sense for the current file are enabled — see **[Modes and the preview toggle](/docs/studio/interface/modes)**.\n\n## The activity bar\n\nThe narrow icon strip on the far left switches what the left panel shows. The activities are **Files**, **Layers**, **Imports**, **Elements**, **State**, **Data**, **Document**, and **Source Control**, with **About** and **Settings** at the bottom.\n\n## The left panel\n\nThe left panel shows whichever activity you picked — your file tree, the element structure of the open page, the palette of things you can insert, and so on. Each activity is described in **[The workspace](/docs/studio/interface)**.\n\n## The canvas\n\nThe center of the window renders your page or component live, exactly as it will look in production. You select, edit, and rearrange elements directly on it — see **[The canvas](/docs/studio/interface/canvas)**, **[Edit mode](/docs/studio/editing)**, and **[Design mode](/docs/studio/design)**.\n\n## The tab strip\n\nEvery open file gets a tab above the canvas, with a dot marking unsaved changes — Studio only saves when you tell it to. Details in **[Tabs and files](/docs/studio/interface/tabs)**.\n\n## The right panel\n\nThree tabs inspect whatever is selected on the canvas: **Properties** (the element's settings), **Events** (what happens on click, input, and so on), and **Style** (the visual inspector). See **[Design mode](/docs/studio/design)** for Style and **[Script & logic](/docs/studio/logic)** for Events.\n\n## The status bar\n\nThe thin strip along the bottom shows what's selected and its position in the page structure, plus short confirmation messages like \"Saved\".\n\n## Quick Access\n\nPress :kbd[⌘P] (macOS) or :kbd[Ctrl+P] (Windows/Linux) anywhere to open a file by typing part of its name — see **[Quick Access](/docs/studio/interface/quick-access)**.\n\n## Next\n\n- Learn each region in depth in **[The workspace](/docs/studio/interface)**\n- Start writing in **[Edit mode](/docs/studio/editing)** or styling in **[Design mode](/docs/studio/design)**\n- Keep your hands on the keyboard with the **[shortcut reference](/docs/studio/interface/shortcuts)**\n",
    "section": "Start here",
    "slug": "start/studio-tour",
    "title": "A tour of Jx Studio",
    "url": "https://jxsuite.com/docs/start/studio-tour/"
  },
  {
    "description": "Build a live counter card in Jx Studio, click by click — declare state, bind text to it, wire up a button's click event, and watch the value update.",
    "markdown": "\n# Tutorial: your first interactive component\n\nIn this tutorial you build a `counter-card` component: a button and a line of text that counts the clicks, live. Small as it is, it walks the whole loop every interactive piece of a Jx site is made from — elements on the canvas, a state value, a binding, and an event — without writing any code.\n\n![The finished counter card on the canvas with Preview on, showing a count of 3](../images/counter-finished.png)\n\n**About 15 minutes.** Before you start:\n\n- Have Jx Studio running — see **[Install Jx Studio](/docs/start/install)**.\n- If Studio is completely new to you, skim **[Your first project](/docs/start/first-project)** first — this tutorial starts where it ends, with a project open.\n\n## 1. Open a project\n\nAny project works. If you already have one from [Your first project](/docs/start/first-project), open it with **Open Project**. Otherwise choose **New Project…**, pick the **Blank** template, click **Next**, name the project (say, \"Counter Demo\"), and click **Create Project**. Every field of the modal is explained in **[Create a project](/docs/studio/projects/create)**.\n\n![The New Project dialog with the template gallery, name, directory, production URL, and deployment adapter fields](../images/new-project-modal.png)\n\nYou should now see your project open on the canvas.\n\n## 2. Create the component\n\n1. Click **Manage** in the toolbar.\n2. Click **New** and choose **Component**.\n3. Type `counter-card` and confirm.\n\n![Jx Studio Manage Files modal with live previews of every project file](../images/mode-manage.png)\n\nStudio writes `components/counter-card.json` and opens it in a new tab — an empty canvas, ready to fill. (When to reach for a component versus a page or layout is covered in **[Pages, layouts, and components](/docs/studio/projects/pages-layouts-components)**.)\n\n## 3. Switch to Design mode\n\nComponent files open in **Edit** mode. In the mode switcher on the right side of the toolbar, click **Design**.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../images/mode-design.png)\n\nThe canvas now shows your component once per breakpoint — empty for the moment — and the right panel offers the **Properties**, **Events**, and **Style** tabs. Modes are just lenses on the same file; see **[Modes and the preview toggle](/docs/studio/interface/modes)**.\n\n## 4. Add a button and a text line\n\n1. Click **Elements** in the activity bar to open the palette.\n2. With nothing selected, click the **button** card. The button is added to the empty component.\n3. Press :kbd[Esc] to clear the selection — otherwise the next element would land _inside_ the button — then click the **p** card to add a paragraph.\n4. Select the button, open the **Properties** tab, and type `Add one` into **Text Content**.\n\n![The design canvas with a button labeled Add one and an empty paragraph below it](../images/counter-elements.png)\n\nYou should now see a button labeled **Add one** with an empty paragraph after it, at every breakpoint. The other ways to insert — dragging cards, the **+** affordance between elements — are covered in **[The canvas](/docs/studio/interface/canvas)** and the **[Elements panel](/docs/studio/design/elements)**.\n\n## 5. Declare the count\n\nThe component needs somewhere to keep its number. That's a state entry:\n\n1. Click **State** (the brackets icon) in the activity bar.\n2. Click the **+ Add…** picker at the bottom of the panel and choose **State Signal**. The new entry appears with a placeholder name and its editor open.\n3. Rename it first: type `count` into the **Name** field and press :kbd[Enter].\n4. Set **Type** to `integer` and leave **Default** at `0`.\n\n![Jx Studio State panel listing a component's state and functions](../images/state-panel.png)\n\nThe panel now shows a **State** section with one row: an **S** badge and the name `count`. Everything the panel can hold — computed values, data sources, functions — is covered in **[State panel](/docs/studio/logic/state)**.\n\n## 6. Bind the text to the count\n\nNow point the paragraph at the value instead of typing fixed text:\n\n1. Select the paragraph on the canvas.\n2. In the **Properties** tab, find the **Text Content** row. Beside its label sits a small mode button reading **abc** — the sign that the value is static.\n3. Click the button until it reads **${}** — the template mode. Each click steps to the next mode; the tooltip names the one a click will switch to.\n4. Studio pre-fills the field with your first state entry: `${state.count}`. Keep it, or mix in words: `Clicked ${state.count} times`.\n\n![The Text Content row in template mode holding ${state.count}, with the mode button accent-colored](../images/counter-text-binding.png)\n\nThe mode button takes on the accent color: this value is now dynamic, and the paragraph will always show the current count. The full ladder of dynamic values — **abc**, **$ref**, **${}**, **fx** — is explained in **[Formulas and expressions](/docs/studio/logic/formulas)**.\n\n## 7. Make the button count\n\n1. Select the button and click **Events** in the right panel.\n2. Click **Add Event**. A binding appears on `onclick` — and since the file has no functions yet, it starts as an inline handler.\n3. Switch the binding's mode to **$expression**, the mode for one-step reactions.\n4. In the formula editor, set the **Operator** to `+=`, point the **Target** at `count` (a **$ref**), and type `1` as the **Value**.\n\n![The Events tab with an onclick binding in expression mode incrementing count](../images/counter-onclick.png)\n\nThe formula reads `$count += 1` in the chip strip, and green badges beside the operands show live values evaluated against the running page. The three ways an event can respond — a named function, an expression, an inline handler — are covered in **[Events panel](/docs/studio/logic/events)**.\n\n## 8. Try it in Preview\n\nSwitch on the **Preview** toggle in the tab bar. The paragraph now shows `0` — the real, resolved value. Click **Add one** a few times.\n\n![The canvas with Preview on, the paragraph showing the climbed count](../images/counter-preview.png)\n\nYou should see the number climb with every click. That's the whole reactive loop: the event writes to `count`, and everything bound to `count` updates by itself.\n\n## 9. Watch the value in the Data explorer\n\nClick **Data** in the activity bar. It lists the same entries as the State panel, but with what each one is worth _right now_ — your `count` row shows the current number. Keep Preview on, click **Add one**, and watch the row change; **Refresh** re-renders the canvas and reads the values again.\n\n![The Data activity with the count entry showing its live value](../images/counter-data-explorer.png)\n\nWhen a page ever looks wrong, this panel is where you find out what it actually sees — see **[Data explorer](/docs/studio/logic/data-explorer)**.\n\n## 10. Try a test value\n\nBecause `count` is a plain state value on a component, it's also one of the component's _props_ — an option a page can set when it uses the card. The tab bar shows a small field named after each prop:\n\n1. Type `100` into the **count** field in the tab bar.\n2. The canvas re-renders with the count starting at 100, at every breakpoint.\n3. Clear the field to return to the default of `0`.\n\n![The tab bar's count field holding a test value, the canvas rendering with it](../images/counter-test-prop.png)\n\nTest values are a preview lens only — they're never saved into the component. Props and test values are covered in **[Working with components](/docs/studio/design/components)**.\n\n## 11. Save your work\n\nThe tab shows a **●** dot for unsaved changes. Press :kbd[⌘S] (macOS) / :kbd[Ctrl+S] (Windows/Linux) — or click the **Save** button in the toolbar — and the status bar confirms with \"Saved\".\n\nYou should see the dot disappear. When you're ready to publish, **Source Control** takes it from here — see **[Source control](/docs/studio/publish/source-control)**.\n\n## What you built\n\nA working, reusable component — and every piece of the interactive toolkit in one pass:\n\n- A **state entry** (`count`) — the component's memory, and automatically its prop.\n- A **template binding** (`${state.count}`) — text that follows the value wherever it goes.\n- An **event expression** (`$count += 1`) — behavior without a line of code.\n- **Preview**, the **Data explorer**, and **test values** — three ways to watch it run.\n\n:::doc-note\nEverything landed in one plain file, `components/counter-card.json`: the entry in its `state` object, the paragraph's text as a `${}` template, and the button's `onclick` as an `$expression`. The formats are documented in **[State](/docs/framework/concepts/state)** and **[Reactivity](/docs/framework/concepts/reactivity)**.\n:::\n\n## Next steps\n\n- Drop the card onto any page from the **[Elements panel](/docs/studio/design/elements)** — your components appear at the top of the palette.\n- Style it — spacing, color, hover states — with the **[Style inspector](/docs/studio/design/style-inspector)**.\n- When one step isn't enough, build multi-step handlers as **[statements](/docs/studio/logic/statements)**.\n- Keep going: **[Tutorial: a blog with content collections](/docs/start/first-collection)**.\n",
    "section": "Start here",
    "slug": "start/first-component",
    "title": "Tutorial: your first interactive component",
    "url": "https://jxsuite.com/docs/start/first-component/"
  },
  {
    "description": "Give a Jx site a blog in Studio: define a posts content type, create entries, bulk-edit them in Grid mode, and list them on a page with a repeater.",
    "markdown": "\n# Tutorial: a blog with content collections\n\nIn this tutorial you give your site a blog the structured way: a `posts` content type with a field schema, three entries created from it, a round of bulk editing in Grid mode, and a Blog page that lists every post from live data. Add a fourth post next month and the page updates itself.\n\n![The Blog page in Preview, listing three post cards with titles, dates, and descriptions](../images/blog-finished.png)\n\n**About 20 minutes.** Before you start:\n\n- Have a project open in Jx Studio — **[Your first project](/docs/start/first-project)** gets you there.\n- Knowing your way around the canvas and panels helps; **[Tutorial: your first interactive component](/docs/start/first-component)** is the gentlest way in, but isn't required.\n\n## 1. Create the posts content type\n\nA content type is your CMS schema: where a collection's entries live, and what fields each one carries.\n\n1. Click the **Settings** gear at the bottom of the activity bar, then go to _Settings > Content Types_.\n2. Click **New Entry** at the bottom of the type list.\n3. Type `Posts` and click **Create**.\n\n![The Content Types section with the new posts type selected and its empty field schema](../images/blog-content-type.png)\n\nYou should see the new type selected, named `posts`, with a matching source folder (`content/posts/`) and an empty field schema ready to fill.\n\n## 2. Give it fields\n\nAdd three fields — for each one, click **Add Field**, type the name, pick its type, and click **Add**:\n\n1. `title` — type **string**, with the **Required** switch on.\n2. `date` — type **string**, format **date**, so entries get a real date field.\n3. `description` — type **string**.\n\n![The posts type with title, date, and description rows in its field schema](../images/blog-schema-fields.png)\n\nYou should now see three field rows in the schema. These become the form every post fills in. Leave **Source** and **Format** as they are, and close Settings. Everything the builder can do — nested fields, references between types — is in **[Content types](/docs/studio/projects/content-types)**.\n\n## 3. Create your first post\n\n1. Click **Manage** in the toolbar.\n2. Click **New** — the menu now lists an item for the type you just defined. Pick **Posts**.\n3. Name the entry `hello-world`.\n\n![Jx Studio Manage Files modal with live previews of every project file](../images/mode-manage.png)\n\nStudio creates the file in `content/posts/` with every schema field pre-filled with a sensible blank, and opens it. Back in Manage, the entry appears under the **Content** filter, labeled with its type.\n\n## 4. Fill it in\n\nThe post opens in **Edit** mode with a **Properties** bar directly above the page — the same fields your schema defined:\n\n1. Type `Hello World` into **title**.\n2. Pick today in the **date** field.\n3. Give **description** a sentence — `The first post on my brand-new blog.`\n\nThen click into the page below and write a paragraph or two of body text.\n\n![Jx Studio editing markdown content inline with a WYSIWYG editor](../images/mode-edit.png)\n\nYou should see the frontmatter form filled in and your words on the page. The same fields also live in the **Document** activity — see **[Frontmatter and page metadata](/docs/studio/editing/frontmatter)**.\n\n## 5. Add two more posts\n\nRepeat step 3 twice — **New** > **Posts**, named `grid-mode-rocks` and `hello-again` (or anything you like). This time skip the fields on purpose; you'll fill them in bulk next.\n\nYou should now have three entries under Manage's **Content** filter, two of them with empty frontmatter.\n\n## 6. Open the collection as a grid\n\nEditing entries one file at a time doesn't scale, so Studio can open the whole collection as a spreadsheet:\n\n1. Click **Files** in the activity bar.\n2. Right-click the `content/posts` folder and choose **Edit Collection in Grid**.\n\n![The posts collection open in Grid mode, one row per entry with title, date, and description columns](../images/blog-grid.png)\n\nYou should see one row per post and one column per schema field, with the **Path** column pinned at the left — and your two blank rows plain to see.\n\n## 7. Fill in the blanks and save\n\n1. Double-click each empty **title** cell and type one. Cells are typed, so **date** cells give you a date field.\n2. Fill in the **date** and **description** cells the same way. Edited cells are highlighted, and nothing touches your files yet.\n3. Click **Save** — the button counts your pending changes — or press :kbd[⌘S] (macOS) / :kbd[Ctrl+S] (Windows/Linux).\n\nStudio writes every change in one batch and reports what saved. Ranges, fill-down, and find & replace are covered in **[Grid mode](/docs/studio/editing/grid)**.\n\n:::doc-note\nSaving a collection row rewrites that file's frontmatter block — which is exactly what fills in your blank entries here.\n:::\n\n## 8. Create the Blog page\n\n1. Click **Manage**, then **New** > **Page**, and name it `Blog`. Studio writes the page and opens it — this is your site's `/blog` address.\n2. In the mode switcher on the right side of the toolbar, click **Design**.\n\nYou should see the empty page on the design canvas, once per breakpoint.\n\n## 9. Design one post card\n\nDesign a single card — the repeater will copy it per post:\n\n1. Open **Elements** in the activity bar. With nothing selected, click the **article** card.\n2. With the article selected, click the **h3** card, then the **p** card twice — each new element lands inside the selection.\n3. Select each of the three in turn and give it placeholder text via **Text Content** in the **Properties** tab: `Post title`, `A line about the post.`, and `2026-01-01`.\n\n![The design canvas with one article card holding a heading and two placeholder paragraphs](../images/blog-card.png)\n\nYou should see one plausible-looking post card on the canvas. Style it as much or as little as you like — see **[Design mode](/docs/studio/design)**.\n\n## 10. Query the collection from state\n\nThe page needs the posts as data it can render:\n\n1. Click **State** in the activity bar, then the **+ Add…** picker.\n2. Pick **ContentCollection** — it's listed with the sources your project's imports and extensions provide.\n3. Rename the new entry to `posts` (type the name and press :kbd[Enter]).\n4. Set **contentType** to `posts`, and add a **sort** rule on the `date` field with order `desc`, so the newest post lists first.\n\n![The State panel editing a ContentCollection entry with contentType posts and a date sort rule](../images/blog-collection-state.png)\n\nOpen the **Data** activity and you should see `posts` worth `Array(3)` — your three entries, live. Filters, limits, and the other sources are covered in **[Data sources](/docs/studio/logic/data-sources)**.\n\n## 11. Repeat the card for every post\n\n1. Right-click the article — on the canvas or in **Layers** — and choose **Repeat…**.\n2. In the dialog, set **Items source** to `posts`.\n3. Click **Create Repeater**.\n\n![The Repeat dialog with posts selected as the items source](../images/blog-repeat-dialog.png)\n\nYour card is now the repeater's _template_, marked **↻** in Layers. On the design canvas it still renders once — that's the template view. Everything about repeaters lives in **[Repeaters](/docs/studio/design/repeaters)**.\n\n## 12. Bind the card to each post's fields\n\nInside the template, each post's data is in scope:\n\n1. Double-click the heading, select the placeholder text, and delete it.\n2. Click **Insert data** on the floating toolbar and pick `item.data.title`. A live placeholder lands in the text.\n3. Do the same for the two paragraphs: `item.data.description` and `item.data.date`.\n\n![The Insert data list open over the card's heading, offering item.data fields](../images/blog-insert-data.png)\n\nEach text now holds a placeholder that fills itself from the current post. (A collection entry's schema fields live under `item.data`; `item` and `index` are there too.)\n\n## 13. Preview and save\n\nSwitch on the **Preview** toggle in the tab bar.\n\n![Preview on, the single card expanded into three post cards with real titles, dates, and descriptions](../images/blog-preview.png)\n\nYou should see the single card expand into three, newest first, each filled in from its own post. Switch Preview off, then save your tabs with :kbd[⌘S] / :kbd[Ctrl+S]. When you're ready, commit the lot from **Source Control** — see **[Source control](/docs/studio/publish/source-control)**.\n\n![Jx Studio commit box — write a message and commit-and-sync straight from the Source Control panel](../images/git-commit.png)\n\n## Give each post its own page\n\nThe listing links nowhere yet, and that's deliberate: one page per post is the job of a _dynamic page_ — a single page file with a parameter in its name (like `[slug]`) that the build expands into one page per entry of the collection. That wiring lives in the page's own format rather than a Studio panel today; **[Routing](/docs/framework/site/routing)** shows the exact file, and **[Content collections](/docs/framework/site/content-collections)** covers looking up \"the entry this URL names\". Once a dynamic page exists, Studio meets you halfway: opening it shows a picker per URL parameter in the tab bar, so the canvas previews a real post instead of a placeholder.\n\n## What you built\n\nA complete content pipeline, end to end:\n\n- A **content type** (`posts`) — schema-backed entries with one-click creation from Manage.\n- **Three entries**, edited both one at a time (the Properties bar) and in bulk (**Grid mode**).\n- A **ContentCollection** state entry — the collection as live, sorted data on a page.\n- A **repeater** whose template binds `item.data` fields — one designed card, rendered per post.\n\n:::doc-note\nOn disk this is all plain files: your types in the `content` section of `project.json`, one file per post in `content/posts/`, and the Blog page holding the query and the repeater. The formats are documented in **[Content collections](/docs/framework/site/content-collections)** and **[Lists](/docs/framework/concepts/lists)**.\n:::\n\n## Next steps\n\n- Turn the post card into a reusable component — **[Working with components](/docs/studio/design/components)**.\n- Add an author type and point posts at it with a **reference** field — **[Content types](/docs/studio/projects/content-types)** and **[Relationships](/docs/framework/site/relationships)**.\n- Give posts their own pages with a dynamic route — **[Routing](/docs/framework/site/routing)**.\n- Publish it — **[Git & publish](/docs/studio/publish)**.\n",
    "section": "Start here",
    "slug": "start/first-collection",
    "title": "Tutorial: a blog with content collections",
    "url": "https://jxsuite.com/docs/start/first-collection/"
  },
  {
    "description": "How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.",
    "markdown": "\n# Coming from WordPress\n\nMost of what you know transfers. You still design visually, write in place, model structured content, and publish from the same tool. What changes is the machinery underneath: your content isn't in a database, there's no `wp-admin`, and no PHP running anywhere — your pages are built ahead of time instead of assembled on every request. A Jx site is a folder of plain files that [Jx Studio](/docs/studio) edits on your machine, and publishing means pushing those files to git so your host can build them into the finished site. A site that needs a real database or server-side logic can have both — see [Databases](/docs/studio/data) — but that's a layer you opt into, not the substrate your pages sit on.\n\nIf Cwicly's shutdown is what brought you here: welcome — losing a good tool mid-project is rough, and you're in familiar company. It's also the failure mode Jx is designed to rule out. Your site is plain files in your own folder and your own repository, so it can't disappear with anyone's product.\n\n## The concept map\n\n| WordPress                         | Jx                           | Where to look                                        |\n| --------------------------------- | ---------------------------- | ---------------------------------------------------- |\n| Theme                             | Starter + Stylebook + tokens | [Starter templates](/docs/studio/projects/starters)  |\n| Blocks                            | Elements and components      | [Elements](/docs/studio/design/elements)             |\n| The block editor                  | Edit mode                    | [Edit mode](/docs/studio/editing)                    |\n| Custom post types + custom fields | Content types                | [Content types](/docs/studio/projects/content-types) |\n| Media library                     | Media                        | [Media](/docs/studio/projects/media)                 |\n| Plugins                           | Extensions and npm packages  | [Extending](/docs/extending)                         |\n| Publish button                    | Commit and sync              | [Publish](/docs/studio/publish)                      |\n\nThe rest of this page walks through each row, then gets honest about what's different.\n\n## Themes\n\nA WordPress theme bundles templates, default styles, and options into something you install — and switching themes means starting over. In Jx the same jobs are separate, ordinary parts of your project:\n\n- A **[starter template](/docs/studio/projects/starters)** gives you complete pages, components, and content to reshape. Nothing about a starter is special afterward — it's plain files you own, with no parent theme to update or fight.\n- The **[Stylebook](/docs/studio/design/stylebook)** sets the default look of every heading, button, link, and form control in one catalog.\n- **[Design tokens](/docs/studio/design/tokens)** name your colors, fonts, and sizes once, so the whole site draws from one palette.\n\n## Blocks and the editor\n\nGutenberg's blocks split into two ideas. **Elements** are the building parts — headings, images, sections, buttons — placed from the [Elements panel](/docs/studio/design/elements) by click or drag. **[Components](/docs/studio/design/components)** are your reusable patterns: select something you built, turn it into a component, and reuse it with per-use properties — closer to block patterns, except you make them from your own work instead of registering them with code.\n\nWriting feels like the part of Gutenberg you liked. Open a content page in [Edit mode](/docs/studio/editing) and the canvas becomes the page itself: click any text and type, use slash commands for headings, lists, images, and tables. It saves as Markdown files, not rows in a `posts` table.\n\n![Jx Studio's Edit mode with a content page open for inline writing on the canvas](../images/mode-edit.png)\n\n## Custom post types and fields\n\nIf you've modeled content with custom post types and Advanced Custom Fields, **[content types](/docs/studio/projects/content-types)** are the same idea made first-class. Name a collection, define its fields — text, numbers, images, dates, required flags, even references from one type to another (a post pointing at its author) — and every entry gets a matching form.\n\nEntries are files in a folder instead of database rows: Markdown, CSV, or JSON. You can edit a whole collection like a spreadsheet in [Grid mode](/docs/studio/editing/grid), and pages query collections to build listing and detail pages — the archive template and single template, reborn. Build one end to end in **[Your first collection](/docs/start/first-collection)**.\n\n## Media\n\nDrag files into the Manage view and they land in your project's `public/` folder — no attachment posts, no uploads table. When the site builds, every image is optimized automatically into multiple sizes and modern formats, which is the job you used to install an optimizer plugin for. See **[Media](/docs/studio/projects/media)**.\n\n## Plugins\n\nThis is the honest gap. Jx has an [extension system](/docs/extending) — first-party extensions like the Markdown parser use the same public hooks yours would — and Studio can pull in npm packages of ready-made components through the [Imports panel](/docs/studio/projects/dependencies). But there is no plugin marketplace yet, and the catalog is a fraction of the WordPress directory's.\n\nThe counterweight: much of what plugins did for you is built in (image optimization, SEO metadata, forms UI, caching — a prebuilt page _is_ the cache) or unnecessary (security scanners, backup plugins — your repository is the backup). The server-backed ones — a members area, a form that stores what visitors send — are a third case: the parts ship with Jx, as [database tables](/docs/studio/data/tables) and [accounts and sessions](/docs/studio/data/auth-and-secrets), but you assemble the feature instead of activating someone else's.\n\n## Publishing\n\nThere's no Publish button mutating a live server. When you're happy, open **Source Control**, write a message, and **Commit and sync** — Studio pushes your files to your repository, and your host rebuilds the site from them. The result is prebuilt pages served from a CDN. The whole flow is in **[Publish](/docs/studio/publish)**.\n\n## What's different, in one list\n\n- **Content isn't in a database.** Pages and posts are files you can read, search, and diff. The data that arrives while the site runs — comments, sign-ups, orders — goes in a real database you add on purpose ([Databases](/docs/studio/data)), kept apart from your writing.\n- **No admin login.** _Editing_ happens in Studio on your machine, against your files — there's no hosted dashboard for bots to brute-force. Your site's own visitors can still have accounts; that's the [auth extension](/docs/studio/data/auth-and-secrets), and it's separate from how you edit.\n- **No plugin marketplace yet.** Extensions and npm components exist; a browsable ecosystem doesn't, yet.\n- **Publishing is git.** Studio handles the git parts with buttons, but a repository and a host are part of the setup.\n\n## What you'll miss\n\n- **The plugin directory.** Decades of plugins for every niche — commerce, memberships, event calendars. The server-backed pieces are buildable today: [data tables](/docs/studio/data/tables) give you real reads and writes, [auth](/docs/studio/data/auth-and-secrets) gives you accounts, sessions, and per-table permissions, and server-side functions cover the logic between them. What's missing is the shortcut — you build the feature instead of installing someone's finished one.\n- **Editing from any browser.** `wp-admin` is a website; Studio is an app where your files are. Git lets you set up a second machine, but it's a clone, not a login.\n- **The sheer size of the ecosystem** — hosts, agencies, themes, and twenty years of tutorials.\n\n## What you gain\n\n- **Nothing to maintain.** No core, plugin, or PHP updates; no security patching; the published pages are prebuilt files, with no admin login to break into.\n- **Fast by default.** Prebuilt pages from a CDN and automatically optimized images, with no caching plugins to tune.\n- **Version control of everything.** Design, settings, and content all live in git — every change is a reviewable commit you can roll back.\n- **Portability.** Your content is Markdown any tool can open; moving hosts is a settings change, not a database migration.\n- **A real design system.** Tokens and the Stylebook instead of overriding theme CSS from a child theme.\n\n## Start here\n\n- **[Your first project](/docs/start/first-project)** — starter to published site in about ten minutes\n- **[Your first collection](/docs/start/first-collection)** — build the Jx version of a custom post type\n- **[A tour of Jx Studio](/docs/start/studio-tour)** — learn the workspace before diving in\n",
    "section": "Start here",
    "slug": "start/coming-from-wordpress",
    "title": "Coming from WordPress",
    "url": "https://jxsuite.com/docs/start/coming-from-wordpress/"
  },
  {
    "description": "A translation guide from Webflow or Framer to Jx — classes, symbols, CMS collections, interactions — for a project that's plain files, not a subscription.",
    "markdown": "\n# Coming from Webflow or Framer\n\nYou already think in boxes, breakpoints, reusable components, and CMS collections — that model carries straight over to Jx. What changes is where your work lives. A Jx project is a folder of plain files on your machine, edited by [Jx Studio](/docs/studio) (a desktop app, or your browser against a local dev server) and published with git to a host you choose. There's no subscription keeping the site alive, no seats to buy, and no export wall between you and your own project.\n\n## The concept map\n\n| Webflow / Framer      | Jx                                | Where to look                                             |\n| --------------------- | --------------------------------- | --------------------------------------------------------- |\n| Style panel + classes | Style inspector                   | [Style inspector](/docs/studio/design/style-inspector)    |\n| Variables             | Design tokens                     | [Design tokens](/docs/studio/design/tokens)               |\n| Tag-level styles      | Stylebook                         | [Stylebook](/docs/studio/design/stylebook)                |\n| Symbols / components  | Components                        | [Working with components](/docs/studio/design/components) |\n| CMS collections       | Content types                     | [Content types](/docs/studio/projects/content-types)      |\n| Collection lists      | Repeaters                         | [Repeaters](/docs/studio/design/repeaters)                |\n| Interactions          | Events, state, and formulas       | [Logic](/docs/studio/logic)                               |\n| Breakpoint bar        | A live canvas per breakpoint      | [Design mode](/docs/studio/design)                        |\n| Site plan + publish   | Commit and sync; your host builds | [Publish](/docs/studio/publish)                           |\n\n## Styling, without class anxiety\n\nIn Webflow, every style is a class you name, combine, and manage forever. Jx spreads the same work across layers, so most elements never need a class at all:\n\n- The **[Stylebook](/docs/studio/design/stylebook)** sets what _every_ heading, button, link, and form control looks like — your baseline, in one catalog.\n- **Component styles** cover \"this card looks like this wherever it's used.\"\n- **Per-element styles** from the [Style inspector](/docs/studio/design/style-inspector) are reserved for true one-offs.\n- **[Design tokens](/docs/studio/design/tokens)** sit underneath all three — named colors, fonts, and sizes that every picker offers, like Webflow's Variables.\n\nThe Style inspector itself gives you the CSS control you're used to — spacing, typography, layout, backgrounds — plus `:hover`, `:focus`, and selectors of your own via **[States and selectors](/docs/studio/design/states-and-selectors)**.\n\n## A canvas per breakpoint\n\nInstead of one canvas you flip between breakpoints, Design mode renders a live panel for **every** breakpoint side by side, each running your real responsive rules. A tablet-only change shows up in the tablet panel while the others hold still — no more toggling back and forth to check you didn't break desktop. See **[Design mode](/docs/studio/design)** and **[Breakpoints](/docs/studio/design/breakpoints)**.\n\n![Jx Studio's Design mode rendering one component across four responsive breakpoints with the style inspector open](../images/mode-design.png)\n\n## Symbols and components\n\nWebflow symbols and Framer components map to Jx **[components](/docs/studio/design/components)**: select something you've built, turn it into a component, and every copy stays in sync with the definition. Per-instance variation comes through **props** — the equivalent of component properties — and **slots** let a component leave room for different content in each use. Build your first one in **[Your first component](/docs/start/first-component)**.\n\n## CMS collections\n\nCollections translate directly. A **[content type](/docs/studio/projects/content-types)** names a collection, defines its fields — text, numbers, images, dates, and references between types — and gives every entry a schema-backed form. **[Repeaters](/docs/studio/design/repeaters)** are your collection lists: bind one element to a collection and it repeats per entry. Dynamic detail pages — one page per entry — are part of the site model too; **[Your first collection](/docs/start/first-collection)** builds the whole loop.\n\nTwo differences worth knowing. Entries are files (Markdown, CSV, or JSON) in a folder, not rows in a hosted database — which means you can edit a whole collection like a spreadsheet in [Grid mode](/docs/studio/editing/grid), and there are no plan-based item limits because there are no plans. And there's no hosted Editor for clients: content editing happens in Studio, and collaborators work through the shared repository.\n\nCollections cover the content you author. Data that arrives while the site is running — form submissions, sign-ups, orders — belongs somewhere else: a real database you connect deliberately through **[Databases](/docs/studio/data)**, with **[accounts and sessions](/docs/studio/data/auth-and-secrets)** for the visitors who sign in. Content being files doesn't rule any of that out; it just keeps the two kinds of data apart.\n\n## Interactions, honestly\n\nBehavior in Jx comes from three surfaces working together: **[State](/docs/studio/logic/state)** declares what a page knows, the **[Events](/docs/studio/logic/events)** tab binds behavior to clicks, typing, and submits, and **[formulas](/docs/studio/logic/formulas)** compute values live. Toggles, tabs, filtered lists, form behavior — the logic side of interactions — is covered, often more directly than a timeline can express it.\n\nWhat Jx doesn't have is Webflow's scroll-driven animation timeline. Hover and focus effects with CSS transitions come through [States and selectors](/docs/studio/design/states-and-selectors), but choreographed scroll animation isn't a built-in surface today.\n\n## Publishing and the missing lock-in\n\nWhen you publish, Studio commits your files and pushes them to your repository; your host builds the site and serves the prebuilt pages from a CDN — **[Cloudflare Pages](/docs/studio/publish/cloudflare)** has a built-in flow, and **[Netlify, GitHub Pages, and others](/docs/studio/publish/other-hosts)** take a two-line setup. Hosting costs whatever your host charges, which for sites this shape is often nothing. Collaborators clone the repository — there are no seats.\n\nIn Webflow, code export is a snapshot that leaves the CMS behind. In Jx there's nothing to export, because the project already _is_ files: readable documents for pages and components, Markdown for content. Open them in any editor, diff them in git, hand them to other tools — Studio is how you edit your project, not where it's kept. The format underneath is fully documented in **[Framework](/docs/framework)**.\n\n## Start here\n\n- **[Your first project](/docs/start/first-project)** — starter template to published site\n- **[Your first component](/docs/start/first-component)** — the symbol workflow, in Jx\n- **[Your first collection](/docs/start/first-collection)** — the CMS workflow, in Jx\n",
    "section": "Start here",
    "slug": "start/coming-from-webflow",
    "title": "Coming from Webflow or Framer",
    "url": "https://jxsuite.com/docs/start/coming-from-webflow/"
  },
  {
    "description": "Switching from Wix or Squarespace to Jx in practice — the same visual editing, with local files, git, and a host you choose instead of a lock-in.",
    "markdown": "\n# Coming from Wix or Squarespace\n\nOn Wix or Squarespace, your site lives inside your account: the editor, the files, and the hosting are one thing you rent. Jx separates them. You still build in a visual editor — click, type, drag, style — but the site itself is a folder of ordinary files that you own, and putting it on the web means handing those files to a hosting service. This page explains what that actually means day to day, including the parts you take on yourself.\n\n## You still get a visual editor\n\nJx Studio is a full visual builder: click any text and type right on the page, drag in headings, images, and sections, and style everything with panels — colors, spacing, fonts — while a live canvas shows the page at every screen size. Nothing in the [Studio documentation](/docs/studio) assumes you can code.\n\n![Jx Studio with a real-estate site open on the canvas, showing the layers tree and properties panel](../images/hero.png)\n\nOne difference you'll notice immediately: Studio is an app you [download](/docs/start/install), not a website you sign into. There's no account, because there's nothing to have an account _on_ — it opens your files directly.\n\n## Where your site lives\n\nInstead of one company's servers, your site touches three places, each with one job:\n\n1. **Your computer** — the project folder. This is the site: pages, images, settings, all as ordinary files Studio edits.\n2. **A repository** — an online copy of that folder, with its full history. It's your backup and your publishing channel.\n3. **A host** — the service that shows the site to visitors. Every time you sync changes to the repository, the host rebuilds and the new version goes live.\n\nThe day-to-day loop is: edit in Studio, click **Commit and sync**, and a minute later the site is updated. All of it happens through Studio's buttons — you never type commands.\n\n## A short glossary\n\n| Term       | What it means                                                                                                                        |\n| ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| Repository | The online copy of your project folder, with its full history. GitHub is the best-known place to keep one.                           |\n| Commit     | A saved snapshot of your changes, with a short note saying what you did.                                                             |\n| Sync       | Sending your commits to the repository — this is the moment the host notices and rebuilds.                                           |\n| Host       | The service that serves your site to visitors. Cloudflare Pages, Netlify, and GitHub Pages all work, and their basic plans are free. |\n| Build      | The automatic step where your project files are turned into the finished pages the host serves.                                      |\n\n## What you take on\n\nHonestly: some one-time setup that Wix or Squarespace did for you.\n\n- **Connecting the pieces.** You'll create a GitHub account and connect a host. Studio walks you through the repository part with **[Publish to GitHub](/docs/studio/publish/github)**, has a built-in flow for **[Cloudflare Pages](/docs/studio/publish/cloudflare)**, and the **[other hosts](/docs/studio/publish/other-hosts)** guide covers the rest — the host setup is two fields, once.\n- **Your domain.** Domains come from a registrar or your host, not from the builder, and you connect one in the host's settings.\n- **No single support desk.** If something on the host's side misbehaves, it's their documentation you'll read — there's no one company renting you the whole stack.\n\nEditing also happens where Studio and your files are — your computer — rather than from any browser anywhere. The repository lets you set up a second machine, but it's a copy you open, not a website you log into.\n\n## What you get back\n\nThe trade is real, and so is the payoff: nothing is locked in.\n\n- A lapsed subscription can never take your site down or hold your content — the files are on your machine and in your repository either way.\n- You can switch hosts without rebuilding anything; the same files publish anywhere.\n- Your pages and writing are readable files today and in twenty years, openable without Jx at all.\n- Hosting a site like this is typically free, and the published site is plain, fast pages. Adding sign-ins or a database later gives your host one small program to run alongside them — but the build writes that program for you, and the pages themselves stay prebuilt either way. There's no server of yours to write or patch.\n\n:::doc-tip\nYou don't have to set up publishing on day one. Create a project, build, and explore entirely on your own machine — the repository and host can come when you're ready to go live.\n:::\n\n## Start here\n\n- **[Install Jx Studio](/docs/start/install)** — download the app for macOS, Windows, or Linux\n- **[Your first project](/docs/start/first-project)** — from starter template to published site\n- **[A tour of Jx Studio](/docs/start/studio-tour)** — find your way around the editor\n",
    "section": "Start here",
    "slug": "start/coming-from-wix",
    "title": "Coming from Wix or Squarespace",
    "url": "https://jxsuite.com/docs/start/coming-from-wix/"
  },
  {
    "description": "How Jx compares to AI site builders — the assistant edits inspectable documents you can see in Layers and State, review in git, and edit by hand.",
    "markdown": "\n# Coming from Lovable or Builder.io\n\nAI builders have settled the question of whether a machine can build a page. The question that matters a month in is different: can you still understand and change what it built? That's the question Jx is shaped around. Studio's [AI assistant](/docs/studio/ai) doesn't generate a codebase beside your project — it edits the project itself, in a document format designed to be read three ways at once: by you, by the visual editor, and by the AI.\n\n## What the assistant edits\n\nA Jx page or component is a structured document — readable JSON describing its elements, styles, and state, in a format that's [fully documented](/docs/framework). Studio's assistant works on those documents, the same files you edit on the canvas. Ask it for a pricing section and the result isn't a wall of generated code: the structure appears in **Layers**, the values and behavior in **State**, and the styles in the Style inspector — the same panels, showing the same file, whether you built it or the assistant did.\n\n| In an AI builder                    | In Jx                                                  |\n| ----------------------------------- | ------------------------------------------------------ |\n| Prompt-to-code generation           | An assistant editing your project's documents          |\n| Generated source you may never read | Documents every Studio panel can show you              |\n| Regenerate and hope                 | Edit by hand, visually, or by prompt — interchangeably |\n| Export or eject                     | Nothing to export — the files are already yours        |\n\n## Editable in both directions\n\nThis is the practical difference. With generated code, the AI is usually the only practical way back in — visual editing on top of a generated codebase is shallow, so changes mean re-prompting. In Jx, the visual editor and the assistant share one format, so you alternate freely: prompt a section into existence, then drag and restyle it by hand; build a component yourself, then ask the assistant to rework it. Click anything the assistant made and the **Properties**, **Style**, and **Events** tabs show exactly what it is — there's no layer you can't open.\n\n## Every change is a diff\n\nBecause the assistant changes files, its work shows up in **[Source Control](/docs/studio/publish/source-control)** like your own edits: a list of changed files, each with a readable diff. Review what it did, commit what you like, discard what you don't, and roll back any commit later. AI output rides the same rails as your own work — it doesn't get a separate, unaccountable channel into your project.\n\n## Ownership\n\nA Jx project is plain files on your machine, versioned in your repository, published to a host you choose — [no hosted service holding it, no account your work lives behind](/docs/studio/publish). (Your _site_ can have a database and signed-in users of its own; those run on infrastructure you control too.) The assistant is optional: everything it does, you can do by hand in Studio, and the format it writes is documented in the open. If you stopped using the assistant — or Jx entirely — your project would still be a folder of readable files.\n\n## Where Jx is different\n\nTwo differences are worth stating plainly, and neither is the one people expect. Jx builds full applications, not just brochure sites: [database connections](/docs/studio/data/connections), [user accounts and sessions](/docs/studio/data/auth-and-secrets), and [server-side logic](/docs/framework/concepts/timing) all ship, and your secrets stay on the server. The honest boundary is a rendering one — Jx has no per-request page rendering. Every page is prerendered at build time and interactivity hydrates as islands, so the set of routes is fixed when you build. That fits apps whose pages are known ahead of time; it does not fit one that assembles a different page structure for every visitor on every request.\n\nThe other difference is shape. Where Builder.io is a hosted platform your app plugs into, Jx is the opposite: a local tool that holds none of your work on anyone's servers.\n\n## Start here\n\n- **[The AI assistant](/docs/studio/ai)** — what it can do and how to work with it\n- **[Your first project](/docs/start/first-project)** — see the editor the assistant shares with you\n- **[Framework](/docs/framework)** — the document format underneath it all\n",
    "section": "Start here",
    "slug": "start/coming-from-ai-builders",
    "title": "Coming from Lovable or Builder.io",
    "url": "https://jxsuite.com/docs/start/coming-from-ai-builders/"
  },
  {
    "description": "Video walkthroughs of Jx Studio are planned. Until they arrive, every topic — first project, design, collections, publishing — has a written tutorial.",
    "markdown": "\n# Video tutorials\n\nVideo walkthroughs of Jx Studio are planned, and this page is where they'll live. None are published yet — rather than embed placeholders, here's what's coming and what to read in the meantime.\n\n## Planned walkthroughs\n\n| Video               | What it will cover                                      | Status      |\n| ------------------- | ------------------------------------------------------- | ----------- |\n| Your first project  | New project, starter templates, editing, and going live | Coming soon |\n| Design basics       | The canvas, style inspector, tokens, and Stylebook      | Coming soon |\n| Content collections | Content types, entries, and listing pages               | Coming soon |\n| Publishing          | GitHub, choosing a host, and the publish flow           | Coming soon |\n\n## In the meantime\n\nEvery planned topic already has a written counterpart you can follow today:\n\n- **[Your first project](/docs/start/first-project)** — the end-to-end tutorial, from starter template to published site\n- **[Your first component](/docs/start/first-component)** and **[Your first collection](/docs/start/first-collection)** — the two workflows you'll use most\n- **[A tour of Jx Studio](/docs/start/studio-tour)** — the workspace, mode by mode\n- **[Design mode](/docs/studio/design)** and **[Publish](/docs/studio/publish)** — the full guides behind the design and publishing videos\n\nThe written docs stay the reference either way — videos will complement them, not replace them.\n",
    "section": "Start here",
    "slug": "start/videos",
    "title": "Video tutorials",
    "url": "https://jxsuite.com/docs/start/videos/"
  },
  {
    "description": "Jx Studio is the visual builder for Jx projects — edit content inline, design on a live canvas, wire logic, and publish with git.",
    "markdown": "\n# Studio\n\nJx Studio is the visual builder for Jx projects. It runs on your machine — as a desktop app or in your browser against a local dev server — and edits the plain files in your project folder.\n\nEverything Studio does is organized around a few surfaces:\n\n- **[Browse your project](/docs/studio/projects/browse)** — pages, components, content, and media with live previews.\n- **[Edit mode](/docs/studio/editing)** — inline writing on the real page, with slash commands and frontmatter forms.\n- **[Design mode](/docs/studio/design)** — a live canvas per breakpoint with a full style inspector.\n- **[Logic](/docs/studio/logic)** — state, formulas, events, and data without leaving the visual editor.\n- **[Databases](/docs/studio/data)** — connect SQLite, D1, or Supabase, define tables, decide who can read and write them, and browse the rows in a grid.\n- **[Publish](/docs/studio/publish)** — commit, branch, and push; your host builds and serves the site.\n\n:::doc-note\nStudio writes plain JSON and Markdown files. Everything you build here is readable, diffable, and documented in the [Framework](/docs/framework) section.\n:::\n",
    "section": "Studio",
    "slug": "studio",
    "title": "Studio",
    "url": "https://jxsuite.com/docs/studio/"
  },
  {
    "description": "Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.",
    "markdown": "\n# The workspace\n\nThe Jx Studio window is one workspace with a fixed set of regions: a toolbar on top, an activity bar and panel on the left, the canvas in the center with its tabs above, an inspector panel on the right, and a status bar along the bottom. This page walks through each one. For a quicker orientation, start with **[A tour of Jx Studio](/docs/start/studio-tour)**.\n\n![The Jx Studio workspace with the canvas in the center, panels on both sides, and the toolbar across the top](../images/hero.png)\n\n## Toolbar\n\nThe toolbar runs across the top of the window.\n\nOn the left:\n\n- **Open Project** opens a project folder. The chevron beside it drops down a menu with **New Project…**, your recent projects (each with a remove button), and **Clear recent projects**.\n- **Manage** opens the project browser — see [Browse your project](/docs/studio/projects/browse).\n- **Publish** opens the publish panel — see [Git & publish](/docs/studio/publish).\n- **Save** writes the active file to disk. It's only enabled when the file has unsaved changes — Studio never saves on its own. See [Tabs and files](/docs/studio/interface/tabs).\n- **Undo** and **Redo** step through the active file's edit history.\n\nIn the middle, the **Search files…** field opens [Quick Access](/docs/studio/interface/quick-access). When your project has new commits waiting on the remote, a **Sync Project** button appears here to pull them.\n\nOn the right:\n\n- The **mode switcher** — **Edit**, **Design**, **Grid**, **Code**, **Stylebook** — changes how the canvas presents the open file. See [Modes and the preview toggle](/docs/studio/interface/modes).\n- A toggle collapses and restores the right panel.\n- A chat icon toggles the Assistant sidebar.\n\nIn the desktop app, the window's minimize, maximize, and close controls also live in this row.\n\n## Activity bar\n\nThe vertical icon strip on the far left picks what the left panel shows. From top to bottom:\n\n- **Files** — the project file tree. Open, rename, and organize the files in your project folder. **New File…** — from the panel's toolbar, or from a folder's right-click menu — opens a dialog with `untitled.json` pre-filled and the extension left unselected, so typing replaces just the name. Studio picks the starting content from the extension you give it.\n- **Layers** — the element structure of the open page or component, as a tree you can select and reorder. In **Stylebook** mode it lists the style targets instead.\n- **Imports** — the components and packages the open document (or project) pulls in.\n- **Elements** — the palette of elements and components you can insert, organized by category with a search filter.\n- **State** — the values, data sources, and functions the open component knows about. See [Script & logic](/docs/studio/logic).\n- **Data** — the live, resolved values of that state as the page runs.\n- **Document** — page-level settings: the title, the tags that go in the page head (description, social previews), and the layout the page uses.\n- **Source Control** — the built-in git client. A badge on the icon counts changed files. See [Git & publish](/docs/studio/publish).\n\nClicking the icon of the activity that's already open collapses the left panel; clicking any icon brings it back. **About** and **Settings** buttons sit at the bottom of the strip.\n\n## Left panel\n\nThe left panel shows the selected activity. Drag its inner edge to resize it — anywhere from a narrow strip to half the window — and double-click the same edge to snap back to the default width.\n\n## Right panel\n\nThe right panel inspects whatever is selected on the canvas, in three tabs:\n\n- **Properties** — the selected element's settings: its text, links, images, and component options.\n- **Events** — what the element does on click, input, submit, and other interactions. See [Script & logic](/docs/studio/logic).\n- **Style** — the visual inspector: spacing, typography, color, layout, and more. See [Design mode](/docs/studio/design).\n\nResize it by dragging its inner edge, or collapse it entirely with the toggle at the right end of the toolbar.\n\n:::doc-note\nStudio remembers your layout — panel widths and which panels are collapsed carry over to your next session.\n:::\n\n## Canvas\n\nThe center region renders the open file live. How you interact with it depends on the current mode; panning, zooming, selection, and direct manipulation are covered in **[The canvas](/docs/studio/interface/canvas)**.\n\n## Tab strip and tab bar\n\nTwo rows sit between the toolbar and the canvas:\n\n- The **tab strip** shows one tab per open file, with a dot marking unsaved changes.\n- The **tab bar** below it carries per-file controls: a **Back** breadcrumb when you've drilled into a nested component, zoom controls, the **Preview** toggle, and mode-specific actions like **Export** in Code mode.\n\nBoth are covered in **[Tabs and files](/docs/studio/interface/tabs)**.\n\n## Status bar\n\nThe strip along the bottom of the window shows:\n\n- The selected element and its **path** — the chain of elements from the page root down to your selection. Each step in the path is clickable, so you can jump to any ancestor.\n- **Content Mode** when the open file is a content document.\n- In **Stylebook** mode, the style rule you're editing.\n- Short, temporary messages such as \"Saved\" after a save.\n\n## Next\n\n- Meet the five canvas modes in **[Modes and the preview toggle](/docs/studio/interface/modes)**\n- Work directly on the page in **[The canvas](/docs/studio/interface/canvas)**\n- Learn the **[keyboard shortcuts](/docs/studio/interface/shortcuts)**\n",
    "section": "Studio",
    "slug": "studio/interface",
    "title": "The workspace",
    "url": "https://jxsuite.com/docs/studio/interface/"
  },
  {
    "description": "What Jx Studio shows before a project is open, and every way to start: new project, open, clone, add a repository, and recent projects.",
    "markdown": "\n# Welcome screen\n\nWhen Jx Studio starts with no project open, the canvas area shows the welcome screen: a **Start** list of ways to get a project in front of you, followed by your projects and recent history. Everything on it is one or two clicks from a working canvas.\n\n![The Jx Studio welcome screen with Start actions, the Projects list, and Recent files](../../images/welcome-screen.png)\n\n## Start a new project\n\n1. Click **New Project…**.\n2. Pick where to start from: a built-in **Template**, a **Starter Site**, an **Import** of an existing site, or an **Agent** prompt describing what you want.\n3. Click **Next**, name the project, choose the **Location** to create it in, and adjust the design quickstart (colors, fonts, logo).\n4. Click **Create Project** — Studio writes the files where you pointed it and opens the project.\n\n![The New Project dialog with the template gallery and project parameters](../../images/new-project-modal.png)\n\nThe full walkthrough is in **[Create a project](/docs/studio/projects/create)**.\n\n## Start from an example\n\n**Start from an Example…** opens the same New Project dialog directly on the **Starter Site** tab — a gallery of complete, themed sites (restaurant, shop, portfolio, blog, and more) that Studio copies in as plain files you own. It's the quickest way to a working site on a first run. Browse the gallery in **[Starter templates](/docs/studio/projects/starters)**.\n\n## Open an existing project\n\n1. Click **Open Project…**.\n2. Choose your project folder in the picker that appears.\n\nStudio opens the project and adds it to **Recent** for next time.\n\nOn **studio.jxsuite.com**, projects live in GitHub repositories instead of local folders, so **Open Project…** opens a repository picker: it lists the GitHub repositories you have write access to (Jx projects first), with a filter field to narrow the list. Click one and Studio opens it at `/edit/owner/repo@branch`. Repositories without a `project.json` show an inline explanation instead of opening.\n\nIf a repository you expect isn't listed, the App simply hasn't been given access to it — see [Repository access](#repository-access) below.\n\n## Clone a git repository\n\nThis entry appears when your Studio setup can run git.\n\n1. Click **Clone Git Repository…**.\n2. Paste the repository URL and click **Clone**.\n\nStudio clones the repository and opens it as a project. See **[Source control](/docs/studio/publish/source-control)** for everything git-related in Studio.\n\n## Add an existing repository\n\nThis entry appears when Studio is connected to your GitHub account.\n\n1. Click **Add Existing Repository…**.\n2. Type in the filter field to narrow the list of repositories your account can reach.\n3. Click a repository — Studio imports it and opens it as a project.\n\nA repository must already contain a Jx project (a `project.json` file); if it doesn't, Studio tells you why it can't be added. Connecting your account is covered in **[Publish to GitHub](/docs/studio/publish/github)**.\n\n## Repository access\n\nStudio only sees the repositories you have granted the **Jx Suite GitHub App** access to. There are two places to change that.\n\nIf your account is connected but Studio can't reach any repositories yet, a **Repository access** section appears on the welcome screen with an **Install the Jx Suite GitHub App** link. Follow it and choose **All repositories** so Studio can create and open projects on your behalf.\n\nOnce the App is installed, the repository picker (**Open Project…** and **Add Existing Repository…**) carries the same controls in its footer, so you never have to leave the dialog to widen access:\n\n1. Click the account name in **Missing a repository?** — GitHub opens that installation's **Repository access** settings in a new tab, where you can add repositories or switch to **All repositories**. **Another account…** installs the App on an account or organization that doesn't have it yet.\n2. Save the change on GitHub, then come back to Studio.\n3. Click **Refresh** — the picker re-reads your repositories and the newly granted ones appear.\n\n## Projects and Recent\n\nBelow the Start actions:\n\n- **Projects** lists the projects your Studio installation knows about that you haven't opened recently. Click one to open it.\n- **Recent** lists projects you've opened, newest first. Click one to reopen it, use the **✕** beside an entry to drop it from the list, or **Clear** to empty the list. Clearing the list doesn't touch the projects themselves — only the history.\n\n:::doc-tip\nYou don't need the mouse: press :kbd[⌘P] (macOS) or :kbd[Ctrl+P] (Windows/Linux) on the welcome screen and [Quick Access](/docs/studio/interface/quick-access) lists your recent projects to reopen.\n:::\n\n## Next\n\n- New to Jx? Follow **[Your first project](/docs/start/first-project)** end to end\n- Once a project is open, get oriented with **[A tour of Jx Studio](/docs/start/studio-tour)**\n",
    "section": "Studio",
    "slug": "studio/interface/welcome-screen",
    "title": "Welcome screen",
    "url": "https://jxsuite.com/docs/studio/interface/welcome-screen/"
  },
  {
    "description": "Edit, Design, Grid, Code, and Stylebook — what each canvas mode is for, which files offer which modes, and how the Preview toggle fits in.",
    "markdown": "\n# Modes and the preview toggle\n\nThe mode switcher on the right side of the toolbar changes how the canvas presents the open file: **Edit**, **Design**, **Grid**, **Code**, and **Stylebook**. A mode is a lens, not a different app — the same file underneath, shown the way that suits the job. Modes that don't apply to the current file are disabled, and every open file remembers its own mode as you switch between tabs.\n\n## Edit\n\nEdit is for writing. The canvas becomes the page itself — click any text and type, press `/` for blocks, fill in the page's metadata alongside. It reads like the finished page because it is the finished page. Full guide: **[Edit mode](/docs/studio/editing)**.\n\n![Jx Studio editing markdown content inline with a WYSIWYG editor](../../images/mode-edit.png)\n\n## Design\n\nDesign is for structure and style. The canvas shows a live panel per breakpoint — phone, tablet, desktop side by side — and the **Style** tab in the right panel becomes a full visual inspector for the selected element. Full guide: **[Design mode](/docs/studio/design)**.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../../images/mode-design.png)\n\n## Grid\n\nGrid is for tabular data. Files like CSV spreadsheets open as an editable table: click a cell to change it, and use the familiar copy, paste, and selection keys — they work on the table's rows and cells rather than the page. Cell edits collect in the tab until you **Save**, which writes them all back to the file at once.\n\n## Code\n\nCode shows the file as raw source in a full code editor with syntax highlighting — the same editor VS Code uses. It's the escape hatch when you want to see exactly what Studio wrote, and the **Export** button in the tab bar saves a copy of the file elsewhere. Everything you can do here you can also do visually; see **[Script & logic](/docs/studio/logic)** for where code fits in Studio.\n\n## Stylebook\n\nStylebook is the catalog of your project's element defaults — every heading, button, and link rendered with its base style, so you set the look of each element type once for the whole site. Selecting Stylebook switches the right panel to the **Style** tab automatically. See **[Design mode](/docs/studio/design)** for how it fits into styling.\n\n![Jx Studio Stylebook mode showing element defaults across breakpoints](../../images/stylebook.png)\n\n## Which files offer which modes\n\nEvery file opens in its natural mode, and only sensible modes are enabled:\n\n| File                               | Opens in      | Also available                                                  |\n| ---------------------------------- | ------------- | --------------------------------------------------------------- |\n| Markdown pages and content (`.md`) | **Edit**      | **Design**, **Code**, and the **Preview** toggle                |\n| Components and pages (`.json`)     | **Edit**      | **Design**, **Code**, **Stylebook**, and the **Preview** toggle |\n| Spreadsheets (`.csv`)              | **Grid**      | **Code**                                                        |\n| The project file (`project.json`)  | **Stylebook** | **Code**                                                        |\n\nInstalled format extensions can add their own file types with their own mode lists, so this table can grow with your project.\n\n## The Preview toggle\n\n**Preview** is not a sixth mode — it's a toggle in the tab bar that layers onto Edit and Design. Switch it on and the canvas shows the page with its real data resolved: dynamic text filled in, repeated lists expanded, exactly what a visitor gets.\n\nAlongside the toggle, the tab bar offers:\n\n- For pages with dynamic addresses (a product page, a blog post), a picker per URL parameter so you choose which real record to preview.\n- For components, a small field per component option so you can try test values.\n- For pages that use a layout, a **Layout** toggle to show or hide the elements the layout contributes.\n\nSwitch Preview off to go back to editing — the mode switcher stays on Edit or Design the whole time.\n\n## Next\n\n- Learn the canvas itself — pan, zoom, selection, inserting — in **[The canvas](/docs/studio/interface/canvas)**\n- See how each tab remembers its mode in **[Tabs and files](/docs/studio/interface/tabs)**\n",
    "section": "Studio",
    "slug": "studio/interface/modes",
    "title": "Modes and the preview toggle",
    "url": "https://jxsuite.com/docs/studio/interface/modes/"
  },
  {
    "description": "Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.",
    "markdown": "\n# The canvas\n\nThe canvas is the center of the workspace, where your page renders live — the real thing, not a mock-up. You work on it directly: click to put the cursor in the text and select the block, drag the block bar's handle to rearrange. How it behaves depends on the current [mode](/docs/studio/interface/modes); this page covers the interactions shared by the visual modes.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../../images/mode-design.png)\n\n## Pan and zoom\n\nIn **Design** and **Stylebook** mode the canvas is an open surface you move around:\n\n- **Pan** — scroll with the mouse wheel or trackpad. Hold :kbd[Shift] while scrolling to pan sideways, or drag with the middle mouse button.\n- **Zoom** — hold :kbd[⌘] (macOS) or :kbd[Ctrl] (Windows/Linux) and scroll; the canvas zooms toward your cursor. :kbd[⌘=] / :kbd[Ctrl+=] zooms in, :kbd[⌘-] / :kbd[Ctrl+-] zooms out, and :kbd[⌘0] / :kbd[Ctrl+0] resets to 100%.\n- The zoom controls in the tab bar do the same, plus **Fit** to bring the whole canvas into view.\n\nIn **Edit** mode the page scrolls like a normal browser page instead of panning, and :kbd[Ctrl]-scrolling zooms the content itself — the text reflows at the new size, like browser page zoom.\n\n## Selecting elements\n\nClick any element to select it. Studio outlines it, the right panel inspects it, and the status bar shows its position in the page structure — a clickable trail of its ancestors.\n\nYou can also move the selection from the keyboard: :kbd[↑] and :kbd[↓] step between siblings, :kbd[←] selects the parent, :kbd[→] steps into the first child, and :kbd[Esc] clears the selection. The full list is in the **[shortcut reference](/docs/studio/interface/shortcuts)**.\n\n## The block action bar\n\nA small floating toolbar appears above the selected element:\n\n- A **back arrow** selects the parent element.\n- The **name badge** shows what's selected — the element's type or its name. When the element can become something else (a paragraph into a heading, for example), clicking the badge lists the conversions.\n- The **⠿ drag handle** — drag it to move the element somewhere else on the page.\n- **Move up** and **Move down** arrows swap the element with its neighbors.\n- For a component instance, **Edit Component** opens the component itself; for anything else, **Convert to Component** turns the selection into a reusable component.\n- While you're editing text, formatting buttons (bold, italic, and friends) and an **Insert data** button join the bar. See [Edit mode](/docs/studio/editing).\n\n![A selected element on the canvas with the floating block action bar above it](../../images/block-action-bar.png)\n\n## Inserting elements\n\nThree ways to add something to the page:\n\n- **The + affordance** — move the pointer between two elements and a **+** appears at the insertion point. Click it and pick an element from the menu; the new element lands right there, selected.\n- **The slash menu** — while editing text, type `/` at the start of a line to insert headings, lists, images, buttons, and more without leaving the keyboard. See [Edit mode](/docs/studio/editing).\n- **The Elements panel** — open the **Elements** activity and drag an element or component card onto the canvas.\n\n## Drag and drop\n\nYou can drag onto and around the canvas from almost anywhere: cards from the **Elements** panel, rows in the **Layers** panel, and the **⠿** handle on the block action bar. While you drag, an indicator line shows exactly where the element will land — before, after, or inside the element under the cursor. Drop to commit, or press :kbd[Esc] to cancel the drag with nothing changed.\n\n## The right-click context menu\n\nRight-click any element for the full action list: **Copy**, **Cut**, **Duplicate**, **Copy styles** and **Paste styles**, **Insert before** and **Insert after**, **Wrap in Div**, **Repeat…** (turn the element into a repeating list), **Set Title**, **Edit Component** or **Convert to Component**, and **Delete**. With something on the clipboard, **Paste inside** and **Paste after** appear too.\n\n## Editing text\n\nClick any text to put the cursor there and start typing. Everything about writing on the canvas — formatting, the slash menu, links — is covered in **[Edit mode](/docs/studio/editing)**.\n\n## Next\n\n- Style what you select in **[Design mode](/docs/studio/design)**\n- Wire up behavior in **[Script & logic](/docs/studio/logic)**\n- Keep your hands on the keys with the **[shortcut reference](/docs/studio/interface/shortcuts)**\n",
    "section": "Studio",
    "slug": "studio/interface/canvas",
    "title": "The canvas",
    "url": "https://jxsuite.com/docs/studio/interface/canvas/"
  },
  {
    "description": "How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.",
    "markdown": "\n# Tabs and files\n\nEvery file you open in Jx Studio gets a tab in the strip above the canvas. Tabs work the way you'd expect from a browser — and they carry two things worth knowing: a dirty marker, because Studio only saves when you say so, and a memory of how you were viewing each file.\n\n![The tab strip with several open files, one showing the unsaved-changes dot](../../images/tab-strip.png)\n\n## Opening and switching\n\nOpen files from the **Files** activity, the **Manage** browser, or [Quick Access](/docs/studio/interface/quick-access) (:kbd[⌘P] on macOS, :kbd[Ctrl+P] on Windows/Linux). Click a tab to switch to it.\n\n- Close a tab with its **×** button, by middle-clicking it, or with :kbd[⌘W] / :kbd[Ctrl+W].\n- When more tabs are open than fit, scroll the mouse wheel over the strip to slide them.\n\n## Studio saves only when you do\n\nJx Studio does **not** auto-save. Edits live in the tab until you save:\n\n- A **●** dot on the tab marks unsaved changes, and the **Save** button in the toolbar lights up.\n- Save with :kbd[⌘S] / :kbd[Ctrl+S] or the **Save** button — the status bar confirms with \"Saved\".\n\n:::doc-warning\nClosing a tab with unsaved changes discards them. Studio always asks first — choose **Close** in the confirmation only if you really mean to throw the edits away.\n:::\n\nUndo and redo are per file as well: each tab keeps its own history, so :kbd[⌘Z] / :kbd[Ctrl+Z] in one tab never unwinds work in another.\n\n:::doc-note\nSaving writes the file in place, in your project folder — plain Markdown, JSON, or CSV. Nothing is held in a database; what you save is what git sees. See [Git & publish](/docs/studio/publish).\n:::\n\n## Each tab remembers its view\n\nA tab keeps its own view state while it's open. Switch from a Markdown page in **Edit** to a component in **Design** and back, and each returns exactly as you left it:\n\n- its [mode](/docs/studio/interface/modes) and **Preview** toggle,\n- its zoom level and canvas position,\n- its selected element and active right-panel tab.\n\nNew files open in their natural mode — Markdown in **Edit**, spreadsheets in **Grid**, the project file in **Stylebook**.\n\n## The tab bar\n\nBelow the tab strip, a second row carries the controls for the active tab:\n\n- **Back** and a breadcrumb trail, when you've drilled from a page into one of its components — click any crumb to jump back up.\n- The zoom controls for the current mode, including **Fit** on the pannable canvases.\n- The **Preview** toggle, the **Layout** toggle, and the preview pickers described in [Modes and the preview toggle](/docs/studio/interface/modes).\n- The **Auto / Light / Dark** color-scheme control, when the project declares a `prefers-color-scheme` breakpoint — it forces the canvas into either scheme (or follows your OS in Auto) without re-rendering. See [Breakpoints](/docs/studio/design/breakpoints).\n- Mode-specific actions, like **Export** in **Code** mode.\n\n## Next\n\n- Find any file fast with **[Quick Access](/docs/studio/interface/quick-access)**\n- Browse the whole project visually in **[Manage](/docs/studio/projects/browse)**\n",
    "section": "Studio",
    "slug": "studio/interface/tabs",
    "title": "Tabs and files",
    "url": "https://jxsuite.com/docs/studio/interface/tabs/"
  },
  {
    "description": "Open any file in your project by name with the Quick Access palette — how to open it, what it searches, and its keyboard controls.",
    "markdown": "\n# Quick Access\n\nQuick Access is the fastest way to open a file: a search palette that drops over the workspace, finds files by name as you type, and opens your pick on :kbd[Enter]. If you know roughly what a file is called, it beats clicking through the file tree every time.\n\n![The Quick Access palette open over the workspace with results for a typed query](../../images/quick-access.png)\n\n## Open it\n\n- Press :kbd[⌘P] (macOS) or :kbd[Ctrl+P] (Windows/Linux) — it works from anywhere in Studio.\n- Or click the **Search files…** field in the middle of the toolbar.\n\nPress :kbd[Esc] or click outside the palette to dismiss it.\n\n## What it finds\n\nWith a project open, type any part of a filename and Quick Access searches the whole project's documents — pages, components, content, and data files alike. Each result shows the filename and the folder it lives in.\n\nBefore you type anything, the palette lists your **recently opened** files, so reopening the file you just closed is :kbd[⌘P], :kbd[Enter].\n\nWith no project open, the palette lists your recent projects instead — pick one to reopen it. It never mixes the two: with a project open you only ever see that project's files.\n\n## Keyboard controls\n\n- :kbd[↓] and :kbd[↑] move through the results.\n- :kbd[Enter] opens the highlighted result in a tab.\n- :kbd[Esc] closes the palette.\n\nThe mouse works too — click any row to open it.\n\n## Next\n\n- See how opened files behave in **[Tabs and files](/docs/studio/interface/tabs)**\n- The rest of the keyboard lives in the **[shortcut reference](/docs/studio/interface/shortcuts)**\n",
    "section": "Studio",
    "slug": "studio/interface/quick-access",
    "title": "Quick Access",
    "url": "https://jxsuite.com/docs/studio/interface/quick-access/"
  },
  {
    "description": "Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.",
    "markdown": "\n# Keyboard shortcuts\n\nEvery shortcut in Jx Studio, grouped by where it applies. macOS uses :kbd[⌘] where Windows and Linux use :kbd[Ctrl]; everything else is the same on all platforms.\n\n## Everywhere\n\nThese work throughout Studio, even while typing in a field.\n\n| Action               | macOS     | Windows / Linux    |\n| -------------------- | --------- | ------------------ |\n| Save the active file | :kbd[⌘S]  | :kbd[Ctrl+S]       |\n| Open Quick Access    | :kbd[⌘P]  | :kbd[Ctrl+P]       |\n| Open a project       | :kbd[⌘O]  | :kbd[Ctrl+O]       |\n| Close the active tab | :kbd[⌘W]  | :kbd[Ctrl+W]       |\n| Undo                 | :kbd[⌘Z]  | :kbd[Ctrl+Z]       |\n| Redo                 | :kbd[⇧⌘Z] | :kbd[Ctrl+Shift+Z] |\n\n## Canvas — with an element selected\n\nThese apply when a block is selected but the cursor is not in its text — after picking a row in the\nlayers panel, for instance. Once the cursor is in the text it owns these keys; see **Writing** below.\n\n| Action                               | macOS                           | Windows / Linux   |\n| ------------------------------------ | ------------------------------- | ----------------- |\n| Duplicate the element                | :kbd[⌘D]                        | :kbd[Ctrl+D]      |\n| Copy the element                     | :kbd[⌘C]                        | :kbd[Ctrl+C]      |\n| Cut the element                      | :kbd[⌘X]                        | :kbd[Ctrl+X]      |\n| Paste                                | :kbd[⌘V]                        | :kbd[Ctrl+V]      |\n| Delete the element                   | :kbd[Delete] or :kbd[Backspace] | same              |\n| Clear the selection                  | :kbd[Esc]                       | :kbd[Esc]         |\n| Insert a paragraph after the element | :kbd[Enter]                     | :kbd[Enter]       |\n| Select the previous / next sibling   | :kbd[↑] / :kbd[↓]               | :kbd[↑] / :kbd[↓] |\n| Select the parent                    | :kbd[←]                         | :kbd[←]           |\n| Select the first child               | :kbd[→]                         | :kbd[→]           |\n\n## Canvas — zoom and pan\n\n| Action                 | macOS                | Windows / Linux      |\n| ---------------------- | -------------------- | -------------------- |\n| Zoom in                | :kbd[⌘=]             | :kbd[Ctrl+=]         |\n| Zoom out               | :kbd[⌘-]             | :kbd[Ctrl+-]         |\n| Reset zoom to 100%     | :kbd[⌘0]             | :kbd[Ctrl+0]         |\n| Zoom toward the cursor | :kbd[⌘] + scroll     | :kbd[Ctrl] + scroll  |\n| Pan                    | scroll               | scroll               |\n| Pan sideways           | :kbd[Shift] + scroll | :kbd[Shift] + scroll |\n| Pan by dragging        | middle-mouse drag    | middle-mouse drag    |\n\nIn **Edit** mode the zoom keys resize the content itself (the text reflows), and the page scrolls instead of panning.\n\n## Writing\n\nWith the cursor in text on the canvas.\n\n### Moving and selecting\n\n| Action                                | macOS                           | Windows / Linux                 |\n| ------------------------------------- | ------------------------------- | ------------------------------- |\n| Put the cursor somewhere              | click                           | click                           |\n| Move through the page, block to block | :kbd[↑] :kbd[↓] :kbd[←] :kbd[→] | :kbd[↑] :kbd[↓] :kbd[←] :kbd[→] |\n| Start / end of line                   | :kbd[Home] / :kbd[End]          | :kbd[Home] / :kbd[End]          |\n| Select, including across blocks       | :kbd[Shift] + move, or drag     | :kbd[Shift] + move, or drag     |\n| Put the cursor away                   | :kbd[Esc]                       | :kbd[Esc]                       |\n\n### Changing the text\n\n| Action                     | macOS                                | Windows / Linux   |\n| -------------------------- | ------------------------------------ | ----------------- |\n| Bold                       | :kbd[⌘B]                             | :kbd[Ctrl+B]      |\n| Italic                     | :kbd[⌘I]                             | :kbd[Ctrl+I]      |\n| Inline code                | :kbd[⌘`]                             | :kbd[Ctrl+`]      |\n| Add a link                 | :kbd[⌘K]                             | :kbd[Ctrl+K]      |\n| Open the block menu        | :kbd[/]                              | :kbd[/]           |\n| New paragraph              | :kbd[Enter]                          | :kbd[Enter]       |\n| Line break, same paragraph | :kbd[Shift+Enter]                    | :kbd[Shift+Enter] |\n| Join onto the block above  | :kbd[Backspace] at the block's start | same              |\n| Pull the next block up     | :kbd[Delete] at the block's end      | same              |\n| Save                       | :kbd[⌘S]                             | :kbd[Ctrl+S]      |\n\nThe block menu opens when :kbd[/] is typed at the start of a line or after a space. Saving always\nwrites what is on screen — your writing reaches the document as you pause, so there is no need to\nfinish anything first.\n\n## Quick Access palette\n\n| Action                    | Key               |\n| ------------------------- | ----------------- |\n| Move through the results  | :kbd[↑] / :kbd[↓] |\n| Open the highlighted file | :kbd[Enter]       |\n| Close the palette         | :kbd[Esc]         |\n\n## Drag and drop\n\n| Action          | Key       |\n| --------------- | --------- |\n| Cancel the drag | :kbd[Esc] |\n\n## Grid mode\n\nIn **Grid** mode the table owns the editing keys: copy, paste, delete, arrows, and :kbd[Enter] act on cells and ranges, and the zoom keys stay with the grid too. The app-level shortcuts — Save, Quick Access, Open project, Close tab, and Undo/Redo — still work as listed above.\n\n## While a dialog is open\n\nA dialog takes the keyboard for as long as it is up: it gets focus when it opens, :kbd[Esc] dismisses it, and focus returns to whatever you were on. Every shortcut on this page stands down meanwhile — the dialog dims the app behind it, so :kbd[Delete], :kbd[Enter], :kbd[⌘S] and the rest cannot reach the page you can't click.\n\n## Next\n\n- See where these fit on the surface itself in **[The canvas](/docs/studio/interface/canvas)**\n- Formatting while writing is covered in **[Edit mode](/docs/studio/editing)**\n",
    "section": "Studio",
    "slug": "studio/interface/shortcuts",
    "title": "Keyboard shortcuts",
    "url": "https://jxsuite.com/docs/studio/interface/shortcuts/"
  },
  {
    "description": "What a Jx project is on disk — a folder of plain files — and the three ways to get one in Studio: create new, open a folder, or clone a repository.",
    "markdown": "\n# Projects\n\nA Jx project is a folder on your computer. No hosted service holds your work and no account owns it — everything you build in Studio is a plain file inside that folder, which you can back up, copy, or put under version control like any other document. A published site can still keep its own runtime data in a real database (see **[Databases](/docs/studio/data)**); the project you edit is always just files.\n\n## What's in the folder\n\nStudio and the folder are two views of the same thing. Each area of your project maps to a subfolder:\n\n| On disk        | What it holds                                   | In Studio                                                    |\n| -------------- | ----------------------------------------------- | ------------------------------------------------------------ |\n| `pages/`       | One file per page of your site                  | **Pages** in the [Manage view](/docs/studio/projects/browse) |\n| `layouts/`     | Shared page shells — headers, footers, wrappers | **Layouts**                                                  |\n| `components/`  | Reusable building blocks                        | **Components**                                               |\n| `content/`     | Posts, entries, and other collection content    | **Content**                                                  |\n| `public/`      | Images, video, fonts, and other media           | **Media**                                                    |\n| `project.json` | The site's settings — name, URL, design tokens  | Project settings                                             |\n\nWhat each kind of file is for — and when to reach for which — is covered in **[Pages, layouts, and components](/docs/studio/projects/pages-layouts-components)**. The full folder anatomy is documented in **[Site architecture](/docs/framework/site)**.\n\n## Get a project\n\nThere are three ways to end up with a project open in Studio, all available from the [Welcome screen](/docs/studio/interface/welcome-screen):\n\n1. **Create a new one.** Click **New Project…** and pick a template or a complete starter site — or jump straight to the starter gallery with **Start from an Example…**. You choose the folder it's created in (on studio.jxsuite.com, the GitHub repository); Studio never picks one for you. The whole modal is walked through in **[Create a project](/docs/studio/projects/create)**.\n2. **Open an existing folder.** Click **Open Project…** and point Studio at a Jx project already on your machine. On studio.jxsuite.com the same button lists the GitHub repositories you can write to instead — pick one and Studio opens it, or use the links in the picker's footer to grant the Jx Suite GitHub App access to more of them. Recently opened projects also appear on the Welcome screen for one-click reopening.\n3. **Clone a repository.** Click **Clone Git Repository…**, paste a repository URL, and click **Clone** — Studio downloads the project and opens it. On platforms linked to a GitHub account, **Add Existing Repository…** lets you pick from your repositories instead of pasting a URL.\n\n:::doc-note\nBecause a project is just a folder, \"moving to Jx\" or \"leaving Jx\" is copying files. Studio never locks your work into a format only it can read.\n:::\n\n## Next\n\n- **[Create a project](/docs/studio/projects/create)** — the New Project modal, field by field\n- **[Browse your project](/docs/studio/projects/browse)** — the Manage view with live previews\n- **[Publish](/docs/studio/publish)** — commit your project and put it live\n",
    "section": "Studio",
    "slug": "studio/projects",
    "title": "Projects",
    "url": "https://jxsuite.com/docs/studio/projects/"
  },
  {
    "description": "Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.",
    "markdown": "\n# Create a project\n\nThe New Project modal is a two-step wizard: first you choose what to start from, then you fill in the project's details and an optional design quickstart. Open it from the [Welcome screen](/docs/studio/interface/welcome-screen) with **New Project…**, or from the dropdown beside **Open Project** in the toolbar.\n\n![The New Project dialog with the template gallery, name, directory, production URL, and deployment adapter fields](../../images/new-project-modal.png)\n\n## Step 1: choose a source\n\nThe first screen — **Start new project from:** — has up to four tabs:\n\n- **Template** — a minimal skeleton to build on. Four options:\n  - **Blank** — start from scratch.\n  - **Desktop First** — an empty project preset with max-width breakpoints, so you design the large layout first and adapt down.\n  - **Mobile First** — the same, with min-width breakpoints: design the phone layout first and adapt up.\n  - **Mobile App** — an app shell with bottom navigation.\n- **Starter Site** — a complete, themed website (restaurant, shop, portfolio, blog, and more) that Studio copies in as plain files you own. Browse the full gallery in **[Starter templates](/docs/studio/projects/starters)**.\n- **Import** — recreate an existing website as a Jx project. Give Studio the site's URL and how many pages to crawl; the import is AI-assisted, so this tab asks for an AI provider key before it unlocks. Not every Studio platform offers this tab.\n- **Agent** — describe the site you want in a sentence or two and the AI assistant builds it in the editor while you watch. Like Import, it needs an AI provider key first.\n\nPick a source and click **Next**.\n\n## Step 2: fill in the parameters\n\nThe second screen — **New Project Parameters** — shows which source you picked, then the project's identity:\n\n1. **Project Name** (required) — the human-readable name, e.g. \"My Site\".\n2. **Location** (required) — the existing folder to create the project folder inside, e.g. `/home/you/Sites`. **Browse…** opens your system's folder picker. In a browser that has no folder-picking support, the button is hidden and you type the path instead.\n3. **Directory** — the folder name for the project. Studio derives it from the name as you type (`My Site` becomes `my-site`); edit it to take over.\n4. **Description** — a short line about the site.\n5. **Production URL** — where the site will live once published, e.g. `https://example.com`.\n6. **Deployment Adapter** — how the build packages the site for your host: **Static**, **Cloudflare Pages**, **Node**, or **Bun**. **Static** emits plain files for any host that serves them, which is all a site of pages and content needs. Pick one of the others if the site will have a database, sign-ins, or server functions: those adapters also package the small server that answers those requests. A database or sign-ins make that mandatory — the build refuses to run on **Static**. Server functions still build there; they just never get served. The choice isn't final — change it later in **[Project settings](/docs/studio/projects/settings)**.\n\nUnder **Location** and **Directory**, Studio shows exactly where the project will land — `/home/you/Sites/my-site` — before you commit to it.\n\nOn the Import tab, this step shows only the name, location, and directory plus the import's progress — the remaining fields and the design quickstart don't apply.\n\n### On Jx Cloud\n\nCloud projects are GitHub repositories, so the same step asks for a **repository location** instead of a folder:\n\n1. **Owner** (required) — the account the repository is created under: your personal account, or any organization you've installed the Jx Suite app on.\n2. **Repository** — the repository name, derived from the project name the same way the directory is. Studio warns you if that name is already taken under the chosen owner.\n3. **Visibility** — **Private** (the default) or **Public**.\n\nThe preview line shows the repository that will be created, e.g. `acme/my-site`.\n\n## The design quickstart\n\nBelow the parameters sits a creation-time subset of the design settings. Every field is optional — anything you leave empty keeps the template or starter's own defaults:\n\n- **Colors** — pick an **Accent**, **Background**, and **Text** color with a swatch or by typing a value.\n- **Fonts** — a **Body Font** and **Heading Font**.\n- **Logo** — upload an image (SVG, PNG, JPEG, WebP, GIF, or ICO); Studio copies it into the project's `public/` folder.\n- **Breakpoints** — the screen sizes your design responds to, as name/value rows. Templates prefill their preset; for a starter site, leave this empty to keep the starter's own breakpoints. **Add Breakpoint** adds a row.\n\n## Create it\n\nClick **Create Project** (or **Create & Start Agent** on the Agent tab). Studio writes the project folder and opens it. **Back** returns to the source step without losing what you've typed.\n\nIf the **Project Name** is missing, the error appears directly under that field. A missing or non-absolute **Location** (or, on the cloud, a missing **Owner**) is reported under the destination fields. If creation itself fails, the message appears just above the footer buttons, so it stays visible however far the form is scrolled.\n\n:::doc-note\nStudio creates the folder you named, with `pages/`, `components/`, and a `project.json` carrying your name, URL, adapter, and design choices. The full folder anatomy is documented in [Site architecture](/docs/framework/site).\n:::\n\n:::doc-warning\nThere is no default location. Studio never picks a folder for you and never falls back to whatever directory it happens to be running in — if the **Location** is empty, nothing is created.\n:::\n\n## Next\n\n- **[Projects](/docs/studio/projects)** — the other ways to get a project: open a folder or clone a repository\n- **[Browse your project](/docs/studio/projects/browse)** — find your way around what was just created\n- **[Starter templates](/docs/studio/projects/starters)** — the full starter gallery\n",
    "section": "Studio",
    "slug": "studio/projects/create",
    "title": "Create a project",
    "url": "https://jxsuite.com/docs/studio/projects/create/"
  },
  {
    "description": "When to use a page, a layout, or a component in Jx Studio, how to create each one from Manage, and where each lives in your project folder.",
    "markdown": "\n# Pages, layouts, and components\n\nEverything you build in Studio is one of a few kinds of file. Knowing which is which — and when to reach for each — is most of what there is to learn about structuring a Jx site.\n\n## Pages\n\nA page is one address on your site. Each file in your project's `pages/` folder becomes one URL: the file named `index` is your home page, `about` becomes `/about`, and a file inside a `blog/` subfolder becomes `/blog/…`. Add a page and you've added a place visitors can go; delete it and the address is gone.\n\nRouting details — dynamic addresses, catch-alls — live in **[Site architecture](/docs/framework/site)**.\n\n## Layouts\n\nA layout is the shared frame around your pages: the header, footer, and everything else that repeats on every page of a section. Layouts live in the `layouts/` folder, and each page picks the layout that wraps it — so changing the header in one layout changes it on every page that uses it.\n\nReach for a layout when you catch yourself rebuilding the same surroundings on a second page.\n\n## Components\n\nA component is a reusable building block — a card, a hero section, a testimonial, a navigation bar. Components live in the `components/` folder and can be placed on any page or layout, as many times as you like. Edit the component once and every place it appears updates.\n\nReach for a component when the same element shows up more than once, or when a page is getting big enough that you want to name its parts. The underlying idea is documented in **[Components](/docs/framework/concepts/components)**.\n\n## Create one\n\nAll three are created the same way, from the [Manage view](/docs/studio/projects/browse):\n\n1. Click **Manage** in the toolbar.\n2. Click **New** and choose **Page**, **Layout**, or **Component**. (The menu also lists your project's content types — see [Browse your project](/docs/studio/projects/browse).)\n3. Type a name in the dialog and click **Create**. Studio turns it into a file name (`About Us` becomes `about-us`), writes the file into the matching folder, and opens it in a tab, ready to edit.\n\n![Jx Studio Manage Files modal with live previews of every project file](../../images/mode-manage.png)\n\n:::doc-note\nStudio writes each new file into `pages/`, `layouts/`, or `components/` in your project folder — plain files you can rename, duplicate, or delete from Manage's right-click menu.\n:::\n\n## Which one do I want?\n\n- **A destination** people should be able to visit → a **page**.\n- **The frame** that repeats around many pages → a **layout**.\n- **An element** that repeats within pages → a **component**.\n\nWhen in doubt, start with a page. You can always select part of it later and grow that part into a component once it earns reuse.\n\n## Next\n\n- **[Design mode](/docs/studio/design)** — style what you just created on the live canvas\n- **[Edit mode](/docs/studio/editing)** — write content inline\n- **[Site architecture](/docs/framework/site)** — the folder-by-folder anatomy underneath\n",
    "section": "Studio",
    "slug": "studio/projects/pages-layouts-components",
    "title": "Pages, layouts, and components",
    "url": "https://jxsuite.com/docs/studio/projects/pages-layouts-components/"
  },
  {
    "description": "The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.",
    "markdown": "\n# Manage\n\nManage is your project's home base — the file layer and the CMS layer in one place. Open it from the toolbar to see everything your site is made of, with live previews.\n\n![Jx Studio Manage Files modal with live previews of every project file](../../images/mode-manage.png)\n\n## Browse your project\n\nThe Manage modal groups everything by kind — **Pages**, **Layouts**, **Components**, **Content**, and **Media** — with a live preview of each page and component and a thumbnail for every asset. Filter and search to jump to a file, and switch between grid and table views.\n\nRight-click any file to **open**, **rename**, **duplicate**, or **delete** it.\n\n## Create pages and content\n\nUse **New** to add a Page, Layout, or Component. Studio asks for a name in a dialog — it tells you which folder the file lands in, and turns what you type into a file name (`About Us` becomes `about-us`). Studio also lists an entry for each **content type** your project defines, so creating a blog post or a doc is one click — Studio pre-fills the frontmatter from that type's schema.\n\n## Upload media\n\nDrag images, video, audio, PDFs, or fonts into Manage and Studio writes them to your project's `public/` folder, ready to reference. The site build optimizes images automatically (responsive `srcset`, WebP/AVIF).\n\n## Model your content\n\nContent types are your CMS schema — each one defines where a collection lives, what format its entries use, and what fields they carry. Define and edit them in **[Content types](/docs/studio/projects/content-types)**.\n\n## Next\n\n- Author content in **[Edit](/docs/studio/editing)**\n- Design components in **[Design](/docs/studio/design)**\n",
    "section": "Studio",
    "slug": "studio/projects/browse",
    "title": "Browse your project",
    "url": "https://jxsuite.com/docs/studio/projects/browse/"
  },
  {
    "description": "The starter templates that ship with Jx Studio — what each includes and who it's for.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Starter templates\n\nJx Studio ships 12 starter templates. Pick one in the **New Project** dialog — Studio copies it into your project folder as a complete, editable site with real content, components, and styles. Nothing about a starter is special afterward: it is plain Jx files you own.\n\n## Bistro & Café\n\n**Restaurant & Food** — A menu-driven site for a restaurant, café, or bakery.\n\nA warm, photo-forward site for a restaurant or café. The menu is a Markdown content collection grouped by course, with hours, location, and a reservations call-to-action. A good showcase of content models and the image pipeline.\n\n- Menu content collection (Markdown)\n- Hours & location\n- Reservations CTA\n- Responsive photo gallery\n\n## Summit Heating & Cooling\n\n**Home Services & Trades** — Reliable HVAC repair and installation, backed by 24/7 service.\n\nA polished starter for a residential heating-and-cooling contractor, with a content-collection services grid, trust and testimonial sections, and prominent free-quote calls to action. Includes home, services, about, and contact pages plus a quote-request form.\n\n- Services grid powered by a content collection\n- Prominent \"Get a Free Quote\" CTAs and quote form\n- Customer testimonials and Licensed / Insured / 24-7 trust bar\n\n## Rivertown Foundation\n\n**Nonprofit & Community** — Neighbors helping neighbors across Rivertown.\n\nA warm, hopeful starter for a local community nonprofit, built around a programs content collection, impact stats, and a prominent Donate call-to-action. Includes home, programs, about, get-involved (volunteer + donation tiers), and contact pages.\n\n- Programs content collection\n- Impact stats\n- Prominent Donate CTA\n- Volunteer & donation tiers\n\n## Craft & Code Academy\n\n**Education & Courses** — Become a web developer in 14 weeks.\n\nA project-based online coding bootcamp with live mentorship and six guided modules. Features an interactive curriculum accordion, FAQ, and a clear enrollment path from first commit to a deployed full-stack capstone.\n\n- Interactive curriculum accordion\n- FAQ accordion\n- Module content collection\n- Pricing & enroll CTA\n\n## Flowlark\n\n**SaaS & Product** — Plan, ship, and scale in one place.\n\nA modern software product landing site with an interactive monthly/annual pricing toggle and an expandable FAQ accordion. Includes a feature grid, comparison table, logo cloud, and testimonials — all as plain Jx files.\n\n- Interactive pricing toggle (monthly/annual)\n- FAQ accordion island\n- Plan comparison table & feature grid\n\n## Horizon Conf 2026\n\n**Events & Conferences** — A landing site for a one-day tech and design conference.\n\nA bold, gradient-driven site for a one-day conference, with a sessions content collection and a reactive agenda-tabs island that filters the schedule by track in real time. Showcases interactive islands, content models, pricing tiers, and a countdown-style save-the-date band.\n\n- Sessions content collection (Markdown)\n- Interactive agenda-tabs island (filter by track)\n- Save-the-date countdown strip\n- Ticket pricing tiers\n\n## The Long Field\n\n**Blog & Publication** — Slow essays on design, technology, and craft.\n\nA personal, editorial blog template with a Markdown posts collection, dynamic article routes, and tags. Built for writers who want a fast, readable publication with a dedicated reading layout and a newsletter CTA.\n\n- Markdown posts collection\n- Dynamic article routes (one page per post)\n- Tags & dedicated reading layout\n- Newsletter subscription CTA\n\n## Aperture Studio\n\n**Creative & Portfolio** — Light, patience, and a good eye.\n\nAn image-forward photography and creative studio portfolio. Features a masonry project gallery, a Markdown-backed projects collection, and dynamic per-project case-study pages with cover, writeup, and gallery.\n\n- Masonry project gallery with hover-zoom cards\n- Dynamic per-project detail pages from a content collection\n- Elegant image-forward editorial design\n\n## Ember Yoga & Fitness\n\n**Health & Wellness** — Boutique yoga, pilates & strength for every body.\n\nA boutique yoga and fitness studio site with a weekly class schedule, flexible membership pricing, and online booking. Small-class, all-levels branding backed by a content-driven schedule, a side-by-side membership comparison table, and a styled booking form.\n\n- Class schedule collection\n- Membership pricing table & tier cards\n- Booking/contact form\n- Instructor profiles\n\n## Meridian Advisory\n\n**Professional Services** — Numbers that move the business forward.\n\nA boutique accounting and advisory firm site for founders and growing companies, with services, a team roster, and an insights library. Adapts cleanly for law and consulting practices with two layouts and per-article dynamic pages.\n\n- Two layouts (marketing shell + article reader)\n- Insights collection with dynamic per-article pages\n- Team roster, services grid, and trust strip\n\n## Cadence Cycles\n\n**Retail & Shop** — Find your next favorite ride.\n\nA neighborhood bike shop selling road, mountain, gravel, and kids' bikes plus gear and expert service. Features a filterable product grid with an interactive category island and dynamic per-product detail pages.\n\n- Filterable product collection grid\n- Reactive category-filter island\n- Dynamic per-product pages\n- On-site service booking\n\n## Northshore Realty\n\n**Real Estate** — Search homes on the north shore.\n\nA residential real-estate agency starter with a CSV-driven property listings collection and dynamic per-listing detail pages. Its split search hero, neighborhood tiles, and an interactive sidebar filter island update results instantly, no page reloads.\n\n- CSV listings collection\n- Split search hero + sidebar filter island\n- Neighborhood tiles & data-forward listing cards\n- Dynamic per-listing detail pages with gallery\n",
    "section": "Studio",
    "slug": "studio/projects/starters",
    "title": "Starter templates",
    "url": "https://jxsuite.com/docs/studio/projects/starters/"
  },
  {
    "description": "Upload images, video, audio, and fonts in Jx Studio's Manage view, pick them with the media picker, and let the build optimize every image.",
    "markdown": "\n# Media\n\nYour site's media — images, video, audio, PDFs, and fonts — lives in the project's `public/` folder, and the Manage view is where you put it there. Open Manage with the **Manage** button in the toolbar and click the **Media** filter to see every asset with a thumbnail.\n\n![The Manage view with live previews and thumbnails of every project file](../../images/mode-manage.png)\n\n## Upload files\n\n1. Drag files from your computer anywhere onto the Manage view — or click **Upload** and pick them in the file dialog. You can drop several at once.\n2. The new files appear in the **Media** section immediately, ready to use.\n\nStudio accepts images (including SVG), video, audio, PDFs, and font files. Right-click any asset to **rename**, **duplicate**, or **delete** it. Renaming preselects just the name, so typing replaces `hero` in `hero.jpg` and leaves the extension alone.\n\n![The Manage view highlighted as a drop target while files are dragged onto it](../../images/media-upload.png)\n\n:::doc-note\nUploads are written to your project's `public/` folder under their own name, and everything in `public/` is served from your site's root: `public/hero.jpg` becomes `/hero.jpg` on the published site. The folder layout is documented in [Site architecture](/docs/framework/site).\n:::\n\n## Pick media in a panel\n\nWherever Studio asks for an image or file — an image's **Properties**, a frontmatter field, the **Document** activity's icon and social-image fields — the same media picker appears: a thumbnail of the current file, a path field, and a browse button.\n\n1. Click the browse (image) button beside the field.\n2. A searchable list of your project's media opens, with a thumbnail beside each image. Type to filter.\n3. Click a file to use it — Studio fills in its path for you.\n\nYou can also type into the path field directly, either a path from your project or a full web address.\n\n![The media picker popover open over an image's Properties, showing the search field and thumbnail list](../../images/media-picker.png)\n\n## What the build does to images\n\nYou only ever upload one copy of an image, at full quality. When your site is built for publishing, each image is optimized automatically: the build generates multiple sizes and modern formats (WebP, AVIF) and wires them up so every visitor's browser downloads the smallest version that looks sharp on their screen. There is nothing to configure in Studio — the pipeline is described in **[Site architecture](/docs/framework/site)**.\n\n## Next\n\n- **[Browse your project](/docs/studio/projects/browse)** — the rest of the Manage view\n- **[Publish](/docs/studio/publish)** — how the optimized site goes live\n",
    "section": "Studio",
    "slug": "studio/projects/media",
    "title": "Media",
    "url": "https://jxsuite.com/docs/studio/projects/media/"
  },
  {
    "description": "Define content types in Jx Studio — a source folder, a format, and a field schema — and create schema-backed entries from the Manage view.",
    "markdown": "\n# Content types\n\nContent types are your site's CMS schema. Each one describes a collection — blog posts, team members, projects — by naming the folder its entries live in, the file format they use, and the fields every entry carries. Once a type exists, creating an on-model entry is one click. Open the builder from the **Settings** gear at the bottom of the activity bar, then _Settings > Content Types_: your types are listed on the left, and selecting one opens its editor on the right.\n\n![The Content Types section with a type selected and its field schema open in the builder](../../images/content-type-builder.png)\n\n## Create a content type\n\n1. Click **New Entry** at the bottom of the type list.\n2. Type a name — \"Blog Posts\" becomes `blog-posts` — and click **Create**.\n\nThe new type starts with a matching source folder (`content/blog-posts/`) and an empty field schema, and is selected ready to edit.\n\n## Source and format\n\n- **Source** — the project folder entries are read from. The default matches the type's name; change it to point anywhere inside your project.\n- **Format** — the file format entries use, by name (for example Markdown or Csv). Leave it empty and the format is worked out from each file's extension.\n\n## Build the field schema\n\nThe fields you add here become the form every entry fills in. Each field row has:\n\n- A **name** — click it to rename.\n- A **type** — string (text), number, boolean (yes/no), array (a list), object (a group of sub-fields), or reference.\n- A **format** for string and array fields — **image**, **date**, or **color** — which upgrades the field's editor: image fields get the [media picker](/docs/studio/projects/media), date fields a date, and so on.\n- A **Req** toggle marking the field as required.\n\nAn **object** field opens a nested area where you add sub-fields the same way. A **reference** field gets a **Target** picker naming another content type — that's how a post points at its author. The trash icon deletes a field.\n\n## Rename or delete a type\n\nWith a type selected, edit its name in the editor's header to rename it, or click the trash icon beside the name to delete it. Deleting the type does not delete the entry files in its source folder.\n\n## Create entries from Manage\n\nBack in the [Manage view](/docs/studio/projects/browse), the **New** menu lists an item for every content type you've defined:\n\n1. Click **New** and pick the type — for example **Blog-posts**.\n2. Name the entry in the dialog and click **Create**. Studio creates the file in the type's source folder with every schema field pre-filled with a sensible blank, and opens it.\n\nEntries show up under Manage's **Content** filter labeled with their type, and when you open one, Studio recognizes which type it belongs to and presents its fields as a form — see **[Frontmatter and page metadata](/docs/studio/editing/frontmatter)**.\n\n:::doc-note\nStudio stores your types in the `content` section of `project.json` — one entry per type, recording its `source` folder, `format`, and field `schema`. Pages query these collections to list and display entries; see [Site architecture](/docs/framework/site).\n:::\n\n## Next\n\n- **[Browse your project](/docs/studio/projects/browse)** — the Manage view where entries are created\n- **[Project settings](/docs/studio/projects/settings)** — the rest of the Settings modal\n",
    "section": "Studio",
    "slug": "studio/projects/content-types",
    "title": "Content types",
    "url": "https://jxsuite.com/docs/studio/projects/content-types/"
  },
  {
    "description": "A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.",
    "markdown": "\n# Project settings\n\nThe Settings modal holds everything that applies to your whole site rather than one page — the favicon, fonts, design tokens, and dependencies. Open it with the **Settings** gear at the bottom of the activity bar; a navigation list on the left switches between sections.\n\n![The Settings modal open on the General section, with the section list on the left](../../images/settings-modal.png)\n\nChanges save as you make them — there is no separate save button.\n\n## General\n\nThe basics of the site:\n\n- **Favicon** — click **Upload Favicon** and pick an image; Studio copies it into your project and shows the current one beside the button.\n- **Platform Adapter** — how the build packages the site for your host: **Static**, **Bun**, **Node**, **Cloudflare Workers**, or **Cloudflare Pages**. This is the same choice you made when [creating the project](/docs/studio/projects/create). **Static** emits plain files for any host that serves them; the other four additionally package the site's server tier, which is what answers a database, sign-ins, or server functions. One of them becomes _required_ once the project has a database or sign-ins — the build stops with an error on **Static**. Server functions still build on **Static**, but only these four actually serve them. See [Build output and adapters](/docs/framework/site/deployment) for what each one writes into `dist/`.\n- **Breakpoints** — the screen sizes your design responds to, as name/value rows. The **Base** row is your default canvas width; **+ Add Breakpoint** adds another, and the × button removes one.\n- **Global Styles** — a shortcut that opens the project file where site-wide default styles live.\n\n## Head\n\nWhat goes into every page's `<head>` — the invisible part of a web page that loads fonts, styles, and services:\n\n- **Google Fonts** — type a font family name and click **+ Add** to load it across the whole site. Loaded fonts are listed with a delete button each.\n- **Head tags** — add a **Link** (external stylesheet), **Meta** (page metadata), **Script**, or **Style** entry and fill in its fields. Script and Style entries get a text box for their body — this is where an analytics snippet or a custom style block goes.\n\n## CSS Variables\n\nYour design tokens — the named colors, fonts, and sizes your styles refer to, grouped into **Colors** (each with a color swatch you can click to pick), **Fonts** (each with a live preview line), **Sizes & Spacing**, and **Other**. Edit a value and every element that uses the token updates; sizes can also carry per-breakpoint overrides so spacing tightens on small screens.\n\n## Definitions\n\nReusable field schemas — descriptions of data shapes (an API response, a shared record type) that other parts of the project can refer to. Definitions use the same visual field builder as content types: named fields with a type, an optional format, and a required toggle. Most sites never need this section; for modeling your content itself, use **[Content types](/docs/studio/projects/content-types)** instead.\n\n## Dependencies\n\nThe npm packages your project uses, as a table of name, current version, and the latest available version:\n\n- Type a package name and click **Add** to install one.\n- A refresh icon appears on any row with an update available; **Update all** takes every row to its latest at once.\n- **Reinstall** re-installs everything from scratch — the fix-it button if packages ever end up in a bad state.\n\nAdding packages and choosing which of their components your site uses is covered in **[Dependencies and imports](/docs/studio/projects/dependencies)**.\n\n## Sections added by extensions\n\nExtensions can contribute their own settings sections, which appear in the same list. **Content Types** — the section where you model your site's content — arrives this way, and has its own page: **[Content types](/docs/studio/projects/content-types)**.\n\n:::doc-note\nEvery section of this modal edits `project.json` at the root of your project — the same file the New Project modal first wrote. The full shape is documented in [Site architecture](/docs/framework/site).\n:::\n\n## Next\n\n- **[Content types](/docs/studio/projects/content-types)** — model your content in the Content Types section\n- **[Dependencies and imports](/docs/studio/projects/dependencies)** — packages and component imports in depth\n",
    "section": "Studio",
    "slug": "studio/projects/settings",
    "title": "Project settings",
    "url": "https://jxsuite.com/docs/studio/projects/settings/"
  },
  {
    "description": "The Imports activity in Jx Studio: add npm packages, cherry-pick their components for the site or one document, and keep everything up to date.",
    "markdown": "\n# Dependencies and imports\n\nImports is where you decide which building blocks a document — or the whole site — can use: components from your own project, and components from npm packages (the web's public library of ready-made building blocks). Open it by clicking **Imports** (the box icon) in the activity bar.\n\n![The Imports activity showing a package section with per-component checkboxes](../../images/imports-panel.png)\n\n## Two contexts\n\nThe panel follows whatever tab is active:\n\n- **A page, layout, or component open** — you're managing that document's imports: which components it can place.\n- **`project.json` open** — you're managing the whole site: packages, site-wide component availability, and class imports.\n\n## Import your own components\n\nWith a document open, the **Components** section lists what it already imports. Use the **Add component…** picker to import another component from your project — it becomes available to place in that document. The × beside an entry removes the import.\n\nIf you mostly build by placing blocks from the [Elements panel](/docs/studio/design/elements), you rarely open this list — it's the same wiring, made visible.\n\n## Add an npm package\n\nWith `project.json` open:\n\n1. Type the package's name into **Add Dependency** and press :kbd[Enter].\n2. Studio installs it. If the package publishes a component catalog (a custom elements manifest), a new section appears for it, listing every component inside.\n\nPackages can also be added from _Settings > Dependencies_ — same result, different door. See **[Project settings](/docs/studio/projects/settings)**.\n\n## Cherry-pick components\n\nUnder each package section, every component has a checkbox. Nothing from a package is available until you tick it — you pick exactly the pieces you want instead of importing the whole library:\n\n- Tick a component in the **`project.json`** context to make it available across the site.\n- Tick it with a page, layout, or component open to enable it for that document only.\n\nThe × in a package's header (site context) removes the package entirely, along with everything you'd picked from it.\n\n## Class imports\n\nThe site context also shows **Class Imports** — named behaviors your project's logic can refer to. This is advanced territory; most visual projects never touch it.\n\n## Stay up to date\n\n- _Settings > Dependencies_ shows each package's current and latest version, with per-row update buttons and **Update all**.\n- When you open a project built with an older version of Jx, Studio offers to update its Jx packages to match — accept, and it rewrites the versions and reinstalls for you. Decline, and it won't ask again for that version.\n- If a project's packages have never been installed on this machine (say, you just cloned it), Studio installs them automatically before the editor loads.\n\n:::doc-note\nPackages are recorded in your project's `package.json`; the components you tick are recorded as `$elements` entries — in `project.json` for site-wide picks, in the document's own file for per-document picks.\n:::\n\n## Next\n\n- **[Project settings](/docs/studio/projects/settings)** — the Dependencies section of the Settings modal\n- **[Pages, layouts, and components](/docs/studio/projects/pages-layouts-components)** — what components are and when to make your own\n",
    "section": "Studio",
    "slug": "studio/projects/dependencies",
    "title": "Dependencies and imports",
    "url": "https://jxsuite.com/docs/studio/projects/dependencies/"
  },
  {
    "description": "Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.",
    "markdown": "\n# Edit mode\n\nEdit is for writing. Open a content page and the canvas becomes the page itself — click any text and type, right where it renders. It feels like working in a doc editor; it saves as clean Markdown.\n\n![Jx Studio editing markdown content inline with a WYSIWYG editor](../images/mode-edit.png)\n\nMarkdown pages open in Edit automatically. For anything else, pick **Edit** in the mode switcher at the right end of the toolbar — see **[Modes and the preview toggle](/docs/studio/interface/modes)**.\n\n## What lives here\n\n- **[Writing and formatting](/docs/studio/editing/writing)** — typing on the page, paragraphs, the inline formatting toolbar, links, pasting, and editing text inside components.\n- **[Slash commands](/docs/studio/editing/slash-commands)** — type `/` to insert headings, lists, images, tables, and more without leaving the keyboard.\n- **[Frontmatter and page metadata](/docs/studio/editing/frontmatter)** — the **Document** activity's forms for titles, descriptions, social-share cards, and your content type's fields.\n- **[Grid mode](/docs/studio/editing/grid)** — a spreadsheet view for whole collections, CSV files, and page metadata, with one batched Save.\n\n## It's just Markdown\n\nEverything you write saves as standard Markdown with your metadata on top. Open the file in any editor, diff it in git, or hand it to an AI — it's plain text, and it's yours. Switch the canvas to **Code** any time to see exactly what Studio wrote.\n\n## Next\n\n- Structure and style the page in **[Design mode](/docs/studio/design)**\n- Organize the content files themselves in **[Browse your project](/docs/studio/projects/browse)**\n",
    "section": "Studio",
    "slug": "studio/editing",
    "title": "Edit mode",
    "url": "https://jxsuite.com/docs/studio/editing/"
  },
  {
    "description": "Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.",
    "markdown": "\n# Writing and formatting\n\nIn Edit mode you write on the real page: click any text — a heading, a paragraph, a list item, a table cell — and the cursor lands exactly where you clicked, ready to type. There is no separate preview to keep in sync; the page you're editing is the page.\n\n![A paragraph mid-edit on the canvas with the formatting toolbar above it](../../images/inline-editing.png)\n\n## Moving around\n\nThere is nothing to start or stop. The cursor is simply on the page, the way it is in a document editor:\n\n- **Click** any text to put the cursor there.\n- **Arrow keys** move it through the whole page, from the end of one block into the next.\n- :kbd[Home] / :kbd[End], word motion, and :kbd[Page Up] / :kbd[Page Down] all work as they do anywhere else.\n- **Drag** — or hold :kbd[Shift] and move — to select, including across several blocks at once.\n- :kbd[Esc] puts the cursor away.\n\nYour writing is saved into the document as you pause, so :kbd[⌘S] (macOS) / :kbd[Ctrl+S] (Windows/Linux) always writes what is on screen, mid-sentence or not.\n\n## What you can click into\n\nAnything that holds text: headings, paragraphs, list items, table cells, captions, definition\nlists, a disclosure's summary. Which ones those are comes from the page's own format rather than a\nfixed list, so a Markdown page and a component page each get the right answer.\n\nTwo cases where the cursor lands somewhere you might not expect, and both are deliberate:\n\n- **A block quote** holds paragraphs, so clicking one puts the cursor in the paragraph inside it —\n  which is the thing you actually want to type in.\n- **A link** is part of a paragraph, not a block of its own. Clicking one puts the cursor in the\n  paragraph, so you can type through and around the link and it stays intact.\n\nCode blocks are not editable this way; their whitespace is significant, so they are left alone.\n\n## Paragraphs\n\nPress :kbd[Enter] to end the paragraph and start a new one. Split a paragraph in the middle and everything after the cursor moves into the new one, with your cursor following — you just keep typing. :kbd[Shift+Enter] stays in the same paragraph instead of starting a new block.\n\n:kbd[Backspace] at the very start of a block joins it onto the one above, and :kbd[Delete] at the very end pulls the next one up — the cursor lands where the two met. Deleting a selection that spans blocks does the same thing: what is left of the first and last block joins together.\n\nTo make the next block something other than a paragraph — a heading, a list, an image — type :kbd[/] and pick from the menu: **[Slash commands](/docs/studio/editing/slash-commands)**.\n\n## The formatting toolbar\n\nThe floating toolbar above the block carries the formatting buttons (the same bar described in **[The canvas](/docs/studio/interface/canvas)**). Select some text first, then click a button to format it — click again to remove the format:\n\n- Paragraphs, list items, and table cells offer **Bold**, **Italic**, **Underline**, **Strikethrough**, **Superscript**, **Subscript**, **Code**, and **Link**.\n- Headings offer the shorter set: **Bold**, **Italic**, **Code**, and **Link**.\n\nWith nothing selected the format buttons are disabled — they act on a range — and only **Link** stays clickable. The keyboard versions: :kbd[⌘B] / :kbd[Ctrl+B] for bold, :kbd[⌘I] / :kbd[Ctrl+I] for italic, :kbd[⌘`] / :kbd[Ctrl+`] for code.\n\n## Links\n\n1. Select the text to link.\n2. Click the **Link** button, or press :kbd[⌘K] / :kbd[Ctrl+K].\n3. Type the address and press :kbd[Enter] (or click **Apply**).\n\nPut the cursor inside an existing link and open the same popover to see its address — **Update** changes it, **Remove** unlinks the text while keeping the words.\n\n## Insert data\n\nThe **Insert data** button beside the format group opens a searchable list of the data available on your page. Pick an entry and Studio drops a live placeholder into your sentence — it shows the real value when the page renders. Inside a repeating list you also get the current item's fields and its position. Where that data comes from is covered in **[Logic](/docs/studio/logic)**.\n\n## Pasting\n\nPaste is always plain text: copy from a website or a Word document and you get the words — never the fonts, colors, or stray markup they were wrapped in. Add your own formatting after pasting.\n\n## Text inside components\n\nClick text inside a component instance — a card title, a button label — and you edit that one instance's text in place. It's a single plain value rather than free-form content, so the rules tighten:\n\n- :kbd[Enter] finishes and keeps the change; :kbd[Esc] cancels it.\n- Formatting, slash commands, and paragraph splits are off.\n- The toolbar's name badge shows which component option you're editing (for example `product-card · title`).\n\nText that a component fills from data can't be edited this way — typing over it would break its connection to the data. Change the source data instead, or click **Edit Component** in the toolbar to open the component itself.\n\n:::doc-note\nBehind the scenes, Studio keeps the markup tidy as you type — adjacent formats merge, empty leftovers are removed — and saves the result as plain Markdown in the page's file, so bold is just `**bold**` on disk.\n:::\n\n## Next\n\n- Insert whole blocks from the keyboard with **[Slash commands](/docs/studio/editing/slash-commands)**\n- All the keys in one place: **[Keyboard shortcuts](/docs/studio/interface/shortcuts)**\n",
    "section": "Studio",
    "slug": "studio/editing/writing",
    "title": "Writing and formatting",
    "url": "https://jxsuite.com/docs/studio/editing/writing/"
  },
  {
    "description": "Insert headings, lists, images, tables, and more from the keyboard with Jx Studio's slash menu — the full block list and how inserting works.",
    "markdown": "\n# Slash commands\n\nThe slash menu inserts blocks without leaving the keyboard: while editing text, type `/` and a menu of everything you can insert opens under your cursor. It's the fastest way to build a page up from a blank paragraph.\n\n![The slash menu open under an empty paragraph, showing the block list](../../images/slash-menu.png)\n\n## Insert a block\n\n1. While editing text, type :kbd[/] on an empty line or after a space.\n2. Keep typing to filter the list — `he` narrows to the headings, `img` finds Image.\n3. Move through the matches with :kbd[↓] and :kbd[↑], then press :kbd[Enter] — or click an entry.\n4. The block appears and you're already editing it — keep typing.\n\nOn an empty paragraph, the paragraph itself becomes the block you chose. On a line that has text, the text stays where it is and the new block lands right after it.\n\nTo close the menu without inserting anything, press :kbd[Esc], click anywhere else, or delete back past the `/`. And a slash in the middle of a word — \"and/or\" — just types a slash; the menu only opens at the start of a line or after a space.\n\n## The block list\n\n| Command             | What you get      |\n| ------------------- | ----------------- |\n| **Heading 1**       | Large heading     |\n| **Heading 2**       | Medium heading    |\n| **Heading 3**       | Small heading     |\n| **Paragraph**       | Plain text        |\n| **Bulleted List**   | Unordered list    |\n| **Numbered List**   | Numbered list     |\n| **Blockquote**      | Quote block       |\n| **Image**           | Image             |\n| **Horizontal Rule** | Divider line      |\n| **Button**          | Button element    |\n| **Link**            | Anchor link       |\n| **Code Block**      | Preformatted code |\n| **Table**           | Table             |\n| **Div**             | Container         |\n| **Section**         | Section container |\n\nFiltering matches either the name or the element's short tag, so `ol` finds Numbered List and `h1` finds Heading 1.\n\n:::doc-tip\nThe same menu appears when you click the name badge on the block action bar to convert a selected element into something else — see **[The canvas](/docs/studio/interface/canvas)**.\n:::\n\n## Next\n\n- Everything else about writing on the page: **[Writing and formatting](/docs/studio/editing/writing)**\n- Other ways to add elements — the + affordance and drag and drop — in **[The canvas](/docs/studio/interface/canvas)**\n",
    "section": "Studio",
    "slug": "studio/editing/slash-commands",
    "title": "Slash commands",
    "url": "https://jxsuite.com/docs/studio/editing/slash-commands/"
  },
  {
    "description": "Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.",
    "markdown": "\n# Frontmatter and page metadata\n\nEvery page carries information that isn't part of its visible text: a title, a description for search engines, an image for social shares, and — for content like blog posts — fields such as a date, an author, or tags. This is the page's _frontmatter_, and Studio edits all of it as plain forms.\n\nTwo surfaces show these fields: the **Document** activity in the left panel, and a **Properties** bar above the canvas for content that belongs to a collection.\n\n## The Document panel\n\nClick **Document** in the activity bar to open it. Its sections, top to bottom, depend on the file you have open:\n\n![The Document activity showing Frontmatter, Page, and OpenGraph sections for a blog post](../../images/document-panel.png)\n\n### Frontmatter\n\nFor a content page that belongs to a collection — a blog post, a product, a listing — this section lists the collection's fields, with required ones marked `*`. Each field gets a control that fits its type:\n\n- Text fields, number fields, and date fields (dates as `YYYY-MM-DD`)\n- On/off checkboxes\n- A dropdown for fields with a fixed set of choices\n- A media picker for image fields\n- Comma-separated entry for list fields\n\nAny extra field already present in the file appears too, even if the collection doesn't define it. Clearing a field removes it from the file entirely. Collections and their fields are defined in **[Content types](/docs/studio/projects/content-types)**.\n\n### Layout\n\nSite pages also get a **Layout** picker: keep the project default, choose a specific layout, or pick **None** — see **[Pages, layouts, components](/docs/studio/projects/pages-layouts-components)**.\n\n### Page\n\nThe basics every page should have:\n\n- **Title** — the browser-tab and search-result title.\n- **Description** — the summary search engines show under the title.\n- **Viewport** — leave the suggested default unless you have a specific reason not to.\n- **Icon** — the small icon in the browser tab, picked from your media.\n\n### OpenGraph\n\nThe card shown when the page is shared on social platforms: **Title**, **Description**, **Image**, and **Type**. If you fill in nothing else, fill in these and the Page section — they're what links to your site look like elsewhere.\n\n### Custom Tags\n\nThe escape hatch for everything else that can live in a page's head: pick a tag (`meta`, `link`, or `script`), type its attribute and value, and click the add button. Existing custom entries are listed with a remove button each. Most sites never need this section.\n\n## The Properties bar\n\nWhen a collection page is open in Edit mode, a **Properties** bar sits directly above the page — the same fields as the Document panel's Frontmatter section (title included), right where you're writing. Click its header to collapse it to a slim strip; every tab remembers whether you left it open or closed.\n\n![The collapsible Properties bar above a post open in Edit mode](../../images/properties-bar.png)\n\n:::doc-note\nOn disk, all of this lives at the top of the page's own file, in a small labeled block above the content — the frontmatter. That's why metadata travels with the page: copy the file and everything comes along, and any text editor can read it.\n:::\n\n## Next\n\n- Edit a whole collection's frontmatter at once in **[Grid mode](/docs/studio/editing/grid)**\n- Define collections and their fields in **[Content types](/docs/studio/projects/content-types)**\n",
    "section": "Studio",
    "slug": "studio/editing/frontmatter",
    "title": "Frontmatter and page metadata",
    "url": "https://jxsuite.com/docs/studio/editing/frontmatter/"
  },
  {
    "description": "Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.",
    "markdown": "\n# Grid mode\n\nGrid turns tabular content into a spreadsheet on the canvas: rows and columns, range selection, copy and paste, and a single **Save** that writes every pending change at once. Reach for it when editing entries one file at a time is too slow — renaming a category across fifty posts, or fixing prices in a product sheet.\n\n![A CSV content collection open as an editable grid in Jx Studio](../../images/grid-mode.png)\n\n## Open a grid\n\n- **A CSV file** — open it from **Files** (or Quick Access); spreadsheet files open straight into Grid.\n- **A content collection** — right-click the collection's folder in **Files** and choose **Edit Collection in Grid**.\n- **All pages** — right-click the `pages` folder and choose **Edit Pages in Grid**.\n- **From Project settings** — the **Open Data Grid** button in the data sections opens a picker that lists pages, every collection, and connected database tables in one place.\n\n## Edit cells\n\nDouble-click a cell to edit it. Columns are typed, so each kind gets the right control:\n\n- Text and number cells — type; number cells clean up currency symbols and commas for you.\n- On/off cells — a checkbox; choice cells — a dropdown; date cells — a date field.\n- List cells — chips: :kbd[Enter] or a comma adds one, :kbd[Backspace] removes the last, and each chip has its own remove button.\n- Image cells open the media picker, and relationship cells — a field that points at another collection — open a picker of that collection's entries.\n\nEdited cells are highlighted, and nothing touches your files yet — everything waits for **Save**. Undo and redo (:kbd[⌘Z] / :kbd[Ctrl+Z]) work on grid edits like anywhere else.\n\n## Work in ranges\n\n- Drag across cells to select a range. Copy and paste work on ranges — including pasting rows copied from a real spreadsheet.\n- :kbd[Delete] or :kbd[Backspace] clears the selected cells, as one undoable step.\n- **Fill Down** copies the range's first row into every row below it (:kbd[⌘D] / :kbd[Ctrl+D]).\n- **Replace** opens find & replace across all text cells; **Replace All** buffers the whole replacement as one undoable change — save to apply it.\n- **Filter rows** searches across every column; each column header also has its own filter box and click-to-sort.\n- Drag column headers to reorder them and drag their edges to resize — each grid remembers its column layout.\n\n## Add and remove rows\n\n- **Add Row** appends a pending row. In a collection or pages grid, fill in its **Path** cell — the file the new entry will become.\n- **Delete Rows** marks the selected rows for deletion; they're removed when you save, after a confirmation.\n- Added and marked rows are tinted until you save.\n\n## Save — one batch\n\nNothing writes until you click **Save** — the button counts your pending changes (:kbd[⌘S] / :kbd[Ctrl+S] does the same). On save, Studio checks required cells, confirms any deletions, writes everything, and reports what saved. If a change can't be written, that cell stays pending with the reason attached — fix it and save again.\n\nStudio also protects you from crossed wires:\n\n- A row whose file is open in a tab with unsaved changes won't save — save or close that tab first.\n- If a file changed on disk after the grid loaded (a teammate's edit, for example), its row is marked stale and skipped rather than overwritten. **Refresh** reloads from disk — asking first if you have pending edits.\n\n:::doc-warning\nSaving with rows marked for deletion permanently deletes those entry files. Studio asks for confirmation before it does.\n:::\n\n## Collection and pages grids\n\nA collection grid shows one row per entry file and one column per field of the collection's content type, plus any extra fields found in the entries — the **Path** column stays pinned at the left. The pages grid does the same for every Markdown page under `pages/`, with title and description first. Editing a cell edits that entry's **[frontmatter](/docs/studio/editing/frontmatter)**; collections themselves are defined in **[Content types](/docs/studio/projects/content-types)**.\n\n:::doc-note\nSaving a collection or pages row rewrites that file's frontmatter block from scratch — hand-written comments and key order inside it aren't preserved. The \"rewrites frontmatter\" note in the grid toolbar is the reminder.\n:::\n\n## CSV grids\n\nA CSV file opens as a grid tab with **Code** as its raw-text alternate, and saves as one atomic step: if the file changed on disk underneath you, the save stops instead of overwriting it. Only the cells you actually edited are rewritten — untouched cells keep their exact original text, so a one-cell fix produces a one-cell diff.\n\n## Next\n\n- The keys the grid claims for itself, and the ones that stay global: **[Keyboard shortcuts](/docs/studio/interface/shortcuts)**\n- Bulk-edit the fields behind your content: **[Frontmatter and page metadata](/docs/studio/editing/frontmatter)**\n",
    "section": "Studio",
    "slug": "studio/editing/grid",
    "title": "Grid mode",
    "url": "https://jxsuite.com/docs/studio/editing/grid/"
  },
  {
    "description": "Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.",
    "markdown": "\n# Design mode\n\nDesign is the visual canvas — the mode for shaping structure and style. Switch to it with the **Design** button in the mode switcher on the right side of the toolbar, and Studio renders the open page or component with the real runtime, exactly as it will render in production, while you edit it directly.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../images/mode-design.png)\n\n## A canvas per breakpoint\n\nInstead of one canvas you resize, Design shows a live panel for **every breakpoint** your project defines — phone, tablet, and desktop side by side, each labeled with its name and width. Your responsive rules evaluate for real in each panel, so a change that only applies on small screens shows up only in the small-screen panel. Click a panel's header to make that breakpoint the active one for styling, and pan and zoom the surface to fit — see **[The canvas](/docs/studio/interface/canvas)** for the controls.\n\nBreakpoints themselves — where they come from and how overrides cascade — are covered in **[Breakpoints](/docs/studio/design/breakpoints)**.\n\n## The panels around the canvas\n\nDesign mode is the canvas plus a set of panels, each with its own page:\n\n- **[Layers panel](/docs/studio/design/layers)** — the page's structure as a tree: select, rename, reorder, and act on any element.\n- **[Elements panel](/docs/studio/design/elements)** — insert HTML elements and your components by click or drag.\n- **[Properties panel](/docs/studio/design/properties)** — attributes, link targets, component props, and page settings for the selection.\n- **[Style inspector](/docs/studio/design/style-inspector)** — visual CSS controls for the selection, section by section.\n- **[Hover states and selectors](/docs/studio/design/states-and-selectors)** — style `:hover`, `:focus`, and selectors of your own.\n- **[Design tokens](/docs/studio/design/tokens)** — name your colors, fonts, and sizes once and reuse them everywhere.\n- **[Stylebook](/docs/studio/design/stylebook)** — set the default look of every heading, button, and link in one catalog.\n- **[Working with components](/docs/studio/design/components)** — turn a selection into a reusable component, wire props and slots.\n- **[Repeaters](/docs/studio/design/repeaters)** — turn one element into a repeating, data-bound list.\n\n## Next\n\n- Learn the shared canvas interactions — selection, drag and drop, the context menu — in **[The canvas](/docs/studio/interface/canvas)**\n- Make it interactive in **[Script & logic](/docs/studio/logic)**\n- The underlying style format is documented in **[Styling](/docs/framework/concepts/styling)**\n",
    "section": "Studio",
    "slug": "studio/design",
    "title": "Design mode",
    "url": "https://jxsuite.com/docs/studio/design/"
  },
  {
    "description": "The Layers panel in Jx Studio: read your page's structure as a tree, select, rename, reorder by drag and drop, and act on any element.",
    "markdown": "\n# Layers panel\n\nLayers is the page's structure as a tree — every element in the open file, nested the way it nests on the page. Open it by clicking **Layers** in the activity bar on the left. Use it whenever the thing you want to grab is hard to click on the canvas: a wrapper with no visible edges, an element hidden behind another, or the exact parent in a deep stack.\n\n![The Layers panel showing a nested element tree with a selected row](../../images/layers-panel.png)\n\n## Read the tree\n\nEach row shows a badge and a name. The badge tells you what the row is:\n\n- The element's type — `div`, `h2`, `img`, and so on.\n- **↻** — a [repeater](/docs/studio/design/repeaters), an element that renders once per item of a list.\n- **⇄** — a switch, an element that shows one of several cases; each case appears as a child row named after the case.\n- **▣** — a slot, the placeholder where a component or layout receives content. Hover it to see the slot's name.\n\nText sits in its own dimmed, italic rows, and rows with children get a chevron — click it to collapse or expand that branch without changing the selection.\n\n## Select and navigate\n\nClick a row to select that element. The canvas pans to bring it into view, and the [Properties panel](/docs/studio/design/properties) and [Style inspector](/docs/studio/design/style-inspector) switch to it. Selection works in both directions: click something on the canvas and its row highlights and scrolls into view in Layers.\n\n## Rename an element\n\nRows are named automatically from the element's content. To give one a name of your own — \"Hero section\" beats a `div` with a text preview — double-click the row, type the name, and press :kbd[Enter]. Press :kbd[Esc] to cancel, or commit an empty name to go back to the automatic label.\n\n:::doc-note\nThe name is a display title only — Studio stores it as a `$title` key on the element in the file. It never changes what the page renders.\n:::\n\n## Rearrange the page\n\nHover a row and its actions appear on the right:\n\n- **Move up** / **Move down** arrows swap the element with its neighbors.\n- The **right arrow** moves the element inside the sibling above it (shown when that sibling can hold children).\n- The **left arrow** moves the element out of its parent, to sit just after it.\n- **✕** deletes the element and everything inside it.\n\nFor bigger moves, drag the **⠿** handle — or the row itself. An indicator shows where the element will land: above or below the row under the cursor, or inside it as a child. You can also drag a row straight onto the canvas and drop it at the spot you see. Press :kbd[Esc] mid-drag to cancel with nothing changed. Dropping an element into itself or its own descendants is blocked.\n\n## Right-click for everything else\n\nRight-clicking a row opens the same context menu as the canvas — **Copy**, **Duplicate**, **Copy styles**, **Wrap in Div**, **Repeat…**, **Convert to Component**, **Delete**, and more. The full list is in **[The canvas](/docs/studio/interface/canvas)**. **Set Title** in that menu starts the same rename as double-clicking.\n\nIn **Stylebook** mode the Layers activity switches to the element catalog instead of the document tree — see **[Stylebook](/docs/studio/design/stylebook)**.\n\n## Next\n\n- Add new elements from the **[Elements panel](/docs/studio/design/elements)**\n- Inspect what you selected in the **[Properties panel](/docs/studio/design/properties)**\n",
    "section": "Studio",
    "slug": "studio/design/layers",
    "title": "Layers panel",
    "url": "https://jxsuite.com/docs/studio/design/layers/"
  },
  {
    "description": "The Elements panel in Jx Studio: insert HTML elements and your project's components by clicking a card or dragging one onto the canvas.",
    "markdown": "\n# Elements panel\n\nElements is the palette of things you can add to the page — the standard HTML building blocks plus your own components, laid out as cards. Open it by clicking **Elements** in the activity bar on the left.\n\n![The Elements panel with the Components section and element category accordions open](../../images/elements-panel.png)\n\n## What's in the palette\n\n- **Components** — at the top, your project's own components, plus components from installed packages once they're enabled for the project (see **[Dependencies](/docs/studio/projects/dependencies)**). Project components render a live preview on their card, so you see the real thing before you place it.\n- **Element categories** — below, the HTML elements grouped by category in accordion sections. Each card shows a small preview of the element and its tag name.\n\nType in the **Filter elements…** box to narrow the palette by name; categories with no matches disappear. Click a category header to collapse or expand it.\n\n## Insert by click\n\n1. On the canvas or in [Layers](/docs/studio/design/layers), select the element you want to insert into.\n2. Click a card. The new element lands inside the selection, after its existing children.\n\nWith nothing selected, the element is added at the end of the document.\n\n## Insert by drag\n\nFor an exact placement, drag the card instead:\n\n1. Press and drag any card toward the canvas.\n2. As you move over the page, an indicator line shows where the element will land — before, after, or inside the element under the cursor.\n3. Drop to insert, or press :kbd[Esc] to cancel.\n\nThe new element arrives selected, ready for the [Properties panel](/docs/studio/design/properties) and [Style inspector](/docs/studio/design/style-inspector). Dragging works the same in Edit and Design mode; the other ways to insert — the **+** affordance between elements and the slash menu — are covered in **[The canvas](/docs/studio/interface/canvas)**.\n\n:::doc-note\nInserting adds one element node to the open file — a component card adds an instance tag like `<site-card>`. The format is described in **[Components](/docs/framework/concepts/components)**.\n:::\n\n## Next\n\n- Make your own cards appear here — **[Working with components](/docs/studio/design/components)**\n- Rearrange what you inserted in the **[Layers panel](/docs/studio/design/layers)**\n",
    "section": "Studio",
    "slug": "studio/design/elements",
    "title": "Elements panel",
    "url": "https://jxsuite.com/docs/studio/design/elements/"
  },
  {
    "description": "The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.",
    "markdown": "\n# Properties panel\n\nProperties is the first tab of the right panel — alongside **Events** and **Style** — and it edits everything about the selected element that isn't styling: what kind of element it is, its attributes, and, for component instances, their props. Select an element on the canvas or in [Layers](/docs/studio/design/layers), then click **Properties** at the top of the right panel.\n\n![The Properties panel for a selected link showing the Element and Link sections](../../images/properties-panel.png)\n\n## The Element section\n\nEvery element starts with the same basics:\n\n- **Tag** — the element's type. Change it to turn a `div` into a `section`, a `p` into an `h2`.\n- **ID** and **Class** — the element's identifier and CSS classes.\n- **Text Content** — the element's text, shown when it has no child elements.\n- **Hidden** — a checkbox that removes the element from the rendered page without deleting it.\n\nA small dot appears next to any field that has a value; click the dot to clear it.\n\n## Attribute sections\n\nBelow the basics, Studio shows only the attributes that apply to the selected element's type, grouped into sections — **Identity**, **Link**, **Media**, **Form**, **Table**, and **Accessibility**. An image gets its source, alt text, and loading behavior; a form input gets its name, placeholder, and validation attributes; a plain `div` gets almost none of these. Sections that already have values open automatically and show a dot on their header.\n\nLinks get special treatment. On an `a` element, the **Link** row pairs a kind picker — **Internal Page**, **External URL**, **Anchor**, **Email**, **Phone** — with the matching input; choosing **Internal Page** lists your site's pages so you pick a destination instead of typing a path. The **Open in** attribute becomes a dropdown of the standard targets.\n\nAnything not covered lives in the **Custom** section: click **+ Add attribute** to add any attribute by name, edit its value inline, or remove it with **✕**.\n\n## Component props\n\nWhen the selection is a component instance, a **Component Props** section lists the options the component exposes — see **[Working with components](/docs/studio/design/components)**. Each prop gets a control matched to its type: a checkbox for on/off props, a number field, a dropdown for a fixed set of choices, a media picker for images, a color control for colors. The mode button beside each label switches the prop between a fixed value, a state binding, and a template, and **→ Edit definition** opens the component itself.\n\n## Make any value dynamic\n\nMost fields carry a small mode button beside their label showing the current mode's glyph. Clicking it steps to the field's next mode — **abc** is a plain value; **$ref** points the field at a state value; **${}** builds the value from a template that mixes text and data — and wraps back around, swapping the field's editor in place. Studio remembers each mode's value for the session, so cycling back restores what you had there. The state values on offer come from the document's **State** panel — see **[Script & logic](/docs/studio/logic)**.\n\n## Page and root settings\n\nSelect the page root — the topmost row in Layers — and extra sections appear:\n\n- **Page** (site pages only) — pick the page's **Layout**: the site default, none, or any layout in the project. See **[Pages, layouts, components](/docs/studio/projects/pages-layouts-components)**.\n- **Media** — the file's breakpoints. Covered in **[Breakpoints](/docs/studio/design/breakpoints)**.\n\nSpecial elements swap in their own sections too: a repeater shows a **Repeater** section (see **[Repeaters](/docs/studio/design/repeaters)**), and a switch element lists its cases with an expression field.\n\n:::doc-note\nEvery field writes a key on the selected element in the open file — attributes into `attributes`, component props into `$props`. The file format is described in **[Components](/docs/framework/concepts/components)**.\n:::\n\n## Next\n\n- Style the same selection in the **[Style inspector](/docs/studio/design/style-inspector)**\n- Bind props and attributes to real data in **[Script & logic](/docs/studio/logic)**\n",
    "section": "Studio",
    "slug": "studio/design/properties",
    "title": "Properties panel",
    "url": "https://jxsuite.com/docs/studio/design/properties/"
  },
  {
    "description": "The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.",
    "markdown": "\n# Style inspector\n\nThe Style inspector is the **Style** tab of the right panel — a full set of visual CSS controls for the selected element. Select an element in Design mode and click **Style** at the top of the right panel; edits apply to the live canvas as you make them.\n\n![Jx Studio style inspector with spacing, typography, and color controls for a selected element](../../images/design-inspector.png)\n\n## The toolbar\n\nTwo controls sit above the sections:\n\n- **Breakpoint tabs** — **Base** plus one tab per breakpoint. Base values apply everywhere; a breakpoint tab edits overrides for that screen size only. See **[Breakpoints](/docs/studio/design/breakpoints)**.\n- **Scheme layer** — no extra tab: when the tab bar's Auto/Light/Dark control forces a scheme the project declares, Base-context edits target that scheme's overrides instead, and a \"Dark variant\"-style badge appears beside the tabs. Base values show through as dimmed placeholders. Switch back to **Auto** to edit base styles; breakpoint tabs always stay breakpoint-scoped.\n- **The selector menu** — style the element's `:hover`, `:focus`, and other states, or a selector of your own. See **[Hover states and selectors](/docs/studio/design/states-and-selectors)**.\n\nBelow them, the filter bar narrows the property list: type part of a property's name, or switch on **Active** to show only properties that have values. While filtering, every matching section stays open.\n\n## Sections\n\nProperties are grouped into accordion sections: **Layout**, **Size**, **Spacing**, **Positioning**, **Typography**, **Background**, **Border**, and **Effects**. Sections that already have values open automatically.\n\nA dot is the inspector's \"this is set\" signal, and it doubles as the eraser:\n\n- A dot next to a property means it has a value here — click the dot to clear it.\n- A dot on a section header means something inside is set — click it to clear the whole section.\n\nSome rows appear only when they're relevant — alignment and gap controls show once the element's display makes them meaningful, for example. A row whose value no longer applies is flagged so you can spot leftovers.\n\n## The inputs\n\nEach property gets a control built for it:\n\n- **Number + unit** — type the number, pick the unit (`px`, `rem`, `%`, `vw`, and friends) or a keyword like `auto` from the attached menu.\n- **Color** — a swatch that opens a full picker, with your project's color tokens on offer; a token shows by its name, like **Primary Blue**. See **[Design tokens](/docs/studio/design/tokens)**.\n- **Font family** — a combobox listing your font tokens and a set of ready-made font stacks; the menu previews each option in its own face. Picking a preset saves it as a font token automatically.\n- **Keyword menus** — properties with fixed values get a dropdown; typography menus preview each choice (weights render at their weight, transforms as they transform).\n\nShorthand rows like **Padding** and **Margin** take a combined value, or expand with their chevron into per-side fields; border rows expand into width, style, and color. Studio recombines the sides into the shortest form when it writes the value.\n\nWhen a value comes from somewhere earlier in the cascade — the Base tab, an earlier breakpoint — it shows as a dimmed placeholder rather than a set value, so you always know what you'd be overriding. Style values also accept the **${}** dynamic mode from their mode button, so a property can follow your data — see **[Script & logic](/docs/studio/logic)**.\n\n## Custom and relative styling\n\nTwo sections close the list:\n\n- **Custom** — any CSS property by name. Type the property, press :kbd[Enter], and fill in its value; each row shows the property's browser default as a placeholder.\n- **Relative Styling** — rules for elements _inside_ the selection (the rows inside a table, the links inside a nav). Click a rule to drill into it and edit it with the full inspector; **+ Add** opens a dialog where you type the selector (`th`, `:hover`, `.active`) and click **Add**.\n\n:::doc-note\nEverything here writes plain CSS into the element's `style` object in the open file — the same nested format documented in **[Styling](/docs/framework/concepts/styling)**.\n:::\n\n## Next\n\n- Set element-wide defaults instead of styling one element at a time — **[Stylebook](/docs/studio/design/stylebook)**\n- Name your colors, fonts, and sizes in **[Design tokens](/docs/studio/design/tokens)**\n",
    "section": "Studio",
    "slug": "studio/design/style-inspector",
    "title": "Style inspector",
    "url": "https://jxsuite.com/docs/studio/design/style-inspector/"
  },
  {
    "description": "Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.",
    "markdown": "\n# Breakpoints\n\nA breakpoint is a named screen-size condition — Tablet, Desktop — at which your design is allowed to change. In Design mode every breakpoint gets its own live canvas panel, labeled with its name and width, all showing the same page at once. The responsive rules evaluate for real in each panel, so you never wonder what the phone view looks like — it's right there next to the desktop one.\n\n![Jx Studio design canvas showing one component across four responsive breakpoints with a style inspector](../../images/mode-design.png)\n\n## The active breakpoint\n\nStyling always targets one breakpoint at a time. Two controls stay in sync:\n\n- The **breakpoint tabs** at the top of the [Style inspector](/docs/studio/design/style-inspector) — **Base** plus one tab per breakpoint.\n- The **panel headers** on the canvas — click a panel's header to make its breakpoint active.\n\nPick **Base** to edit the styles that apply everywhere; pick a breakpoint to edit that screen size's overrides.\n\n## How the cascade works\n\nBase is the design; breakpoints are exceptions to it:\n\n1. Values on the **Base** tab apply at every size.\n2. On a breakpoint tab, everything inherited from earlier in the cascade shows as a dimmed placeholder — nothing is duplicated.\n3. Set a value on a breakpoint tab and it becomes an override for that breakpoint only, marked with a set-dot.\n4. Click the dot to remove the override; the inherited value shows through again.\n\nBreakpoints layer in the same order a browser applies their media queries, so what you see per panel is exactly what ships.\n\n## Define your breakpoints\n\nBreakpoints belong to the whole site. To edit them, click the **Settings** gear at the bottom of the activity bar, then find **Breakpoints** in the _General_ section. Each row is a name and a width condition, like `(min-width: 768px)`; add, rename, edit, or remove rows there.\n\nA single file can also carry breakpoints of its own: select the page root in [Layers](/docs/studio/design/layers) and open the **Media** section of the [Properties panel](/docs/studio/design/properties). There you set the file's **Base width** — how wide the Base canvas panel renders — and add breakpoints with a friendly name (Studio derives the stored name, \"Tablet\" becomes `--tablet`). File-level breakpoints add to the site's list for that file. More site-wide options live in **[Project settings](/docs/studio/projects/settings)**.\n\nOnly width conditions get a canvas panel. A breakpoint with a different kind of condition — reduced motion, for example — appears instead as a toggle in the tab bar.\n\nA `prefers-color-scheme` breakpoint is special: it surfaces as an **Auto / Light / Dark** control in the tab bar instead of a generic toggle. Auto follows your OS preference; Light and Dark force that scheme on the canvas — exactly what a visitor's [color-scheme switcher](/docs/framework/concepts/color-schemes) does on the published site. The control appears only when the project declares a scheme breakpoint.\n\n:::doc-note\nBreakpoints are stored as a `$media` map — in `project.json` for the site, or at the top of a file for file-level ones. Per-breakpoint styles nest under the breakpoint's name inside each element's `style`, as described in **[Styling](/docs/framework/concepts/styling)**.\n:::\n\n## Next\n\n- Use the breakpoint tabs while styling in the **[Style inspector](/docs/studio/design/style-inspector)**\n- Element defaults respond to breakpoints too — **[Stylebook](/docs/studio/design/stylebook)**\n",
    "section": "Studio",
    "slug": "studio/design/breakpoints",
    "title": "Breakpoints",
    "url": "https://jxsuite.com/docs/studio/design/breakpoints/"
  },
  {
    "description": "Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.",
    "markdown": "\n# Hover states and selectors\n\nButtons darken on hover, inputs glow on focus, disabled controls fade — those are the same element in a different _state_, and each state can carry its own styles. In Studio you style states with the selector menu in the [Style inspector](/docs/studio/design/style-inspector)'s toolbar, next to the breakpoint tabs.\n\n![The Style inspector's selector menu open, listing built-in states and existing selectors](../../images/selector-menu.png)\n\n## Style a state\n\n1. Select the element and open the **Style** tab.\n2. Open the selector menu — it reads **(base)** when you're styling the element's normal look.\n3. Pick a state, say **:hover**.\n4. Edit styles as usual. Everything you set now applies only in that state.\n\nThe full inspector works here — every section, the set-dots, even the breakpoint tabs, so a hover effect can differ between desktop and phone. Values the state inherits from the element's base styles show as dimmed placeholders. Switch the menu back to **(base)** to return to the normal styles.\n\n## The built-in states\n\nThe menu offers the common ones: **:hover**, **:focus**, **:active**, **:focus-within**, **:focus-visible**, **:disabled**, **:first-child**, **:last-child**, and the **::before**, **::after**, and **::placeholder** extras. A **●** after an entry means the element already has styles there — your map of where to look when something styles unexpectedly.\n\nThey mean what they mean on the web: `:hover` while the pointer is over the element, `:focus` while it holds keyboard focus, `:first-child` / `:last-child` when it's the first or last among its siblings, `::placeholder` for an input's hint text.\n\n## Add your own selector\n\nChoose **+ Add custom…** at the bottom of the menu, type a selector, and press :kbd[Enter]:\n\n- `:nth-child(2)` — any state or position selector beyond the built-ins.\n- `.featured` — only when the element has that class.\n- `&.active` — only when the element itself carries the `active` class.\n- `[disabled]` — only when the element has that attribute.\n\nA custom selector must start with `:`, `.`, `&`, or `[`. Once created it joins the menu for that element, marked with **●** while it has styles. Rules for elements _inside_ the selection are the inspector's **Relative Styling** section instead — clicking a rule there drills into it through this same selector context.\n\n## Remove state styles\n\nSwitch the menu to the state and clear its values with their dots — property by property, or a whole section from its header dot. When a state has no values left, it drops out of the menu's marked entries.\n\n:::doc-note\nEach state is saved as a nested rule inside the element's `style` object — real CSS pseudo-classes that behave natively on the published page. The format is described in **[Styling](/docs/framework/concepts/styling)**.\n:::\n\n## Next\n\n- Combine states with screen sizes in **[Breakpoints](/docs/studio/design/breakpoints)**\n- Give every link and button a default hover in the **[Stylebook](/docs/studio/design/stylebook)**\n",
    "section": "Studio",
    "slug": "studio/design/states-and-selectors",
    "title": "Hover states and selectors",
    "url": "https://jxsuite.com/docs/studio/design/states-and-selectors/"
  },
  {
    "description": "Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.",
    "markdown": "\n# Design tokens\n\nA design token is a named value — **Primary Blue** instead of `#3b82f6`, **Body Serif** instead of a font list. Define it once for the site and reference it everywhere; change it once and everywhere updates. Tokens are what keep a growing site on one palette instead of thirty slightly different blues.\n\n![The CSS Variables settings section with grouped color, font, and size variables](../../images/css-variables.png)\n\n## Define tokens\n\n1. Click the **Settings** gear at the bottom of the activity bar.\n2. Open the **CSS Variables** section.\n\nTokens are grouped by what they name:\n\n- **Colors** — each row has a color swatch (click it for a native picker), the token's name, and its value. When the project declares a [color scheme](/docs/framework/concepts/color-schemes), each color token also carries a per-scheme row (e.g. **Dark**) — leave it empty to inherit the base value, or set it to give the token a scheme-specific value. Projects without a scheme get an **Enable dark scheme** button that declares one. Edits appear on the canvas immediately.\n- **Fonts** — each font renders a preview sentence in its own face below the row.\n- **Sizes & Spacing** — widths, gaps, and radii. When a size token has a different value at a breakpoint, the override appears beneath it for editing.\n- **Other** — anything that doesn't fit the groups above.\n\nTo add a token, type a friendly name and a value in the group's empty row and click **Add**. Studio derives the stored variable name from the group and your name — \"Primary Blue\" in Colors becomes `--color-primary-blue`. The trash button removes a token; anything still referencing it falls back to nothing, so remove with care.\n\n## Use tokens while styling\n\nTokens surface right inside the [Style inspector](/docs/studio/design/style-inspector)'s controls:\n\n- The **color picker** lists your color tokens; pick one and the field shows the token's name with its swatch instead of a raw code.\n- The **font family** box lists your font tokens first, each previewed in its own face. Picking one of the ready-made presets creates a matching font token automatically, so even your first font choice becomes reusable.\n- Any field accepts a token typed directly as `var(--color-primary-blue)` — useful for the occasional property without a picker.\n\nA field showing a token name is _following_ the token: edit the token in Settings and every element using it updates.\n\nStudio also nudges toward tokens in the other direction — when its AI assistant writes styles, hard-coded values that duplicate an existing token are flagged with the token to use instead.\n\n:::doc-note\nTokens are CSS custom properties in your project's site-wide `style` (in `project.json`), inherited by every page and component. The format is described in **[Styling](/docs/framework/concepts/styling)**.\n:::\n\n## Next\n\n- Pair tokens with element defaults in the **[Stylebook](/docs/studio/design/stylebook)**\n- Size tokens that shift per screen — see **[Breakpoints](/docs/studio/design/breakpoints)**\n",
    "section": "Studio",
    "slug": "studio/design/tokens",
    "title": "Design tokens",
    "url": "https://jxsuite.com/docs/studio/design/tokens/"
  },
  {
    "description": "Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.",
    "markdown": "\n# Stylebook\n\nStylebook is a canvas mode that catalogs your elements — every heading, paragraph, button, list, table, and form control, plus your components, rendered as specimen cards with their current default styles. Instead of styling one `h2` on one page, you style what _every_ `h2` looks like, in one place. Switch to it with **Stylebook** in the mode switcher on the right side of the toolbar.\n\n![Jx Studio Stylebook mode showing element defaults across breakpoints](../../images/stylebook.png)\n\nYour project file opens in Stylebook automatically — that's the natural home for site-wide defaults. Opening Stylebook on a page or component styles that file's element defaults instead.\n\n## Read the catalog\n\nLike Design mode, Stylebook shows one panel per breakpoint, all rendering the same catalog — so an element default that changes at Tablet is visibly different in the Tablet panel. The catalog renders through the real runtime, exactly as the defaults will render on your pages.\n\nTwo controls sit above the canvas: a **Filter** box to narrow the catalog by name, and a **Customized** toggle to show only the elements you've already styled. The **Layers** activity mirrors the catalog as a tree — elements with their nested parts (a table with its rows and cells), then your components — and marks customized entries with a dot.\n\n## Style an element type\n\n1. Click a specimen card on the canvas, or its row in **Layers**. The right panel switches to the **Style** tab on its own, headed **Styling: &lt;h1&gt;** (or whatever you picked).\n2. Edit styles with the full [Style inspector](/docs/studio/design/style-inspector) — sections, set-dots, and all.\n3. Watch every panel update: you're styling the element type, so every specimen of it changes at once — and so does every matching element in your project.\n\nThe inspector's toolbar works the same as in Design mode: **breakpoint tabs** scope a default to a screen size (see **[Breakpoints](/docs/studio/design/breakpoints)**), and the **selector menu** adds states — give every link a default `:hover` in one edit (see **[Hover states and selectors](/docs/studio/design/states-and-selectors)**). Nested parts style as compound selections, like the header cells inside tables.\n\n## Stylebook, element styles, or component styles?\n\nThree layers, from broadest to narrowest:\n\n- **Stylebook defaults** — \"every `h2` looks like this.\" Your typography, link, and form baseline. Change it here and the whole site follows.\n- **Component styles** — \"this card component looks like this, wherever it's used.\" Style the elements _inside_ a component definition in Design mode; see **[Working with components](/docs/studio/design/components)**.\n- **Per-element styles** — \"this one element is special.\" The [Style inspector](/docs/studio/design/style-inspector) on a single selection, layered on top of both.\n\nThe narrower layer wins where they overlap, so keep the common look in the Stylebook and reserve per-element styling for true exceptions. [Design tokens](/docs/studio/design/tokens) slot in underneath all three — defaults built from tokens keep even your baseline swappable.\n\n:::doc-note\nElement defaults are saved as tag-named rules in the open file's top-level `style` — in `project.json` they become site-wide and merge into every page and component. The format is described in **[Styling](/docs/framework/concepts/styling)**.\n:::\n\n## Next\n\n- Name the values your defaults are built from in **[Design tokens](/docs/studio/design/tokens)**\n- How Stylebook fits among the modes — **[Modes and preview](/docs/studio/interface/modes)**\n",
    "section": "Studio",
    "slug": "studio/design/stylebook",
    "title": "Stylebook",
    "url": "https://jxsuite.com/docs/studio/design/stylebook/"
  },
  {
    "description": "Turn any selection into a reusable component in Jx Studio, wire its props in the Properties panel, add slots, and preview it with test values.",
    "markdown": "\n# Working with components\n\nA component is a piece of design you build once and use everywhere — a card, a header, a pricing row. Each use is an _instance_; edit the component and every instance follows. This page covers the component workflows on the Design canvas; the bigger picture of what belongs in a component lives in **[Pages, layouts, components](/docs/studio/projects/pages-layouts-components)**.\n\n## Convert a selection into a component\n\nThe natural way to make a component is to design it in place first, then promote it:\n\n1. Build the element on the canvas — structure, styles, content.\n2. Right-click it (on the canvas or in [Layers](/docs/studio/design/layers)) and choose **Convert to Component**.\n3. Give it a tag name — lowercase, with a hyphen, like `pricing-card` — and click **Convert**. Studio validates the name as you type and won't let you collide with an existing component.\n\n![The Convert to Component dialog with a tag name filled in](../../images/convert-to-component.png)\n\nStudio saves the component as its own file in the project's `components/` folder, swaps your selection for an instance of it, and adds it to the [Elements panel](/docs/studio/design/elements) — drop more instances anywhere from there. If the component's slots need attention, Studio says so in the status bar.\n\nTo open a component from an instance, right-click the instance and choose **Edit Component**, or click **→ Edit definition** under its props in the Properties panel.\n\n## Props: the component's options\n\nProps are the knobs an instance can turn — the card's title, its image, whether it's featured. A component's props come from its state: every plain value you declare in the component's **State** panel becomes a prop, with that value as its default. See **[Script & logic](/docs/studio/logic)** for declaring state.\n\nOn an instance, the [Properties panel](/docs/studio/design/properties) shows a **Component Props** section with a fitting control per prop — checkbox, number field, dropdown, media or color picker. Each prop can also be _bound_: click the mode button beside its label to switch the prop to a state binding (or a template) and point it at one of the page's state values, so the instance follows live data instead of a fixed setting.\n\n## Slots: openings for content\n\nBy default an instance renders exactly what the component defines. A **slot** is a deliberate opening — add a `slot` element inside the component definition, and whatever an instance holds as children flows into that opening. Give slots names to offer several openings (a card with an icon slot and a body slot). In [Layers](/docs/studio/design/layers), slots show a **▣** badge; hover it for the slot's name. Layouts distribute page content the same way.\n\n## Preview with test props\n\nA component file open on its own renders with its defaults — but defaults are often empty. The tab bar shows one small field per prop; type a value to see the component render with it, live on the canvas at every breakpoint. Numbers, `true`/`false`, and lists in JSON form are understood as such; anything else counts as text.\n\nTest values are a preview lens only — they're never saved into the component, and clearing a field returns the prop to its default. They pair with the **Preview** toggle described in **[Modes and preview](/docs/studio/interface/modes)**.\n\n:::doc-note\nA component is a plain file — `components/pricing-card.json` — and converting also records a reference to it in the page's `$elements` list. The format is described in **[Components](/docs/framework/concepts/components)**.\n:::\n\n## Next\n\n- Repeat a component per item of a list — **[Repeaters](/docs/studio/design/repeaters)**\n- Insert instances from the **[Elements panel](/docs/studio/design/elements)**\n",
    "section": "Studio",
    "slug": "studio/design/components",
    "title": "Working with components",
    "url": "https://jxsuite.com/docs/studio/design/components/"
  },
  {
    "description": "Repeaters in Jx Studio: turn one element into a repeating list, bind it to your data, and use each item's fields inside the template.",
    "markdown": "\n# Repeaters\n\nA repeater renders one element once per item of a list — design a single card, bind it to your products, and get a card per product. The element you design becomes the repeater's _template_; the list it follows can be state, a content collection, or a data source.\n\n## Turn an element into a repeater\n\n1. Design one instance of the repeating thing — one card, one row, one gallery tile.\n2. Right-click it (on the canvas or in [Layers](/docs/studio/design/layers)) and choose **Repeat…**.\n3. In the dialog, pick the **Items source** — a list from the document's state, or **Create new…** to declare a fresh one by name.\n4. Optionally pick a **Filter** or **Sort** function, if the document defines any.\n5. Click **Create Repeater**.\n\n![The Repeat dialog with an items source selected](../../images/repeat-dialog.png)\n\nYour element is now the template of a repeater, marked **↻** in Layers. The repeated items render directly where the element stood — no wrapper is added around them.\n\n## Bind the list\n\nThe items source is what makes a repeater useful. It can be:\n\n- **A plain list in state** — start with `Create new…` and fill in items later in the **State** panel.\n- **A content collection** — every blog post, product, or team member in your project's content. See **[Content types](/docs/studio/projects/content-types)**.\n- **A data source** — anything that produces a list, like a `Request` fetching from an API.\n\nDeclaring and feeding these lives in **[Script & logic](/docs/studio/logic)**. With the repeater selected, the [Properties panel](/docs/studio/design/properties) shows a **Repeater** section where you can rebind **Items** and add or change **Filter** and **Sort** after the fact.\n\n## Edit the template\n\nSelect the repeater and click **Edit template →** in the Repeater section — or just click into the repeated content on the canvas. On the design canvas the template renders once, as the single element you design; switch on the **Preview** toggle to see the list expanded with its real items (see **[Modes and preview](/docs/studio/interface/modes)**).\n\nInside the template, each item's data is in scope:\n\n- While editing text, the **Insert data** button on the block action bar offers `item` — the current item — and `index`, its position, alongside the item's own fields: `item.data.title` for a content collection's fields, or `item.name`-style fields for lists of records.\n- In the Properties panel, binding menus offer the current item and its position wherever a `$ref` binding is available.\n\nBind the template's text and attributes to item fields and every rendered copy fills itself in from its own item.\n\n:::doc-warning\nThe template is one element styled once — changes to it apply to every repeated item. To make one item special, style by position with a selector like `:first-child` (see **[Hover states and selectors](/docs/studio/design/states-and-selectors)**) or use data-driven styling.\n:::\n\n:::doc-note\nA repeater is stored in the file as an `Array` node with `items` (the binding), `map` (the template), and optional `filter` and `sort`. The reactive model behind it is described in **[Reactivity](/docs/framework/concepts/reactivity)**.\n:::\n\n## Next\n\n- Feed repeaters from collections — **[Content types](/docs/studio/projects/content-types)**\n- Make the template a reusable component — **[Working with components](/docs/studio/design/components)**\n",
    "section": "Studio",
    "slug": "studio/design/repeaters",
    "title": "Repeaters",
    "url": "https://jxsuite.com/docs/studio/design/repeaters/"
  },
  {
    "description": "Make pages interactive in Jx Studio — where the State, Data, Events, and Code surfaces fit together, with a 60-second counter to see it working.",
    "markdown": "\n# Logic\n\nThere is no single \"logic mode\" in Studio. Interactivity comes from a few focused surfaces you move between, and each one answers a different question:\n\n- **[State](/docs/studio/logic/state)** — an activity in the left panel. _What does this page or component know?_ Declare values, computed entries, data sources, and functions here.\n- **[Data](/docs/studio/logic/data-explorer)** — the activity right below it. _What are those values right now?_ It shows the live, resolved data as the page runs.\n- **[Events](/docs/studio/logic/events)** — a tab in the right panel. _What happens when a visitor clicks or types?_ Bind behavior to the selected element here.\n- **[Formulas](/docs/studio/logic/formulas)** — not a place but an affordance: almost any value field carries an **fx** menu that turns a fixed value into a computed one, with live previews as you build it. A **[full-screen workspace](/docs/studio/logic/formula-workspace)** opens when a formula deserves the whole canvas.\n- **[Code](/docs/studio/logic/code)** — the escape hatch. A real code editor for function bodies, and a **Code** canvas mode that shows any file as raw source.\n\n![Jx Studio State panel listing a component's state and functions](../images/state-panel.png)\n\nTogether they cover the whole range: most interactions never need code, and the ones that do get a proper editor rather than a cramped text box.\n\n## A counter in 60 seconds\n\nHere is the shape of the workflow, end to end:\n\n1. Open a component and click **State** in the activity bar. Choose _+ Add… > State Signal_, name it `$count`, set its **Type** to `integer` and its **Default** to `0`. The component now knows a number.\n2. Add a button to the canvas and set its text to show `$count` — any text field's **fx** menu can point at a state value.\n3. With the button selected, open the **Events** tab and click **Add Event**. Set the event to `onclick`, choose the **$expression** mode, and build the one-step formula `$count += 1`.\n4. Toggle **Preview** on and click the button. The number climbs — and if you open the **Data** activity, you can watch `$count` change in real time.\n\nNo files were written by hand: Studio stored the value, the binding, and the handler inside the component's own JSON file.\n\n## When each surface applies\n\n- Reach for **State** first — every other surface refers back to what you declare there.\n- Use **Events** whenever behavior belongs to one element (\"this button submits\", \"this field filters the list\").\n- Use **formulas** for values that are calculated rather than typed, and **[statements](/docs/studio/logic/statements)** when a handler needs several steps in order.\n- Open **Data** whenever something looks wrong — it shows what the page actually sees, not what you hoped it sees.\n- Drop into **Code** when a function outgrows the structured editors, or when you want to read exactly what Studio wrote.\n\n## Next\n\n- Declare your first values in the **[State panel](/docs/studio/logic/state)**\n- Wire data in from files, APIs, and the browser with **[Data sources](/docs/studio/logic/data-sources)**\n- The reactive model underneath it all is documented in **[Reactivity](/docs/framework/concepts/reactivity)**\n",
    "section": "Studio",
    "slug": "studio/logic",
    "title": "Logic",
    "url": "https://jxsuite.com/docs/studio/logic/"
  },
  {
    "description": "Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.",
    "markdown": "\n# State panel\n\nState is where you declare everything the open page or component knows: the values it holds, the values it derives, the data it fetches, and the functions it can run. Open it by clicking **State** (the brackets icon) in the activity bar.\n\n![Jx Studio State panel listing a component's state and functions](../../images/state-panel.png)\n\nEvery entry belongs to the open file — each page or component carries its own state, saved inside its own JSON file.\n\n## Read the list\n\nEntries are grouped into collapsible sections with counts — **State**, **Computed**, **Data**, **Expressions**, **Functions** — and only sections with entries appear. Each row shows:\n\n- A colored badge for the kind of entry (**S** state, **C** computed, **E** expression, **F** function; data sources show their kind's initial).\n- The entry's name.\n- A short hint — a Request's method and URL, a storage entry's key, the first line of a function.\n\nClick a row to expand its editor in place; click again to collapse it. A file with no entries yet shows \"No state defined\".\n\n## Add an entry\n\n1. Click the **+ Add…** picker at the bottom of the panel.\n2. Choose what to add:\n   - **State Signal** — a plain value the component holds (text, a number, a flag, a list).\n   - **Computed** — a value derived from other entries, recalculated automatically.\n   - **Fetch (Request)**, **LocalStorage**, **SessionStorage**, **IndexedDB**, **Cookie**, **Set**, **Map**, **FormData** — the built-in data sources, covered in **[Data sources](/docs/studio/logic/data-sources)**.\n   - **External Module…** — a data source provided by a JavaScript module you point at.\n   - Any sources your project imports or its extensions provide (for example **ContentCollection**) appear next.\n   - **Expression** — a named formula, covered in **[Formulas and expressions](/docs/studio/logic/formulas)**.\n   - **Function** — a reusable piece of behavior, covered in **[Statements](/docs/studio/logic/statements)** and **[Code editing](/docs/studio/logic/code)**.\n3. The new entry appears with a placeholder name and its editor open — rename it first.\n\n## Rename and delete\n\n- To rename, edit the **Name** field at the top of an entry's editor. The change commits when you press :kbd[Enter] or leave the field, and the new name must not already be taken. Everything that referred to the old name keeps its old reference, so rename before you wire an entry into formulas and events.\n- To delete, click the trash icon on the entry's row.\n\n## Plain values\n\nA **State Signal** is the workhorse — the counter, the search text, the \"menu open\" flag. Its editor offers:\n\n- **Type** — `string`, `integer`, `number`, `boolean`, `array`, or `object`. Defaults you type are converted to match.\n- **Format** — for strings only: `image`, `date`, or `color`. An image-formatted value gets a media picker for its default.\n- **Default** — the starting value. Array and object defaults are typed as JSON.\n- **Description** — a note to your future self, also shown when the component is used elsewhere.\n\n## Computed values\n\nA **Computed** entry derives its value from other entries — a total from a price and a quantity, a filtered list from a search box. Type the calculation in the **Expression** field, referring to other entries by name (`$price * $qty`). Studio detects which entries the expression depends on and lists them underneath; the value recalculates whenever any of them changes.\n\n## Functions\n\nA **Function** entry is behavior you can bind to events or call from formulas. Its editor offers:\n\n- **Description** — what the function does.\n- **Parameters** — type a name and press :kbd[Enter] to add one as a chip; **Advanced** switches to full rows with a type, description, and optional flag per parameter.\n- **Body** — with a **Statements**/**Code** toggle: build the body as visual steps, or write JavaScript. See **[Statements](/docs/studio/logic/statements)** and **[Code editing](/docs/studio/logic/code)**.\n\nA function stored in a separate file shows **Source** and **Export** fields instead of a body — see the sidecar section of **[Code editing](/docs/studio/logic/code)**.\n\n## Components: props, attributes, and events\n\nWhen the open file is a component, its plain state entries double as the component's options — the values a page can set when it uses the component. Component files add a few fields:\n\n- On plain values: **Attribute** (the HTML attribute that sets this value), **Reflects**, and **Deprecated**.\n- On functions: an **Emits** list declaring the events the component can send, each with a name, type, and description. Declared events show up in the **[Events panel](/docs/studio/logic/events)** wherever the component is used.\n\nName an entry with a leading `#` (like `#cache`) to keep it private: private entries never become component options and are left out of the component's published description.\n\n:::doc-note\nEverything in this panel is written to the `state` object of the open file's JSON. The shapes Studio writes — plain values, computed entries, `$prototype` sources, functions — are documented in **[Components](/docs/framework/concepts/components)** and **[Reactivity](/docs/framework/concepts/reactivity)**.\n:::\n\n## Next\n\n- Watch these entries carry real values in the **[Data explorer](/docs/studio/logic/data-explorer)**\n- Feed them from files, APIs, and the browser with **[Data sources](/docs/studio/logic/data-sources)**\n- Bind them to clicks and keystrokes in the **[Events panel](/docs/studio/logic/events)**\n",
    "section": "Studio",
    "slug": "studio/logic/state",
    "title": "State panel",
    "url": "https://jxsuite.com/docs/studio/logic/state/"
  },
  {
    "description": "Build dynamic values without code: the field-mode button on bindable fields, the formula editor, chip pipeline, live value badges, and the formula palette.",
    "markdown": "\n# Formulas and expressions\n\nA formula is a value Studio calculates instead of one you type — a price times a quantity, a name in uppercase, a label that switches on a condition. Formulas aren't confined to one panel: nearly every value field in Studio can become one, and while you build it, Studio shows the live result computed from your page's real data.\n\n## The field-mode button: any value can be dynamic\n\nBindable value rows in the **Properties** and **Style** tabs carry a small mode button beside their label. It shows the current mode's glyph — gray while the value is static, accent-colored once it's dynamic — and each click steps to the next mode, wrapping back around at the end:\n\n- **abc** — a fixed value you type. The default.\n- **$ref** — follow a state value: pick an entry from the **[State panel](/docs/studio/logic/state)** and the field always shows its current value.\n- **${}** — a template mixing fixed text and values, like `Hello ${state.$name}`.\n- **fx** — a full formula, edited right in the row.\n\nEach position only offers the modes it supports (the button's tooltip names the mode a click will switch to), and Studio remembers what you had in each mode for the rest of your session — cycle away and back and your value is restored, not reset. Prefer the earliest mode that does the job — a `$ref` is easier to read (and to revisit) than a formula that only fetches one value.\n\n![A Properties value row with the mode button showing the ${} template glyph in accent color](../../images/field-mode-button.png)\n\nFormulas also appear as their own state entries (_+ Add… > Expression_ in the State panel) and as event handlers (the **$expression** mode in the **[Events panel](/docs/studio/logic/events)**).\n\n## The formula editor\n\nA formula is a tree of small operations, and the editor edits one operation at a time:\n\n- **Operator** — what this step does. The picker groups the whole vocabulary: assignment, arithmetic, comparison, logical, conditional, array methods, pure string/array/number methods, aggregates (`map`, `filter`, `reduce`), and `call` for invoking a named formula. The complete list, with what each operator means, is the **[operator reference](/docs/framework/reference/operators)**.\n- **Target** and **Value** — the operands. Each one is a **lit** (a typed-in string, number, boolean, or null), a **$ref** (a state value), or an **expr** — a nested formula of its own, drawn indented beneath its parent.\n\nOperators bring their own rows: the conditional shows **If** / **Then** / **Else**; `switch` shows an **On** row plus one row per case and a default, with **+ Add case** to grow it; `call` shows a **Callee** and one argument row per parameter.\n\n## Chips and live value badges\n\nAbove the editor, the whole formula reads left to right as a strip of **chips** — the starting value first, then each operation applied to it, with nested branches shown as parenthesized groups. It's the \"pipeline\" view of the same tree: `$name › trim › toUpperCase`.\n\nNext to chips and operands, green monospace **badges** show live values — each one is the actual result of that piece of the formula, evaluated against the running page's real data. The root's badge is the formula's final result, and if the formula can't evaluate, the error appears in red instead. Watching the badges while you edit is the fastest way to see where a formula goes wrong.\n\nIn the compact inline editor the chips are a summary; clicking them to navigate is what the **[formula workspace](/docs/studio/logic/formula-workspace)** is for.\n\n## The formula palette\n\nYou don't have to assemble everything operator by operator. The brackets button beside the operator picker opens the **formula palette** — a search box over the whole catalog:\n\n![The formula palette open with search results grouped into formulas, operators, and globals](../../images/formula-palette.png)\n\n- Type to filter by name, group, or description; :kbd[↓] and :kbd[↑] move through results and :kbd[Enter] inserts the highlighted entry.\n- **Formulas** lists the named formulas already defined in this file.\n- **Formulas library** lists ready-made formulas that ship with Jx — `average`, `capitalize`, and friends. The full generated list is the **[formula catalog](/docs/framework/reference/formulas)**.\n- The remaining groups are the operators themselves, plus the blessed standard-library functions (`Math.max`, `JSON.stringify`, …) callable from formulas.\n\nPicking an entry replaces the current step with that operation, ready for you to fill in its operands.\n\n:::doc-note\nLibrary formulas are **copied in**, not linked: picking one writes its full definition into your file's state as a named formula, and the inserted step just calls it. Your project stays self-contained — there is no runtime dependency on the catalog, and you can open the copy in the State panel to inspect or edit it.\n:::\n\n## Named formulas\n\nAn **Expression** entry in the State panel is a formula with a name. Once it declares parameters, it becomes callable from any other formula via the `call` operator — the palette lists it under **Formulas**, and its argument rows are labeled with the parameter names. Library formulas arrive with their parameters declared; to add parameters to a formula of your own, edit its entry in **[Code mode](/docs/studio/logic/code)**. That's how you build a vocabulary: define `discountedPrice` once, call it everywhere.\n\n## Next\n\n- Give a big formula the whole canvas in the **[Formula workspace](/docs/studio/logic/formula-workspace)**\n- Every operator, defined precisely: **[Operator reference](/docs/framework/reference/operators)**\n- Every packaged formula: **[Formula catalog](/docs/framework/reference/formulas)**\n",
    "section": "Studio",
    "slug": "studio/logic/formulas",
    "title": "Formulas and expressions",
    "url": "https://jxsuite.com/docs/studio/logic/formulas/"
  },
  {
    "description": "Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.",
    "markdown": "\n# Formula workspace\n\nThe formula workspace gives one formula the entire canvas. The compact editors in the panels are fine for a two-step formula; when one grows branches, the workspace lays the whole tree out with live values at every step and your page's data alongside.\n\n![The full-screen formula workspace with the chip strip, selected-step editor, and data rail](../../images/formula-workspace.png)\n\n## Open it\n\nClick the full-screen icon beside any formula:\n\n- On an **Expression** entry in the **[State panel](/docs/studio/logic/state)**.\n- On an **$expression** event binding in the **[Events panel](/docs/studio/logic/events)**.\n\nThe canvas is replaced by the workspace, and the tab bar shows a breadcrumb — the file's name, then _fx_ and the formula's name. Click **Back** in the tab bar (or **Close** in the workspace header) to return to the normal canvas; your edits are already in the document.\n\n## The layout\n\n- **Header** — the formula's name and kind (a state expression or an event expression), a **Catalog** button that opens the **[formula palette](/docs/studio/logic/formulas)**, and **Close**.\n- **Chip strip** — the whole formula as a left-to-right pipeline of chips, each with its live value badge. This is the map.\n- **Editor pane** — the currently selected step, edited with the same operator/operand form as everywhere else. A \"Selected\" line above it names the step you're on.\n- **Data rail** — the page's live data on the right: every value in scope with its type and an expandable tree of its contents, exactly as the **[Data explorer](/docs/studio/logic/data-explorer)** would show it. You build the formula while looking at the data it consumes.\n- **Footer** — the bottom line, literally: `= result` in green, computed live. Formulas that change a value rather than produce one are marked \"(mutates target)\", and an evaluation problem shows its error message here in red.\n\n## Navigate by chip\n\nClick any chip to select that step — the editor pane retargets to it, so you edit one piece of a large formula without scrolling through the whole nested form. Click the first chip to jump back to the start of the pipeline; edits always land in the right place in the tree, and each edit is a single undo step.\n\n## Live-context previews\n\nThe badges, the data rail, and the footer result are all computed against the **running page** — real fetched data, real list items, real state. Two details make this more useful than a static preview:\n\n- An event formula that lives inside a repeated list is previewed with the first item's data, so `$map/item` references show real values.\n- If the canvas hasn't produced data yet (for example, the page hasn't rendered since you opened the file), the footer says so — \"Preview unavailable\" — rather than showing stale numbers. Values reappear as soon as the canvas renders.\n\n## Insert from the catalog\n\nThe **Catalog** button opens the same searchable palette as the inline editors, but here a pick replaces the **selected step** — so you can navigate to a branch by chip, then drop `average` or a `switch` scaffold exactly there. Library formulas are copied into your file's state on first use, as described in **[Formulas and expressions](/docs/studio/logic/formulas)**.\n\n## Next\n\n- The editing vocabulary itself: **[Formulas and expressions](/docs/studio/logic/formulas)**\n- Multi-step behavior belongs in **[Statements](/docs/studio/logic/statements)**, not one giant formula\n- Check what the page's data actually looks like in the **[Data explorer](/docs/studio/logic/data-explorer)**\n",
    "section": "Studio",
    "slug": "studio/logic/formula-workspace",
    "title": "Formula workspace",
    "url": "https://jxsuite.com/docs/studio/logic/formula-workspace/"
  },
  {
    "description": "Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.",
    "markdown": "\n# Statements\n\nStatements are function bodies built as a vertical list of visual steps instead of written as code. Each step is a card — \"set this value\", \"call that function\", \"if this, then…\" — and the cards run top to bottom. For most handlers, this is all the programming a page needs.\n\n![A function body as statement cards with an If / Else branch expanded](../../images/statement-editor.png)\n\n## Where Studio offers it\n\nAnywhere a function body appears, a **Statements** / **Code** toggle picks the representation:\n\n- A **Function** entry's **Body** in the **[State panel](/docs/studio/logic/state)**.\n- An **inline** event handler in the **[Events panel](/docs/studio/logic/events)**.\n\n**Statements** is the structured editor described here; **Code** is a JavaScript text body — see **[Code editing](/docs/studio/logic/code)**.\n\n:::doc-warning\nThe toggle switches representations; it does not translate between them. Picking the other mode replaces the current body with an empty one — undo (:kbd[⌘Z] / :kbd[Ctrl+Z]) brings the old body back.\n:::\n\n## Add steps\n\nEvery list of steps ends in a **+ Add statement** picker with five kinds:\n\n- **Set state** — store a value in a state entry. This is the everyday step: the card is a one-step formula whose operator is an assignment (`=`, or `+=` and friends for read-modify-write), whose target is the entry, and whose value can be anything a **[formula](/docs/studio/logic/formulas)** can produce.\n- **Call function** — run another function from your state, with rows for the arguments to pass it.\n- **If / Else** — run different steps depending on a condition.\n- **Switch** — pick one of several branches by matching a value.\n- **Dispatch event** — send an event out of a component, so the page using it can react.\n\nEach card has a header naming its kind, a delete button, and a drag handle (⠿) — drag cards to reorder them within their list.\n\n## Branch with If / Else\n\nAn **If / Else** card holds:\n\n1. An **If** row — the condition, written as an operand: a state value, or a nested comparison formula.\n2. A **Then** lane — an indented list of steps with its own **+ Add statement** picker, run when the condition holds.\n3. Optionally an **Else** lane — click **+ Add else** to add it, or the remove button on the lane to drop it.\n\nLanes nest: a step inside **Then** can itself be an **If / Else** or a **Switch**.\n\n## Branch with Switch\n\nA **Switch** card matches one value against several cases:\n\n1. **Switch on** — the value to examine.\n2. One lane per case, each labeled with the value it matches (edit the label field to change it). **+ Add case** appends another.\n3. A **Default** lane for when nothing matches.\n\n## Dispatch an event\n\nA **Dispatch event** card sends a custom event from a component — the counterpart of the **Emits** list on a function in the State panel:\n\n- **Event** — the event's name. In a component whose functions declare emitted events, this is a combo box offering the declared names.\n- **Detail** — the data to send along, as an operand (a state value, a literal, or a formula).\n- **Options** — **Bubbles** and **Composed** checkboxes controlling how far the event travels.\n\nPages using the component can then bind that event in their own **[Events panel](/docs/studio/logic/events)** and read the payload as `event#/detail`.\n\n:::doc-note\nStatement bodies are saved as a JSON list in the function's `body`, one object per card — the same file the rest of the component lives in, diffable like everything else Studio writes.\n:::\n\n## Next\n\n- Bind a statement-bodied function to a click in the **[Events panel](/docs/studio/logic/events)**\n- When a body outgrows steps, switch to **[Code editing](/docs/studio/logic/code)**\n- The formula vocabulary inside each step: **[Formulas and expressions](/docs/studio/logic/formulas)**\n",
    "section": "Studio",
    "slug": "studio/logic/statements",
    "title": "Statements",
    "url": "https://jxsuite.com/docs/studio/logic/statements/"
  },
  {
    "description": "Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.",
    "markdown": "\n# Events panel\n\nEvents is the right-panel tab where an element gets its behavior: what happens when it's clicked, typed into, submitted, or hovered. Select an element on the canvas, then click **Events** in the right panel — with nothing selected, the tab just asks you to select an element first.\n\n![The Events tab with an onclick binding in expression mode](../../images/events-panel.png)\n\n## Add a binding\n\nClick **Add Event**. Studio creates a binding on the first free event name and points it at your file's first function — or, if the file has no functions yet, starts an inline handler instead. Each binding row then has three controls:\n\n- The **event name** — pick from `onclick`, `oninput`, `onchange`, `onsubmit`, `onkeydown`, `onkeyup`, `onfocus`, `onblur`, `onmouseenter`, and `onmouseleave`.\n- The **mode** — one of the three ways to respond, below.\n- A trash button that removes the binding.\n\n## Three ways to respond\n\n**$ref — call a named function.** A picker lists the functions declared in the **[State panel](/docs/studio/logic/state)**; pick one and the event runs it. This is the tidiest option when the same behavior is used in more than one place.\n\n**$expression — an inline formula.** The event runs a single formula, edited right in the panel with live value badges — ideal for one-step reactions like `$count += 1`or`$menuOpen = true`. The full-screen icon opens it in the **[formula workspace](/docs/studio/logic/formula-workspace)**.\n\n**inline — a handler written on the element itself.** A **Statements** / **Code** toggle picks how you write it: as visual **[statement](/docs/studio/logic/statements)** cards, or as JavaScript in a small text field with an **Open in editor** button for the real **[code editor](/docs/studio/logic/code)**.\n\nSwitching modes replaces the binding with a fresh start in the new mode; undo restores the previous one.\n\n## Read values from the event\n\nHandlers can read from the event that triggered them. In expression and statement editors on this panel, the value pickers offer `event#/` entries alongside your state:\n\n- `event#/target/value` — what the visitor has typed into the field. The classic `oninput` pattern is one step: set `$searchText` to `event#/target/value`.\n- `event#/detail` — the data a component sent along when it dispatched a custom event.\n\nAn `event#/` reference can point at any property of the event — `event#/key` for the pressed key, for example — by writing the reference in **[Code mode](/docs/studio/logic/code)**; the pickers offer the two common ones.\n\n## Component events\n\nWhen the open file is a component, a **Declared Events** section lists every event its functions declare they emit — the name, the function it comes from, and its payload type. That declaration is the component's contract: a page that uses the component reacts by handling the event and reading its payload as `event#/detail`. Declaring emits, and dispatching them, is covered in **[Statements](/docs/studio/logic/statements)**.\n\n:::doc-note\nA binding is stored on the element itself, as an `onclick` (etc.) key in the file's JSON — a `$ref` to a function, an `$expression`, or an inline function definition. The handler model is documented in **[Reactivity](/docs/framework/concepts/reactivity)**.\n:::\n\n## Next\n\n- Declare the functions your events call in the **[State panel](/docs/studio/logic/state)**\n- Watch state change as events fire, in the **[Data explorer](/docs/studio/logic/data-explorer)**\n- Multi-step handlers read best as **[Statements](/docs/studio/logic/statements)**\n",
    "section": "Studio",
    "slug": "studio/logic/events",
    "title": "Events panel",
    "url": "https://jxsuite.com/docs/studio/logic/events/"
  },
  {
    "description": "See the live values behind the open page in the Data activity — expand real data, refresh it, and try test values for component options.",
    "markdown": "\n# Data explorer\n\nData is the read side of the **[State panel](/docs/studio/logic/state)**: the same entries, but showing what each one is worth **right now**, as the canvas runs the page — the actual list your fetch returned, the current count, the parsed form. Open it by clicking **Data** in the activity bar. When something on the page looks wrong, this is where you find out what the page actually sees.\n\n![The Data activity with state entries expanded into live data trees](../../images/data-explorer.png)\n\n## Read the values\n\nEach state entry gets a row: its kind badge, its name, and a value summary —\n\n- `Array(12)` — a list and how many items it holds.\n- `{5}` — an object and how many fields it has.\n- `string`, `number`, `boolean` — a plain value's type.\n- `pending` — no value yet: a data source that hasn't finished resolving (or failed to).\n\nClick a row to expand the value as a tree. Nested objects and lists unfold a few levels deep, long lists show their first items with a \"… N more\" tail, and long strings are shortened — enough to verify shape and content without drowning in data.\n\n## Refresh\n\nThe values are a snapshot from the canvas render. Click **Refresh** in the panel's toolbar to re-render the canvas and read them again — useful after editing a data source, or when you want to re-fire a fetch.\n\n## Test values for component options\n\nA component file renders on the canvas with its options at their defaults. To see it with real-looking data, use the option fields in the tab bar: one small field per component option, as introduced in **[Modes and the preview toggle](/docs/studio/interface/modes)**.\n\n1. Open a component file. The tab bar shows a field named after each option.\n2. Type a test value. Values that read as JSON are treated that way — `42` is a number, `true` a flag, `[\"a\",\"b\"]` a list — and anything else is text.\n3. The canvas re-renders with the value, and the Data activity, template previews, and formula badges all see it.\n4. Clear the field to return that option to its authored default.\n\nTest values are a preview aid — they live with your editing session, not in the component file.\n\n## Debug with it\n\n- A `pending` Request usually means the URL is wrong or the server didn't answer — check the entry in the State panel, then **Refresh**.\n- A computed value that shows the wrong result: expand the entries it depends on here first; most \"formula bugs\" are actually surprising input data.\n- Events not visibly doing anything? Trigger them with **Preview** on and watch the target entry's row change.\n\n## Next\n\n- The entries themselves are declared in the **[State panel](/docs/studio/logic/state)**\n- Where the data comes from: **[Data sources](/docs/studio/logic/data-sources)**\n- The same live values ride along in the **[formula workspace](/docs/studio/logic/formula-workspace)**'s data rail\n",
    "section": "Studio",
    "slug": "studio/logic/data-explorer",
    "title": "Data explorer",
    "url": "https://jxsuite.com/docs/studio/logic/data-explorer/"
  },
  {
    "description": "The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.",
    "markdown": "\n# Data sources\n\nA data source is a state entry whose value comes from somewhere — a URL, the browser's storage, a form, your project's content, a database table — instead of being typed in. You add one from the **[State panel](/docs/studio/logic/state)**'s **+ Add…** picker, and from then on it behaves like any other value: bind it, compute from it, show it. Sources are reactive — when the underlying data changes, everything built on it updates. The model behind that is **[Reactivity](/docs/framework/concepts/reactivity)**.\n\nEach source's editor shows just the fields that source needs.\n\n## Request — fetch from a URL\n\n**Fetch (Request)** loads data over HTTP — a JSON API, most typically.\n\n![The State panel editing a Request source's URL, method, and timing](../../images/data-source-request.png)\n\n- **URL** — where to fetch from.\n- **Method** — `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`.\n- **Timing** — `client` fetches in the visitor's browser; `server` runs the request on the server instead — useful when an API can't be called from a browser.\n\nWhile the request is in flight the entry reads as `pending` in the **[Data explorer](/docs/studio/logic/data-explorer)**; the resolved response then flows wherever the entry is used.\n\n## LocalStorage and SessionStorage — remember in the browser\n\nBoth persist a value in the visitor's browser: **LocalStorage** survives closing the browser, **SessionStorage** lasts for the visit. Their editors are identical:\n\n- **Key** — the name the value is stored under.\n- **Default** — what the entry is worth before anything has been stored. JSON here gives you a structured default.\n\nWriting to the entry (from an event handler, say) stores it; the visitor's next visit reads it back. Think \"remember my theme choice\", \"keep the cart\".\n\n## Cookie — a value shared with the server\n\n- **Cookie** — the cookie's name.\n- **Default** — the value before the cookie exists.\n\nUse a Cookie instead of LocalStorage when the server needs to see the value on each request.\n\n## IndexedDB — a structured browser database\n\nFor larger structured data the browser stores locally:\n\n- **Database** — the database name.\n- **Store** — the object store within it.\n- **Version** — the schema version number.\n\n## FormData — the state of a form\n\n**FormData** holds a browser form-data object — the shape requests use to submit forms:\n\n- **Fields** — a JSON object naming the fields and their starting values.\n\nThe entry starts out seeded with those fields, ready to fill in and send as a request body.\n\n## Set and Map\n\nTwo small structural sources round out the built-ins: **Set** (a list without duplicates) and **Map** (a keyed collection). Each has a single **Default** field, edited as JSON.\n\n## ContentCollection — query your project's content\n\n**ContentCollection** turns your project's content — the entries behind your **[content types](/docs/studio/projects/content-types)** — into a queryable list: \"the six newest posts\", \"properties under $500k\". It's provided by the parser extension, so it appears in the **+ Add…** picker via your project's imports. Its form is generated from the source's own description:\n\n- **contentType** — which content type to query, picked from the ones your project defines.\n- **filter** — zero or more rules, each a **field** (picked from the content type's own fields), an **op** (`==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `not contains`, `empty`, `not empty`), and a **value**.\n- **sort** — zero or more rules, each a **field** and an **order** (`asc` or `desc`).\n- **limit** — the maximum number of entries to return.\n\nOn a page with a dynamic address, a field holding a reference shows a binding picker instead of a plain value — **Static value**, the page's URL parameters, or **Custom…** — so a detail page can query \"the entry this URL names\".\n\n## Database sources\n\nEverything above reads from the browser, the network, or your project's files. With the connector extension enabled, your **[data tables](/docs/studio/data)** join the picker too — the data that appears and changes while the site runs. Each of these sources starts with a **table**, the name of a table your project defines:\n\n- **TableQuery** — the rows matching a query. **filter** and **sort** take the same rules as ContentCollection above, and **limit** and **offset** page through the results; **include** names reference fields to expand, so a query for posts can carry each post's author with it.\n- **TableEntry** — a single row, named by **id**, with the same **include**. On a page with a dynamic address the id can bind to a URL parameter, which is how a detail page shows the row its URL names.\n- **TableInsert**, **TableUpdate**, and **TableDelete** — writes rather than reads. Insert and update take **values**, literal columns merged over the fields of the form that submits them; update and delete take an **id**. Each resolves to a handler, so you point a form's submit event at one instead of displaying it — and once a write lands, the queries on the page refresh themselves.\n\n## Account sources\n\nThe auth extension adds two more, both about who is looking at the page:\n\n- **Session** — the signed-in visitor: a `userId`, their `role` if your project defines roles, and the user record. The whole entry is `null` when nobody is signed in — not an empty object — so test the entry itself before reaching into it for a `userId`. It updates live when someone signs in or out, and it is also always `null` on a page as it is generated: every prerendered page ships its signed-out view and fills in once the browser has checked. The one field, **baseUrl**, defaults to your own site's `/_jx/auth`.\n- **AuthActions** — the handlers behind sign-up, sign-in, social sign-in, and sign-out, wired to a form's submit event like the table actions. **redirects** sets where a visitor lands after signing in or out, **provider** names the default social provider, and **baseUrl** is the same as above.\n\nBoth extensions are covered from the other side — the tables, the settings, the secrets — in **[Databases](/docs/studio/data)** and **[Auth and secrets](/docs/studio/data/auth-and-secrets)**.\n\n## External sources\n\n**External Module…** points an entry at a JavaScript module of your own (**Source** and **Prototype** fields); if the module describes its options, Studio renders them as a form, the same way it does for ContentCollection. Sources added by any other installed extension list themselves in the **+ Add…** picker the same way.\n\n:::doc-note\nEvery source is stored as a small JSON object in the file's `state` — a `$prototype` naming the kind plus the fields above. Nothing here is code; the runtime interprets these declarations, as described in **[Reactivity](/docs/framework/concepts/reactivity)**. The table sources are rewritten at build time into ordinary fetches against `/_jx/data`, so no extension code ships to the browser.\n:::\n\n## Next\n\n- Watch a source resolve, live, in the **[Data explorer](/docs/studio/logic/data-explorer)**\n- Compute over fetched data with **[Formulas and expressions](/docs/studio/logic/formulas)**\n- Content types themselves are managed in **[Content types](/docs/studio/projects/content-types)**\n",
    "section": "Studio",
    "slug": "studio/logic/data-sources",
    "title": "Data sources",
    "url": "https://jxsuite.com/docs/studio/logic/data-sources/"
  },
  {
    "description": "Drop down to real code when you need it — Monaco for function bodies, sidecar files, and Code mode for a document's raw source.",
    "markdown": "\n# Code editing\n\nEverything in the Logic pages so far works without writing code. But Studio doesn't pretend code doesn't exist — when a function outgrows **[statements](/docs/studio/logic/statements)**, or you want to see exactly what a file contains, you get a real editor: Monaco, the same component that powers VS Code, with syntax highlighting, completions, and inline error checking.\n\n![Jx Studio editing a component state function in the Monaco code editor](../../images/mode-script.png)\n\nThere are two distinct code surfaces, for two different jobs.\n\n## The function editor\n\nA function body in **Code** mode (the **Statements**/**Code** toggle) is JavaScript. The small text field in the panel is fine for one line; for anything more, click the code icon — **Open in code editor** on a State-panel function, **Open in editor** on an inline event handler. The editor takes over the canvas, and the tab bar shows a breadcrumb — the file's name, then _ƒ_ and the function's name — with a **Back** button to return.\n\nWhat you get:\n\n- **Formatting on open** — the body is pretty-printed before you start.\n- **Live linting** — problems are underlined as you type, with the message on hover.\n- **Completions** — type `state.` to see every entry from the **[State panel](/docs/studio/logic/state)** (your values, data sources, and functions), and `window.` for the standard library (`Math`, `JSON`, …). Named formulas carry their descriptions into the suggestions.\n- **Automatic write-back** — edits flow into the document as you type; there is no separate apply step. Save the file as usual when you're done.\n\nInside a body, `state` holds your entries (`state.$count += 1` is the code twin of a **Set state** statement), event handlers also receive `event`, and a function's declared parameters are available by name.\n\n## Sidecar files\n\nA function body normally lives inside the component's JSON. When one grows large enough to deserve its own file, it can live in a separate `.js` file instead: the function entry then shows **Source** (the file's path) and **Export** (which function to use from it) in the State panel, in place of a body. The format is documented in **[Components](/docs/framework/concepts/components)**.\n\n## Code mode: the whole file as source\n\nThe **Code** entry in the toolbar's mode switcher shows the open file itself as raw source — JSON for pages and components, Markdown for content — as introduced in **[Modes and the preview toggle](/docs/studio/interface/modes)**. It's the same document the visual surfaces edit, from the other side:\n\n- Edits parse back into the document as you type, so switching back to **Edit** or **Design** shows your changes. While the source is momentarily unparseable mid-edit, Studio simply waits — it never replaces your document with a broken parse.\n- JSON files are checked against your project's own schema as you type — mistyped keys, wrong value types and missing required properties are underlined, and :kbd[Ctrl+Space] completes property names. Studio uses the `project.schema.json` and `document.schema.json` that [`jx schema`](/docs/framework/build/cli) generates from your enabled extensions, so the editor enforces exactly what `jx validate` does, including extension-contributed sections. It reads them directly, with no network access — an offline project still gets full validation.\n- **Export** in the tab bar saves a copy of the file elsewhere.\n\n:::doc-note\nYou never have to generate those files yourself: Studio refreshes them whenever they are missing or out of date, so a project you have never run `jx schema` on still validates, and turning an extension on or off updates the rules without a restart. In the browser at [studio.jxsuite.com](https://studio.jxsuite.com) the rules are composed for you on the server and nothing is written to your repository at all.\n:::\n\n## When to drop down\n\n- A handler needs a loop, error handling, or an API the structured editors don't cover.\n- You're doing a bulk edit — renaming a state entry everywhere it's referenced is a find-and-replace in Code mode.\n- You want to add parameters to a named formula, or make other edits the panels don't surface.\n- You're learning the format: build something visually, then read it in Code mode. It's the fastest way to understand what Studio writes.\n\n:::doc-tip\nEverything the visual editors do lands in the same file you see in Code mode — there is no hidden layer. If you can express a change in either surface, the result on disk is the same kind of JSON.\n:::\n\n## Next\n\n- The file format you're reading in Code mode: **[Components](/docs/framework/concepts/components)** and **[Reactivity](/docs/framework/concepts/reactivity)**\n- Prefer structure when it fits: **[Statements](/docs/studio/logic/statements)**\n- Ship your work with **[Source control](/docs/studio/publish/source-control)**\n",
    "section": "Studio",
    "slug": "studio/logic/code",
    "title": "Code editing",
    "url": "https://jxsuite.com/docs/studio/logic/code/"
  },
  {
    "description": "Connect a real database to your Jx site: connections, data tables, schema push, and the data grid — from a local SQLite file to D1 or Supabase.",
    "markdown": "\n# Databases\n\nYour content collections are files in your project — right for pages, posts, and anything you write and publish. A database holds the other kind of data: rows that appear and change while the site runs — comments, sign-ups, orders, form submissions. The connector extension adds that half to Jx: you describe your database and its tables in Studio, pages read and write the rows, and a spreadsheet-style grid lets you browse and fix the data at any time.\n\nYou don't need to have run a database before. The default setup is a single file inside your project — nothing to install, no account to create — and the same table definitions carry over unchanged when you later point them at a hosted service.\n\n![The Settings modal open on the Data Tables section with a table selected and its field schema visible](../images/data-tables-section.png)\n\n## Where it lives\n\nOpen the **Settings** gear at the bottom of the activity bar. Projects with the connector extension enabled get two extra sections in the list:\n\n- **Connections** — which database (or databases) the project talks to: **[Connections](/docs/studio/data/connections)**.\n- **Data Tables** — the tables inside them: fields, ids, and access rules: **[Data tables](/docs/studio/data/tables)**.\n\nBoth sections carry an action row with **Test Connection**, **Push Schema**, and **Open Data Grid** — the grid opens as its own tab on the canvas: **[Data grid](/docs/studio/data/grid)**.\n\n:::doc-note\nThe sections are contributed by the `@jxsuite/connector` extension, listed in the `extensions` array of `project.json` — the same mechanism that brings Content Types. Everything you edit in them is stored as the `connections` and `data` sections of `project.json`; only the table _definitions_ live there — the rows themselves stay in the database. See [project.json](/docs/framework/site/project-json).\n:::\n\n## From empty to live data\n\n1. **Add a connection** — name where the data lives. A new connection starts as SQLite, a database file inside your project that works immediately.\n2. **Define tables and push** — build each table's fields in the visual schema builder, then **Push Schema** creates the real tables. A dry-run plan always shows you what will happen first, and pushes only ever _add_ — they never drop or rewrite what exists.\n3. **Browse in the grid** — insert, edit, and delete rows in a paged spreadsheet view.\n\nPages then use the data through the **[State panel](/docs/studio/logic/state)**: the connector provides table query and table action sources that list, filter, and write rows — they appear in the **+ Add…** picker like any other **[data source](/docs/studio/logic/data-sources)**, and a form's submit event can point straight at a table insert.\n\n## Local and deployed\n\nWhile you build, everything runs against local files:\n\n- A **SQLite** connection is a file at `.jx/data/<name>.sqlite` inside your project — created on first use.\n- A **Cloudflare D1** connection is stood in locally by a SQLite file of the same kind, so you develop against it without touching the real database. Deployed, the same tables live in D1 itself.\n- A **Supabase** connection is Postgres — a hosted service you connect to with a URL.\n\nAnything secret — a database URL, an API token — never enters your project files: Studio stores values in the git-ignored `.dev.vars` file locally and only the _names_ travel anywhere. How that works (and what the auth extension adds) is covered in **[Auth and secrets](/docs/studio/data/auth-and-secrets)**.\n\n## Availability\n\nThe database console needs a Studio backend that can reach databases: the local dev server and the **[desktop app](/docs/studio/desktop)** both can. On a backend without database access the sections still show your definitions, but the Test, Push, and grid actions stay hidden.\n\nThe command-line equivalent of the push button is `jx db push` — same plan, same additive rules — documented in the **[CLI reference](/docs/framework/build/cli)**.\n\n## Next\n\n- **[Connections](/docs/studio/data/connections)** — add SQLite, D1, or Supabase\n- **[Data tables](/docs/studio/data/tables)** — fields, relationships, permissions, and Push Schema\n- **[Data grid](/docs/studio/data/grid)** — browse and edit the rows\n- **[Auth and secrets](/docs/studio/data/auth-and-secrets)** — sign-ins, access rules, and the secret store\n",
    "section": "Studio",
    "slug": "studio/data",
    "title": "Databases",
    "url": "https://jxsuite.com/docs/studio/data/"
  },
  {
    "description": "Add a database connection in Jx Studio — SQLite, Cloudflare D1, or Supabase — env-var names in project files, secret values kept out, Test Connection.",
    "markdown": "\n# Connections\n\nA connection names a database your project talks to. Most sites need exactly one; you can add more if, say, orders and analytics live in different places. Open the **Settings** gear at the bottom of the activity bar, then _Settings > Connections_: your connections are listed on the left, and selecting one opens its form on the right.\n\n![The Connections section with a connection selected and its provider form open](../../images/connections-section.png)\n\n## Add a connection\n\n1. Click **New Entry** at the bottom of the list.\n2. Type a name — `main` is a fine first choice — and click **Create**.\n\nThe new connection starts on the **sqlite** provider: a database file inside your project, no installation and no account. If that's where you want to stay, you're done — the file is created the first time the database is touched.\n\nTo use a different backend, change the **provider** field to one of the ids below. The form is generated from the provider's own settings, and every field carries its explanation.\n\n## SQLite — a file in your project\n\nThe zero-setup option, and the local stand-in the others build on.\n\n- **file** — where the database file lives, relative to the project root. Leave it empty for the default, `.jx/data/<name>.sqlite`.\n\n## Cloudflare D1 — provider `d1`\n\nCloudflare's hosted SQLite, for sites deployed to Workers or Pages (see **[Publish to Cloudflare](/docs/studio/publish/cloudflare)**):\n\n- **binding** — the name your deployed site reaches the database under.\n- **databaseId** — the database's UUID from the Cloudflare dashboard. An identifier, not a secret.\n- **accountId** — your Cloudflare account id; leave it empty to use the `CLOUDFLARE_ACCOUNT_ID` environment variable.\n\nWhile you develop, a D1 connection is stood in by a local SQLite file at `.jx/data/<name>.sqlite` — you build and test against your own machine, and the real D1 database is only touched when you push the schema to it or the deployed site runs. Reaching real D1 from outside a deployed Worker uses Cloudflare's API and needs a `CLOUDFLARE_API_TOKEN` value in your secrets.\n\n## Supabase — provider `supabase`\n\nA hosted Postgres service:\n\n- **urlEnv** — the database connection URL. This field is a **secret field**: paste the URL from your Supabase dashboard and Studio stores the _value_ in the secret store, keeping only the environment-variable _name_ it's filed under (the field then reads \"Stored as …\"). See below.\n- **hyperdriveId** and **binding** — for sites deployed to Cloudflare Workers, the Hyperdrive configuration that fronts the Postgres connection there. Leave both empty otherwise.\n\n## Names on file, values in the vault\n\nNothing secret is ever written into your project: connection entries record identifiers and environment-variable _names_ only. When you paste a value into a secret field, Studio saves it under a name derived from the connection — the URL of a connection called `main` becomes `MAIN_URL` — and locally that lands in the git-ignored `.dev.vars` file. The value is never shown back and never leaves the machine it's stored on. The full story, including what deployed sites use instead, is in **[Auth and secrets](/docs/studio/data/auth-and-secrets)**.\n\n## Test a connection\n\nSelect a connection in the list and click **Test Connection** in the action row. Studio asks the backend to run a minimal probe query against that database and reports beside the buttons — \"main: connected\", or the error that came back (an unreachable host, a missing secret, a bad URL). The button is disabled until a connection is selected.\n\nWith a connection selected, **Push Schema** in the same row is scoped to just that connection — table schemas and pushing are covered in **[Data tables](/docs/studio/data/tables)**.\n\n:::doc-note\nThis section edits the `connections` section of `project.json` — one entry per connection, holding the provider id and the identifier fields above, never secret values. On backends without database access (see [Databases](/docs/studio/data)), the entries remain editable but the Test, Push, and grid actions are hidden.\n:::\n\n## Next\n\n- **[Data tables](/docs/studio/data/tables)** — define what's _in_ the database\n- **[Auth and secrets](/docs/studio/data/auth-and-secrets)** — where the secret values actually live\n",
    "section": "Studio",
    "slug": "studio/data/connections",
    "title": "Connections",
    "url": "https://jxsuite.com/docs/studio/data/connections/"
  },
  {
    "description": "Define database tables in Jx Studio's Data Tables section: the visual field schema, ids, indexes, permissions — and the additive, dry-run Push Schema flow.",
    "markdown": "\n# Data tables\n\nA data table is like a content type for live data: a name, a set of fields, and the connection its rows are stored in. You define tables here; **Push Schema** then creates them in the actual database. Open the **Settings** gear at the bottom of the activity bar, then _Settings > Data Tables_ — tables on the left, the selected table's editor on the right.\n\n![The Data Tables editor pane showing the connection picker and field schema builder](../../images/data-table-editor.png)\n\n## Create a table\n\n1. Click **New Entry** at the bottom of the table list.\n2. Type a name — \"Comments\" becomes `comments` — and click **Create**.\n\nThe new table starts empty, readable by everyone and writable by no one. First pick its **connection** — a dropdown of the entries from **[Connections](/docs/studio/data/connections)**.\n\n## Build the fields\n\nThe **schema** editor is the same visual field builder as **[content types](/docs/studio/projects/content-types)**: each field has a name, a type (string, number, boolean, array, object, reference), an optional format for string and array fields, and a **Req** toggle. Required fields must be provided whenever a row is inserted.\n\nA **reference** field links a row to something else — its **Target** picker lists your content types, and the row stores the target entry's id. How references resolve is the same story as content **[relationships](/docs/framework/site/relationships)**.\n\n:::doc-note\nUnder the hood a table's fields are ordinary Jx field schemas in the `data` section of `project.json`. The schema format also allows table-to-table references — a to-one reference becomes a `<field>_id` column, and a to-many reference between two tables materializes a junction table on push — but the visual Target picker currently offers content types; table targets are written in the JSON directly.\n:::\n\n## Table options\n\n- **id** — how rows are identified: `uuid` (random text ids, the default) or `integer` (1, 2, 3…).\n- **timestamps** — on by default; every row gets `created_at` and `updated_at` columns the server maintains for you.\n- **indexes** — column names to index for faster lookups; an inner list makes one composite index.\n- **permissions** — who may do what, one rule per action (`read`, `insert`, `update`, `delete`). Rules are `public` (anyone), `none` (no one), `authenticated` (any signed-in user), `owner` (the row's owner), or `role:<name>`. Defaults are read `public` and every write `none` — nothing is writable until you say so. Every rule beyond `public` and `none` needs the auth extension; without it those actions are simply denied. The rules are explained in **[Auth and secrets](/docs/studio/data/auth-and-secrets)**.\n- **ownerField** — the column that records which user a row belongs to; required for `owner` rules, and stamped automatically on signed-in inserts.\n\n## Push the schema\n\nDefining a table describes it; pushing creates it. Click **Push Schema** in the action row:\n\n1. Studio first compiles a **plan** — a dry run, nothing touched yet — and shows it as a list of steps: tables to create, columns to add, indexes, junction tables, and (with the auth extension) its account tables. Warnings appear below the steps. If everything already matches, the dialog says \"Nothing to push — the schema is up to date\" and there is no apply button.\n2. Click **Apply** to execute the plan, or **Cancel** to back out. The dialog then reports \"Schema applied.\" — or the errors, with nothing half-done claimed.\n\nFrom the Data Tables section a push covers every connection; from the Connections section, selecting a connection first scopes the push to it.\n\nPushes are **additive only**: they create missing tables and columns and never drop, rename, or retype anything that exists. Removing a field from a schema leaves its column (and its data) in place — the plan notes such drift as a warning instead of destroying data. This makes pushing safe to run repeatedly.\n\nA terminal or CI can push too: `jx db push` applies the same additive rules and takes the same `--dry-run` flag — see the **[CLI reference](/docs/framework/build/cli)**. Two differences matter. The CLI pushes the tables you define here and nothing else, so the auth extension's account tables come only from the button above. And the CLI talks to each connection exactly as declared, while the button goes through Studio's local backend — so for a **Cloudflare D1** connection, which is stood in by a local SQLite file while you develop, `jx db push` is what creates the tables in the real D1 database.\n\n## Using tables from pages\n\nRows never pass through your project files — pages talk to the tables live. In the **[State panel](/docs/studio/logic/state)**, the connector's sources appear in the **+ Add…** picker alongside the built-in **[data sources](/docs/studio/logic/data-sources)**:\n\n- **Table query** — a list of rows. **filter** and **sort** take the same rules as a content collection query; **limit** and **offset** page through a longer list; **include** names reference fields to expand, so a query on `comments` can come back with each row's linked `post` filled in — the whole row, not just its id — and to-many references expand into arrays the same way.\n- **Table entry** — one row by **id**. The id can be a fixed value, a `${…}` expression, or the current route's parameter, which is how a detail page like `pages/posts/[id].json` fetches exactly the row its URL names.\n- **Table insert**, **update**, and **delete** — write actions, made to be wired to a form's submit event. After a successful write, every query on the page refreshes itself.\n\nFiltering, sorting, and paging all happen in the database rather than in the browser, so a query with a limit fetches only that many rows.\n\n:::doc-note\nCompiled pages carry no extension code. Each source is lowered at build time into a plain request to your site's own `/_jx/data` routes — `GET /_jx/data/comments?filter=…&sort=…&limit=…&offset=…&include=…` for a query, `GET /_jx/data/comments/<id>` for an entry, `POST`/`PATCH`/`DELETE` for the actions — and each write action into an inline handler that reads the submitted form. Permission rules are evaluated on the server for every one of those requests.\n\nIn the JSON, a route parameter binds as `{ \"$ref\": \"#/$params/id\" }` (the same way a **[dynamic route](/docs/framework/site/routing)** binds a content entry), and a form points at a write action with `\"onsubmit\": { \"$ref\": \"#/state/addComment\" }`. That last one is written in **[Code mode](/docs/studio/logic/code)** today — the **[Events panel](/docs/studio/logic/events)** picker offers plain functions only.\n:::\n\nTo see and fix the rows by hand, open the **[Data grid](/docs/studio/data/grid)**.\n\n## Next\n\n- **[Data grid](/docs/studio/data/grid)** — browse, insert, and edit rows\n- **[Auth and secrets](/docs/studio/data/auth-and-secrets)** — make permission rules beyond `public` work\n",
    "section": "Studio",
    "slug": "studio/data/tables",
    "title": "Data tables",
    "url": "https://jxsuite.com/docs/studio/data/tables/"
  },
  {
    "description": "Browse and edit live database rows in Jx Studio's data grid: paging, typed cells, batched Save, add and delete rows — and how it differs from file grids.",
    "markdown": "\n# Data grid\n\nThe data grid is **[Grid mode](/docs/studio/editing/grid)** pointed at a database table: the same spreadsheet surface — typed cells, range selection, fill down, find & replace, one batched **Save** — but the rows come from a live database instead of files in your project. Use it to check what visitors have submitted, fix a bad value, seed test rows, or clean out old data.\n\n![A database table open as a grid tab with the toolbar and pager visible](../../images/data-grid.png)\n\n## Open a table\n\nClick **Open Data Grid** in the action row of _Settings > Connections_ or _Settings > Data Tables_. The picker lists every grid-able source in one place — pages and content collections under **Project**, then a **Data** group per connection with its tables. A connection with no tables yet shows \"No tables — push a schema first\": define and push in **[Data tables](/docs/studio/data/tables)**, then come back.\n\nEach table opens as its own tab, so you can keep a grid alongside the page you're building.\n\n## Read the columns\n\nColumns come straight from the real database, so the grid always shows what's actually there — including columns your schema no longer mentions, and the account tables the auth extension creates. Three columns are managed for you and read-only: the id column (pinned at the left) and the `created_at` / `updated_at` timestamps. Everything else edits with the control its type calls for — text, number, checkbox for booleans, a date field for dates; structured values show as JSON text.\n\n## Page through rows\n\nBig tables aren't loaded whole: the grid shows 50 rows at a time, with **‹ Prev** / **Next ›** and the current range next to the total count in the toolbar. The **Filter rows** box searches within the rows currently loaded — use the pager to move through the rest. Per-column filters and click-to-sort, which file grids offer, aren't available on database grids yet.\n\n## Edit, add, delete\n\nEditing works exactly as in **[Grid mode](/docs/studio/editing/grid)** — double-click a cell, edit ranges, **Fill Down**, **Replace**, undo and redo — and nothing touches the database until **Save**:\n\n- **Add Row** appends a pending row; fill in its cells (required fields must be provided).\n- **Delete Rows** marks the selected rows; they're removed on save, after a confirmation.\n- **Save** (:kbd[⌘S] / :kbd[Ctrl+S]) writes the whole batch — the button counts your pending changes.\n\nOn save, each row becomes its own write to the database. Rows that succeed are done; a row the database refuses — a missing required value, a wrong type — stays pending with the reason attached to its cells, so you can fix it and save again. There are no stale-file checks here because there are no files: the database itself is the authority, and **Refresh** re-reads it at any time.\n\n:::doc-warning\nDatabase rows aren't files in your project — they're not in version control, and a saved deletion cannot be undone. Studio confirms before deleting, but after that the rows are gone.\n:::\n\n## Where it's different from file grids\n\n|                 | File grids (collections, pages, CSV)                                     | Data grid                  |\n| --------------- | ------------------------------------------------------------------------ | -------------------------- |\n| Rows are        | files in your project                                                    | rows in a database         |\n| Loaded          | whole                                                                    | 50 per page                |\n| Save writes     | files (frontmatter, CSV cells)                                           | one database write per row |\n| Conflict safety | stale-file detection                                                     | per-row database errors    |\n| Undo after save | revert the file in [Source control](/docs/studio/publish/source-control) | none — the write is live   |\n\n## User accounts in the grid\n\nWith the auth extension enabled, its `user` and session tables show up in the picker like any other table. Editing user rows here is also how roles are assigned — see **[Auth and secrets](/docs/studio/data/auth-and-secrets)**.\n\n## Next\n\n- **[Grid mode](/docs/studio/editing/grid)** — the shared spreadsheet surface in full: ranges, fill down, find & replace\n- **[Data tables](/docs/studio/data/tables)** — change what columns a table has\n",
    "section": "Studio",
    "slug": "studio/data/grid",
    "title": "Data grid",
    "url": "https://jxsuite.com/docs/studio/data/grid/"
  },
  {
    "description": "How Jx keeps secret values out of your project — names on the wire, values in .dev.vars — and how the auth extension adds accounts, sessions, and sign-in.",
    "markdown": "\n# Auth and secrets\n\nTwo related subjects live here: how Studio handles values that must stay secret (database URLs, signing keys, API credentials), and what the auth extension — user accounts and sign-in for your site — adds to Studio.\n\n## Secrets: values stay put, names travel\n\nA hard rule runs through everything database-related: **secret values never enter your project files.** What `project.json` records is only the _name_ of an environment variable — `MAIN_URL`, `AUTH_SECRET` — and the value lives elsewhere:\n\n- **Locally**, values are stored in `.dev.vars` at your project root — a plain name-equals-value file that is ignored by git, so a commit can never carry a credential. The dev server and the desktop app read it automatically whenever a database or auth feature needs the value.\n- **Deployed**, the same names are looked up in your host's environment. You set the values there once — for Cloudflare, with `wrangler secret put` or the dashboard's environment settings.\n\nIn Studio you meet this as the **secret field**: settings that hold something sensitive (a Supabase URL in **[Connections](/docs/studio/data/connections)**, the auth signing secret below) render as a password-style box. Paste the value and press :kbd[Enter]; Studio sends it to the backend's secret store, the box empties, and from then on it just reads \"Stored as MAIN_URL\" — the value is write-only and is never displayed or sent back to the browser again. The derived name is what gets written into `project.json`. To replace a value, paste a new one; to inspect what's stored, open `.dev.vars` itself — Studio will only ever show you the names.\n\n:::doc-warning\n`.dev.vars` is the one place your local secret values exist — it is deliberately not committed, so back it up your own way, and re-enter the values (or copy the file) when moving to another machine.\n:::\n\n## The auth extension\n\nThe `@jxsuite/auth` extension gives your site user accounts: sign-up and sign-in, sessions, and the table permission rules that depend on knowing who someone is. It's honest to say up front that **most of auth is server-side** — it runs a full authentication service (Better Auth) inside your site, mounted at `/_jx/auth`, and that machinery has no Studio panels of its own. What you see in Studio is three things: a settings section, its account tables in the data grid, and its building blocks for sign-in pages.\n\n### Turn it on\n\nAuth is an extension package, and it keeps its accounts in a database, so it builds on what **[Databases](/docs/studio/data)** already set up:\n\n1. **Have a connection.** Add one in **[Connections](/docs/studio/data/connections)** if the project has none — the account tables live on one of them.\n2. **Install the package.** Type `@jxsuite/auth` into _Settings > Dependencies_ and click **Add** (see **[Dependencies and imports](/docs/studio/projects/dependencies)**).\n3. **List it as an extension.** Open `project.json` and add `\"@jxsuite/auth\"` to its `extensions` array — that list is what switches the section on. Studio has no field for it yet, so this one edit happens in **[Code mode](/docs/studio/logic/code)**; the array is described in **[project.json](/docs/framework/site/project-json)**.\n4. **Pick a server-capable adapter.** In **[Project settings](/docs/studio/projects/settings)**, set the deployment target to Cloudflare Pages, Cloudflare Workers, Node, or Bun. A site with accounts is no longer purely static, and the build says so if you leave it on Static.\n\nReopen Settings and an **Authentication** section is waiting. Filling it in writes an `auth` section into `project.json`.\n\n### The Authentication settings section\n\nThe section is a single form, and every field is optional — the defaults below are what applies when you leave one blank:\n\n- **connection** — which of your **[connections](/docs/studio/data/connections)** stores the account tables. Defaults to the first one you declared.\n- **secretEnv** — the _name_ of the environment variable holding the signing secret that protects sessions. Defaults to `BETTER_AUTH_SECRET`. It renders as a secret field: paste any long random string and it's stored as described above. Auth refuses to start without a value for it.\n- **methods** — first-party sign-in methods; **emailPassword** (on by default) enables classic email + password accounts.\n- **providers** — social sign-in, keyed by provider (`github`, `google`, …), each with a **clientIdEnv** and **clientSecretEnv**. As everywhere, these are env-var _names_; leave them blank and the provider's own defaults apply (`GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`). Setting one up takes a couple more steps — see **Social sign-in** below.\n- **redirects** — **afterSignIn** and **afterSignOut**: the paths a visitor lands on after each. Leave them empty and the page stays where it is.\n- **roles** — role names you want to grant (say `admin`, `editor`), usable in table rules as `role:admin`. Declaring any role is also what adds the `role` column to the `user` table.\n- **trustedOrigins** — extra origins allowed to call the sign-in routes; most sites leave this empty, since a site's own pages are always allowed.\n\n### Account tables and roles\n\nAuth keeps its users and sessions in ordinary database tables (`user`, `session`, `account`, `verification`) on the connection you chose, and they show up in the **[data grid](/docs/studio/data/grid)** like any other table. That grid _is_ the user-management surface today: to make someone an `admin`, open the `user` table and set their `role` cell. Sign-up can never set a role, so roles only come from you.\n\nThe tables are created for you two ways: `jx dev` creates them the first time anything touches `/_jx/auth`, and **[Push Schema](/docs/studio/data/tables)** plans them as its own steps after the connector's, so a dry run shows them before anything runs.\n\n:::doc-warning\n`jx db push` from a terminal covers your **Data Tables** only — the account tables are not part of the CLI push today. A database that only ever received a CLI push has no `user` table, and sign-in fails against it.\n:::\n\n### What the rules mean\n\nDeclaring auth is what makes table **permissions** beyond `public`/`none` work (**[Data tables](/docs/studio/data/tables)** is where you set them):\n\n- `authenticated` — any signed-in user.\n- `owner` — the user a row belongs to. The table's **ownerField** column is stamped with the signed-in user's id on every insert — visitors can't forge ownership — and reads and writes are scoped to their own rows.\n- `role:<name>` — users whose `role` matches.\n\nWithout the auth extension these rules simply deny — the door fails closed, never open.\n\n### Sign-in pages\n\nThere are no ready-made sign-in components to drop in. You build the pages yourself, from ordinary elements plus two sources the extension adds to the **+ Add…** picker in the **[State panel](/docs/studio/logic/state)**, alongside every other **[data source](/docs/studio/logic/data-sources)**.\n\n**Session** — who is signed in, kept up to date as that changes. It gives you the signed-in user's **id**, their whole **user** record (email, name, and anything else the account carries), and their **role** when they have one — or nothing at all when no one is signed in. Bind a greeting to the email, and hide the \"Sign in\" link when a session exists.\n\nOne thing to plan around: **the session is always empty at build time.** Your pages are prerendered, and there is no visitor and no browser during a build, so the HTML that ships always shows the signed-out state; the signed-in version appears a moment later, once the page loads and asks the server who is there. That is a consequence of how Jx publishes, not a bug — so keep the signed-out state presentable, and don't put anything at the top of a page that only makes sense to someone signed in.\n\n**Auth actions** — the four handlers a form's submit event points at: **sign in with email**, **sign up with email**, **sign in with a provider**, and **sign out**. Each one reads the fields of the form that submitted it, so the **name** you give each input is the contract:\n\n| Handler        | Reads                                                                                                   |\n| -------------- | ------------------------------------------------------------------------------------------------------- |\n| `signInEmail`  | `email`, `password`                                                                                     |\n| `signUpEmail`  | `email`, `password`, and optionally `name` — without it, the part of the address before the `@` is used |\n| `signInSocial` | `provider` — or the default provider set on the state entry                                             |\n| `signOut`      | nothing — wire it to a button's click                                                                   |\n\nAn input named anything else is invisible to the handler, and the attempt goes out with an empty value.\n\nOn success a handler stops the browser's own form submission, refreshes the session, re-runs the page's table queries — so a list scoped to `owner` fills in the moment someone signs in — and sends the visitor to the matching **redirect** if you configured one. On failure (wrong password, an address that already has an account) the page is simply left as it was; surfacing a message is up to you.\n\n:::doc-note\nThe wiring is two state entries and one property on the form. The **[Events panel](/docs/studio/logic/events)** picker only offers plain functions, so point the form at its handler in **[Code mode](/docs/studio/logic/code)**:\n\n```json\n{\n  \"state\": {\n    \"session\": { \"$prototype\": \"Session\", \"timing\": \"client\" },\n    \"auth\": { \"$prototype\": \"AuthActions\", \"timing\": \"client\" }\n  },\n  \"tagName\": \"main\",\n  \"children\": [\n    {\n      \"tagName\": \"form\",\n      \"onsubmit\": { \"$ref\": \"#/state/auth/signInEmail\" },\n      \"children\": [\n        { \"tagName\": \"input\", \"attributes\": { \"type\": \"email\", \"name\": \"email\", \"required\": \"\" } },\n        {\n          \"tagName\": \"input\",\n          \"attributes\": { \"type\": \"password\", \"name\": \"password\", \"required\": \"\" }\n        },\n        { \"tagName\": \"button\", \"textContent\": \"Sign in\" }\n      ]\n    },\n    { \"tagName\": \"p\", \"textContent\": \"${state.session ? state.session.user.email : 'Signed out'}\" }\n  ]\n}\n```\n\n`Session` resolves to `{ userId, role?, user }` or `null`, so `${state.session?.userId}` and `${state.session?.role === 'admin'}` are the expressions to reach for. `AuthActions` resolves to a map of the four handlers, which is why the reference is `#/state/auth/signInEmail`. Both belong to the browser — `\"timing\": \"client\"` is what the State panel writes for them, and it keeps the build from trying to resolve a session that can't exist yet. Both also accept an optional `baseUrl`; without one they talk to the site's own `/_jx/auth`.\n:::\n\n### Social sign-in\n\nA provider needs three things, in this order:\n\n1. **Register an OAuth app** with the provider (GitHub, Google, …). The redirect — or callback — URL to give it is your site's origin plus `/_jx/auth/callback/` and the provider's id: `https://example.com/_jx/auth/callback/github`. Add a second app, or a second callback URL, for local development against `http://localhost:3000`.\n2. **Store the credentials as secrets**, under the names the settings form shows — `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` unless you changed them. Locally that means `.dev.vars`; deployed, your host's secret store.\n3. **Point a form at the social sign-in handler**, with the provider id in a field named `provider` (a hidden input on a per-provider button works well).\n\nTwo behaviors to know. A provider whose id or secret has no value is dropped from the configuration entirely — nothing fails at startup, the sign-in attempt just doesn't work — so check the names match if a button does nothing. And if your site reaches visitors through a different origin than the worker sees (a proxy, or a custom domain in front of it), set `BETTER_AUTH_URL` to the public origin, `https://example.com`; otherwise the origin is taken from each incoming request and the provider can send people back to the wrong place. `BETTER_AUTH_URL` isn't secret, but it's set the same way as the other environment values.\n\n### Current limits\n\n- No emails are sent yet — no address verification and no password reset; email accounts work immediately after sign-up.\n- A table with `insert: \"public\"` accepts writes from anyone on the internet. Prefer `authenticated` inserts unless you knowingly want an open drop-box.\n- Sign-in errors aren't surfaced for you; a failed attempt leaves the form as it was.\n\n## Next\n\n- **[Data tables](/docs/studio/data/tables)** — put the permission rules on your tables\n- **[Data grid](/docs/studio/data/grid)** — manage users and roles by editing rows\n- **[project.json](/docs/framework/site/project-json)** — the `auth`, `connections`, and `data` sections as committed config\n- **[Security](/docs/framework/concepts/security)** — what the server enforces, and what it never trusts\n",
    "section": "Studio",
    "slug": "studio/data/auth-and-secrets",
    "title": "Auth and secrets",
    "url": "https://jxsuite.com/docs/studio/data/auth-and-secrets/"
  },
  {
    "description": "What the Studio AI assistant can do with a project or page open, how to connect an AI provider, and exactly what leaves your machine when you chat.",
    "markdown": "\n# AI assistant\n\nStudio has a built-in AI assistant: a chat sidebar that doesn't just talk about your project but works on it — it creates pages and components, edits the page on the canvas while you watch, and answers questions about what it finds in your files. It runs against an AI provider **you** connect; Studio ships no account, no hosted AI, and sends nothing anywhere until you do.\n\n![The assistant sidebar open beside a page on the canvas, mid-conversation](../images/ai-sidebar.png)\n\nOpen it with the **Toggle Assistant** chat-bubble button at the right end of the toolbar. The sidebar is always available — before you open a project, with a project open, and with a page on the canvas — and what the assistant can do grows with each of those states.\n\n## What it can do\n\n**With nothing open** — the assistant bootstraps. Describe a site and it creates a project for you: name, folders, starter pages, and a design quickstart (colors and fonts) derived from your description, then keeps building inside it. It will ask you where to put the project before creating anything — tell it a folder (or, on the cloud, a GitHub account or organization). The **[New Project](/docs/studio/projects/create)** dialog's **Agent** tab is the same idea as a form: describe the site you want, and the assistant builds it in the editor while you watch.\n\n**With a project open** — the assistant works across files. It can list and read any project file, find files by name, create new pages and components, and rewrite files whole. Anything it writes as a Jx document is validated before it touches disk. It can also open a page on the canvas to continue there.\n\n**With a page on the canvas** — the assistant edits that page live: text, styles, element properties, adding, moving, and removing elements, and the page's state entries. This is the most precise mode, and the one you can watch and undo — see **[Document assistant](/docs/studio/ai/document-assistant)**.\n\nIn every state it also answers questions — \"what does this page's state do?\", \"which component renders the header?\" — by reading the same files you see.\n\nEach request gets five working rounds — five turns of thinking and calling tools before it must reply. If a big request runs out, the assistant stops and lists what it finished and what went wrong; send another message to continue, or split the request into smaller ones.\n\n## Connect a provider\n\nThe first time you open the sidebar (and any time no key is stored), the chat is replaced by the **AI provider key** form:\n\n1. Paste an API key. Any OpenAI-compatible key works — OpenAI itself, a compatible hosted provider, or a local model server.\n2. Pick a **Model**. Click **Fetch models** to list what your key can use, or type a model ID directly.\n3. Optionally set an **Endpoint** — leave it empty for OpenAI, or point it at a compatible server such as a local LLM (for example `http://localhost:11434/v1`).\n4. Click **Save**.\n\nTo change any of this later, click the gear button (**API key & endpoint**) at the bottom of the sidebar. You can also switch models per conversation with the model picker next to the message box.\n\n:::doc-note\nThe key, endpoint, and model choice are stored locally on your machine, per browser or app install. If the Studio backend you're running already has a provider key configured (for example a dev server started with an `OPENAI_API_KEY` environment variable), the assistant unlocks without asking for one.\n:::\n\n## What leaves your machine\n\nNothing is sent anywhere until you send a message. When you do, Studio sends your configured provider — and only your configured provider — what the assistant needs to answer:\n\n- your message and the rest of the conversation,\n- any context you attached (the current page reference, the selected element),\n- the full contents of the page open on the canvas,\n- a summary of the open project: its name and settings, component names, and file paths,\n- whatever files the assistant reads while working on your request.\n\nRequests travel through Studio's own local proxy straight to the endpoint you configured; your key rides along only on those requests. If you point the endpoint at a model running on your own machine, nothing leaves it at all.\n\n## Learn the two surfaces\n\n- **[The AI sidebar](/docs/studio/ai/chat)** — the chat itself: attaching context, watching edits land, chat history, and reviewing or undoing what the assistant changed.\n- **[Document assistant](/docs/studio/ai/document-assistant)** — how the assistant works when a page is open on the canvas, and when to use that instead of project-wide edits.\n\n## Next\n\n- Create a project for the assistant to work in: **[New Project](/docs/studio/projects/create)**\n- The state entries it can add for you are explained in the **[State panel](/docs/studio/logic/state)**\n- Working the same project from outside Studio — with a coding agent, or in CI: **[Working with agents](/docs/framework/agents)**\n",
    "section": "Studio",
    "slug": "studio/ai",
    "title": "AI assistant",
    "url": "https://jxsuite.com/docs/studio/ai/"
  },
  {
    "description": "The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.",
    "markdown": "\n# The AI sidebar\n\nThe AI sidebar is the assistant's home: a chat column on the right edge of the workspace that stays put while you work. It survives tab switches — your draft message, scroll position, and conversation are all still there when you come back — and it works in every state of Studio, from the welcome screen to a page mid-edit.\n\n![A conversation in the sidebar: a user message with context chips, an assistant reply with a tool chip, and the composer below](../../images/ai-sidebar-chat.png)\n\n## Open it\n\nClick the **Toggle Assistant** chat-bubble button at the right end of the toolbar. Click it again to collapse the sidebar; drag its inner edge to resize it. Both the width and the open/closed state are remembered across sessions.\n\nIf no AI provider is set up yet, the sidebar shows the key form instead of a chat — see **[Connect a provider](/docs/studio/ai#connect-a-provider)**.\n\n## Send a message\n\nType in the message box at the bottom and press :kbd[Enter] to send — :kbd[Shift+Enter] inserts a newline. The box grows as you type, and the send button enables once there's something to send.\n\nThe row under the message box holds the composer's controls:\n\n- **Attach context** (paperclip) — pin the current page or the selected element to your message (below).\n- The **model picker** — switch models mid-conversation; the list comes from your provider.\n- **API key & endpoint** (gear) — reopen the provider form.\n- **Send** — becomes **Stop** while the assistant is replying; click it to halt the reply and any further actions.\n\n## Attach context\n\nThe paperclip menu offers two attachments, each shown as a removable chip above the message box:\n\n- **Current page** — the file open in the active tab.\n- **Selected element** — the element currently selected on the canvas, identified by its tag and a snippet of its text.\n\nAttaching the selected element is the precise way to say \"this one\": \"make _this_ heading smaller\" works reliably when the heading rides along as a chip. One chip of each kind is kept, chips clear after sending, and sent messages display their chips so you can see later what a request pointed at.\n\nEven without attachments the assistant already knows a lot: each message carries the open page's full contents and a summary of the project — its name, settings, component names, and file paths. Attachments are for pointing, not for granting access.\n\n## Watch it work\n\nThe assistant's reply streams in live. When it acts on your project, each action appears as a small labeled chip in the reply — one per edit or file operation — so the reply doubles as a log of what was done. Actions that fail show a warning row explaining why; successful ones stay quietly in their chips.\n\nDocument edits land on the canvas as they happen, so for canvas work you can literally watch the page change. If something goes wrong mid-request — a lost connection, a provider error — the sidebar shows the error with advice on how to recover.\n\n## Review and undo edits\n\nWhat the assistant may change, and how you take it back, follows two rules:\n\n**Edits to a page open on the canvas** are applied to the open editor, not to disk. The page's tab is marked unsaved, exactly as if you had made the edits yourself — review them on the canvas, then save the tab to keep them or close without saving to discard. They also enter the page's normal undo history as **one undo step per request**: press :kbd[⌘Z] (macOS) or :kbd[Ctrl+Z] (Windows/Linux) once to roll back everything the assistant did to that page in its last reply. If one request edited several pages, each page carries its own single step.\n\n**File-level changes** — new pages, new components, whole-file rewrites — are saved straight to disk and are **not undoable** from Studio's history. Two guards keep this safe: Jx documents are validated (and test-rendered) before writing, and the assistant refuses to overwrite a file you have open with unsaved changes. When it writes a file you _do_ have open (with no unsaved edits), the tab refreshes to show the new contents.\n\n:::doc-tip\nFor disk-level changes, source control is the review tool: the **[Source Control](/docs/studio/publish/source-control)** panel shows every file the assistant touched as a pending change you can diff or discard before committing.\n:::\n\n## Chats and history\n\nThe header names the current chat and holds two buttons: the history button (left) opens the **Chats** list, and **+** starts a new chat.\n\n- Chats are titled after your first message and listed newest-first with a timestamp and message count.\n- Click a chat to reopen it; the conversation continues where it left off.\n- Hover a row and click the trash button to delete a chat. Deleting the open one leaves you in a fresh empty chat.\n- When you reopen Studio, your last open chat is restored.\n\nHistory is stored on your machine and kept per project, so conversations never mix between projects. Each project keeps its 20 most recent chats, and each chat keeps its latest 50 messages.\n\n## Next\n\n- How the assistant edits the open page: **[Document assistant](/docs/studio/ai/document-assistant)**\n- What each message shares with your provider: **[AI assistant](/docs/studio/ai#what-leaves-your-machine)**\n- Where saved and unsaved files live: **[Tabs and files](/docs/studio/interface/tabs)**\n",
    "section": "Studio",
    "slug": "studio/ai/chat",
    "title": "The AI sidebar",
    "url": "https://jxsuite.com/docs/studio/ai/chat/"
  },
  {
    "description": "How the assistant edits the page open on the canvas — live, validated changes in one undo step per request — and when to work project-wide instead.",
    "markdown": "\n# Document assistant\n\nThe document assistant is what the **[AI sidebar](/docs/studio/ai/chat)** becomes when a page or component is open on the canvas: the same chat, now scoped to that document. It isn't a separate panel — opening a document simply hands the assistant that page's full structure and a set of precise editing abilities it doesn't have otherwise. This is the mode to use when you want to _see_ the assistant work and be able to take it back.\n\n![The sidebar with an assistant reply carrying edit chips beside the open page on the canvas](../../images/ai-document-assistant.png)\n\n## What it can do on the open page\n\nWith a document active, the assistant edits the page the way you would, one change at a time:\n\n- rewrite any text,\n- restyle elements property by property — the same styles you'd set in the **[style inspector](/docs/studio/design/style-inspector)**,\n- change element properties: tags, classes, attributes, component options,\n- add new elements, move them, and remove them,\n- add or update the page's state entries — the same ones the **[State panel](/docs/studio/logic/state)** shows.\n\nEvery change lands on the canvas the moment it's made, so the page updates in front of you while the reply streams. To aim the assistant at one specific element, select it on the canvas and attach it with the composer's paperclip menu — see **[Attach context](/docs/studio/ai/chat#attach-context)**.\n\n## Every edit is checked\n\nAfter each change, Studio validates the document and test-renders it out of sight. If an edit introduced a problem — an invalid property, something that breaks rendering — the assistant is told exactly what went wrong and fixes it with follow-up edits in the same request. The page may pass through an imperfect intermediate state while it iterates; that's the self-correction loop working, not the final result.\n\nThe rules it's checked against are your project's own — the same generated schemas the **[code editor](/docs/studio/logic/code)** underlines against, so sections contributed by the extensions you've enabled are enforced too. Writes to `project.json` go through the same gate before they reach disk, and a change to your enabled extensions updates the rules for both surfaces immediately.\n\nIf the assistant runs out of working rounds before everything is fixed, it stops and lists what was applied and which problems remain, so nothing fails silently.\n\n## Undo and save\n\nDocument edits follow Studio's normal editing rules:\n\n- They go into the page's undo history as **one step per request** — :kbd[⌘Z] (macOS) or :kbd[Ctrl+Z] (Windows/Linux) rolls back the assistant's whole last reply on that page. If a request switched between pages, each page gets its own single step.\n- They live in the open editor, not on disk: the tab is marked unsaved until you save it, and closing without saving discards them.\n\nThe full review workflow — including the file-level changes that _don't_ work this way — is covered in **[Review and undo edits](/docs/studio/ai/chat#review-and-undo-edits)**.\n\n## Page open or not — which to choose\n\nWhere the assistant works depends only on what's on the canvas, so you steer it by what you open:\n\n- **Open the page first** when you care about watching and undoing. With the page on the canvas, requests like \"tighten the hero spacing\" become live, validated, undoable canvas edits to that page.\n- **Leave the canvas empty (or ignore it)** for project-wide work. Without a document scope the assistant creates and rewrites files directly — faster for \"scaffold an about page and a contact page\", but those writes go straight to disk and aren't undoable from Studio.\n- **Let it switch itself.** The assistant can open a page on the canvas as part of a request; from then on its document edits target that page. It does this when you ask to see a page, or when fine-grained editing suits the task better than rewriting the file.\n\nA simple habit: if you'd want to press undo afterwards, open the page before you ask.\n\n## Next\n\n- The chat itself — composer, history, reviewing edits: **[The AI sidebar](/docs/studio/ai/chat)**\n- What the assistant can do in every state, and provider setup: **[AI assistant](/docs/studio/ai)**\n- The state entries it adds are explained in the **[State panel](/docs/studio/logic/state)**\n",
    "section": "Studio",
    "slug": "studio/ai/document-assistant",
    "title": "Document assistant",
    "url": "https://jxsuite.com/docs/studio/ai/document-assistant/"
  },
  {
    "description": "How a Jx site goes live: Studio commits and pushes your files, then your host builds the site — prebuilt pages, plus a worker if the project has one.",
    "markdown": "\n# Publish\n\nShipping a Jx site is part of the Studio flow — no terminal, no separate deploy tool. The one boundary worth knowing up front: **Studio publishes your code — it doesn't build or deploy the site.** Studio's job ends when your files reach your repository; your host takes it from there.\n\n![Jx Studio commit box — write a message and commit-and-sync straight from the Source Control panel](../images/git-commit.png)\n\n## How it goes live\n\n1. In the **Source Control** panel, you write a message and **Commit and sync** — Studio records the change and pushes it to your repository.\n2. The push wakes your host (or CI), which builds the project into plain HTML, CSS, and a little JavaScript.\n3. The host serves that output from a CDN. Your site is live.\n\nThe **deployment adapter** you picked when [creating the project](/docs/studio/projects/create) — Static, Cloudflare Pages, Node, or Bun, with Cloudflare Workers also on offer in [Project settings](/docs/studio/projects/settings) — tells the build how to package the output for your target. Switching hosts means switching the adapter; your pages, components, and content stay the same.\n\nEvery page is built ahead of time, so what the CDN serves is finished HTML however the site is put together. Pick one of the non-Static adapters and the same build writes a small worker beside those files for your host to run — that is what answers the `/_jx/*` routes behind a database, sign-ins, or server functions. A database or sign-ins make an adapter mandatory: the build stops with an error on **Static**. Server functions still build there, but nothing serves them without an adapter. **[Build output and adapters](/docs/framework/site/deployment)** lists what each adapter emits.\n\nBecause every Jx file is plain JSON or Markdown, each publish is a clean, reviewable set of changes — no binary blobs, no database dumps.\n\n## The two surfaces\n\n- **[Source control](/docs/studio/publish/source-control)** — the built-in git client: review and stage changes, commit and sync, branches, pulling, and history.\n- **[GitHub](/docs/studio/publish/github)** — connect your GitHub account and publish a brand-new project as a repository in one flow.\n\n## Next\n\n- Understand the build and routing in **[Site architecture](/docs/framework/site)**\n",
    "section": "Studio",
    "slug": "studio/publish",
    "title": "Publish",
    "url": "https://jxsuite.com/docs/studio/publish/"
  },
  {
    "description": "The Source Control panel in Jx Studio: review and stage changes, commit and sync, switch branches, pull safely, and browse your project's history.",
    "markdown": "\n# Source control\n\nSource Control is Studio's built-in git client — a sidebar panel that records your work as commits and keeps your copy of the project in sync with its repository. Open it by clicking **Source Control** (the branch icon) in the activity bar; a badge on the icon shows how many files have changed.\n\n![The Source Control panel with sync status, branch selector, commit box, and changed files](../../images/git-panel.png)\n\nIf the project isn't under version control yet, the panel offers **Initialize Repository** to start tracking it locally, and **Publish to GitHub** to go straight to a hosted repository — see **[GitHub](/docs/studio/publish/github)**.\n\n## Review your changes\n\nThe **Local Changes** tab lists every changed file, grouped by the part of the project it belongs to. Each row shows the file's name and a status badge — **M** for modified, **A** for added, **U** for a brand-new untracked file.\n\n- Click a changed file to open a diff in the canvas — what changed since your last commit.\n- Click **+** on a row to stage it (mark it for the next commit), or the header's stage-all button to stage everything. Staged files move to a **Staged Changes** section, where **−** unstages them.\n- Click the undo icon on a row to discard its changes. Studio asks for confirmation first.\n\n:::doc-warning\n**Discard** permanently throws away a file's changes since the last commit — there is no undo beyond the confirmation dialog.\n:::\n\n## Commit and sync\n\n1. Type a summary of your work in the message box.\n2. Click **Commit and sync** — Studio records the commit and pushes it to your repository in one step. That push is what triggers your host's build, as described in **[Publish](/docs/studio/publish)**.\n\nTo record a commit without pushing, open the dropdown beside the button and choose **Commit (don't sync)**, or press :kbd[Ctrl+Enter] in the message box. If nothing is staged, the commit takes all changed files.\n\n![Jx Studio commit box — write a message and commit-and-sync straight from the Source Control panel](../../images/git-commit.png)\n\n## Stay in sync\n\nThe bar at the top of the panel shows where you stand against the repository — **Up to date**, or how many commits you are ahead or behind — with a last-updated time. Studio refreshes this automatically while the panel is open. Three buttons act on it:\n\n- **Fetch** — check the repository for news without changing your files.\n- **Pull** — bring teammates' commits into your copy.\n- **Push** — send your local commits up.\n\nStudio also pulls automatically when you open a project that has a remote, so a session starts from the current state. If a pull can't merge cleanly, Studio reports the error and changes nothing — with one exception: conflicts caused purely by Studio's own automated package updates are resolved for you (Studio discards its own machine-generated edits, pulls, and re-applies them; if _you_ edited those files it asks before discarding anything).\n\nA project with no remote yet shows **Local only (no remote)** here, with a **Publish to GitHub** shortcut.\n\n## Branches\n\nThe **Active branch** row shows which branch you're on. Use its picker to switch to another branch, or choose **+ New branch…** — Studio opens a **New Branch** dialog; type a name and click **Create**. Branches let you try a redesign on the side and only merge it when it's ready.\n\n## History\n\nThe **History** tab lists your project's recent commits — message, author, and when — so you can see how the site got to where it is.\n\n## Next\n\n- **[GitHub](/docs/studio/publish/github)** — put a local project on GitHub without leaving Studio\n- **[Publish](/docs/studio/publish)** — how a push becomes a live site\n",
    "section": "Studio",
    "slug": "studio/publish/source-control",
    "title": "Source control",
    "url": "https://jxsuite.com/docs/studio/publish/source-control/"
  },
  {
    "description": "Connect Jx Studio to GitHub — authorize with a one-time code, then publish your project as a new repository and push it in one flow.",
    "markdown": "\n# GitHub\n\nStudio can take a project that exists only on your machine and put it on GitHub — account sign-in, repository creation, and the first push — without leaving the app. Once it's there, every **Commit and sync** from the [Source Control panel](/docs/studio/publish/source-control) keeps the repository current, and your host can build the site from it.\n\n## Authorize Studio\n\nThe first time you use a GitHub feature, Studio asks you to sign in with a one-time code:\n\n1. A **Sign in to GitHub** dialog appears showing a short code.\n2. Click the link in the dialog — it opens GitHub's device-authorization page in your browser.\n3. Enter the code there and approve the request.\n4. Back in Studio, the dialog closes on its own once GitHub confirms.\n\nStudio remembers the authorization on this device, so you won't be asked again on your next publish.\n\n## Publish to GitHub\n\n**Publish to GitHub** lives in the Source Control panel — it's offered when your project isn't a repository yet, and again in the sync bar while the project has no remote.\n\n1. Click **Publish to GitHub**. Sign in first if prompted.\n2. In the dialog, confirm the **Repository name** (prefilled with your project's name) and add an optional **Description**.\n3. Choose the visibility. **Private repository** is on by default — turn it off to make the code public.\n4. Click **Create Repository**.\n\nStudio creates the repository on GitHub, connects your project to it, and pushes everything up. The status bar reports each step, and when it finishes, the Source Control panel switches from **Local only (no remote)** to live sync status — you're one **Commit and sync** away from publishing changes from now on.\n\n:::doc-note\nPublishing uploads your project's files to GitHub. With **Private repository** on, only you (and people you invite on GitHub) can see them; public repositories are visible to anyone.\n:::\n\n:::doc-tip\nSome Studio platforms connect to GitHub through the **Jx Suite GitHub App** instead — when that applies, the [Welcome screen](/docs/studio/interface/welcome-screen) offers **Install the Jx Suite GitHub App** and an **Add Existing Repository…** picker for repositories your account can already reach. The picker's footer links to the App's repository-access settings for each account, so you can widen what Studio sees at any time — see [Repository access](/docs/studio/interface/welcome-screen#repository-access).\n:::\n\n## Next\n\n- **[Source control](/docs/studio/publish/source-control)** — the day-to-day commit and sync flow\n- **[Publish](/docs/studio/publish)** — how a push becomes a live site\n",
    "section": "Studio",
    "slug": "studio/publish/github",
    "title": "GitHub",
    "url": "https://jxsuite.com/docs/studio/publish/github/"
  },
  {
    "description": "Connect Jx Studio to Cloudflare Pages: sign in or paste a token, create and connect a Pages project, and watch deployments ride every commit.",
    "markdown": "\n# Publish to Cloudflare Pages\n\nThe Publish panel connects your project to Cloudflare Pages — a free host that serves your prebuilt pages from a CDN and, if your project has a database or sign-ins, runs the small worker Jx emits for their `/_jx/*` routes — so that every commit you sync builds and publishes automatically. Open it with the **Publish** button in the toolbar.\n\n![The Publish modal showing the connected state with the latest deployment status](../../images/publish-panel.png)\n\nBefore you start, the project needs to live on GitHub — if it doesn't yet, **[GitHub](/docs/studio/publish/github)** walks you through publishing it from Studio.\n\n## Connect your Cloudflare account\n\nThe first time you open the panel, it asks for a Cloudflare connection. What you see depends on your Studio platform:\n\n- **Connect Cloudflare** — click it and sign in to Cloudflare in the window that opens. Done.\n- **API token form** — some platforms ask you to paste a Cloudflare API token instead (create one in the Cloudflare dashboard with the permissions the panel names: Account Settings Read, Pages Read/Write), then click **Verify & Connect**. The token stays on your machine.\n\nIf the panel instead says it can't reach the Cloudflare API on this platform, you don't need it at all — set up Pages once in Cloudflare's own dashboard and publishing still works the same way: commit and sync, your host builds.\n\n## Create and connect a Pages project\n\nOnce connected, the panel offers to create a Pages project tied to your repository:\n\n1. **Account** — pick your Cloudflare account.\n2. **Pages project name** — pre-filled from your project's name; this becomes part of your free site address.\n3. **GitHub owner** and **GitHub repository** — where the project lives.\n4. **Production branch** — the branch that publishes to your live site, normally `main`.\n5. Click **Create & Connect**.\n\nStudio creates the Pages project (or reuses one with that name if it already exists) and configures it to build and publish your site on every push. If Cloudflare reports it can't see the repository, the error includes a link to install the **Cloudflare Pages** GitHub App — install it on the repository and try again.\n\n## Watch deployments\n\nAfter connecting, the panel becomes a status view:\n\n- The connected Pages project's name, with a link to your live site address.\n- The latest deployment's stage and status — for example _deploy: success_ — with a **preview** link to that exact build.\n- **Refresh** re-checks; **Disconnect** removes the connection (your Pages project and site stay up — only the link from Studio is removed).\n\nThere is no publish button, because publishing is automatic: every **Commit and sync** in **[Source control](/docs/studio/publish/source-control)** triggers a fresh build and deployment. Right after connecting, the panel shows _No deployments yet_ — your next commit starts the first one.\n\n## Sites with a database or sign-in\n\nPages serves both halves of a Jx site, but a project with **[data tables](/docs/studio/data)** or accounts needs three things arranged once:\n\n1. **Set the adapter.** In _Settings > General_, set **Platform Adapter** to **Cloudflare Pages** — or **Cloudflare Workers** if you deploy the site as a Worker instead. Connecting this panel doesn't change it for you, and the build stops with an error on **Static** as soon as the project declares data tables.\n2. **Push the schema to the real database, from a terminal.** While you develop, a [D1 connection](/docs/studio/data/connections) is stood in by a local SQLite file — and Studio's **Push Schema** button goes through that same local backend, so it creates your tables in `.jx/data/<connection>.sqlite` and never touches D1 or `wrangler.jsonc`. `jx db push` is the path that talks to the connection as declared: it applies the same additive plan to D1 itself, and writes D1's binding into your project's `wrangler.jsonc` on the way through. Reaching D1 from outside a deployed worker goes over Cloudflare's API, which needs three things together — the connection's **database ID**, a `CLOUDFLARE_API_TOKEN`, and an account ID (the connection's own **account ID**, or `CLOUDFLARE_ACCOUNT_ID`). Miss one and the push reports the connection as unreachable. Accounts need a second pass: the auth extension's own tables aren't part of the CLI push — **[Auth and secrets](/docs/studio/data/auth-and-secrets)** covers where they come from.\n3. **Set the secret values on Cloudflare.** `project.json` records only the _names_ of environment variables — the session signing secret, a database URL, OAuth credentials. Give each name a value on the Pages project, with `wrangler pages secret put <NAME>` or the environment settings in Cloudflare's dashboard; locally the same names are read from `.dev.vars`. **[Auth and secrets](/docs/studio/data/auth-and-secrets)** covers the whole arrangement.\n\nYour pages stay prerendered and CDN-served either way — only the `/_jx/*` routes reach the worker. The two adapters arrange that differently: **Cloudflare Pages** ships a `_routes.json` alongside the worker that tells Cloudflare to wake it for `/_jx/*` and nothing else, while a **Cloudflare Workers** deploy puts the worker in front of every request and hands anything that isn't one of its routes straight to the static assets.\n\n:::doc-note\nStudio records the connection under `build.deploy` in `project.json` — provider, account, project name, and live address — so it travels with the repository, and any copy of Studio that opens the project knows publishing is already set up. Cloudflare builds with `bunx jx build` and serves the `dist/` output.\n:::\n\n## Next\n\n- **[Source control](/docs/studio/publish/source-control)** — the commit-and-sync flow that triggers each deployment\n- **[Other hosts](/docs/studio/publish/other-hosts)** — the same site on Netlify, GitHub Pages, or anywhere else\n",
    "section": "Studio",
    "slug": "studio/publish/cloudflare",
    "title": "Publish to Cloudflare Pages",
    "url": "https://jxsuite.com/docs/studio/publish/cloudflare/"
  },
  {
    "description": "Publish a Jx site anywhere: pick an adapter, have your host run the build, and serve the output — with Netlify and GitHub Pages sketches.",
    "markdown": "\n# Other hosts\n\nA Jx site doesn't need a special host. The build turns your project into an ordinary folder of web files, so any host that can run one build command and serve a folder can serve your site — and a host that also runs a small server can carry a site with a database or sign-ins. Studio's built-in flow covers **[Cloudflare Pages](/docs/studio/publish/cloudflare)**; for everything else, the recipe below is the whole story.\n\n## The recipe\n\n1. **Pick the deployment adapter.** In _Settings > General_, set **Platform Adapter** for your target. **Static** is the choice for a site made only of pages and content — it works on any host that serves files. If the project has a database, sign-ins, or `timing: \"server\"` functions, pick **Node**, **Bun**, **Cloudflare Workers**, or **Cloudflare Pages** instead: those also package the worker that serves your `/_jx/*` routes. A database or sign-ins make that mandatory — the build stops with an error on **Static**. Server functions don't stop the build, but on **Static** nothing ends up serving them, so they need an adapter just the same. See **[Project settings](/docs/studio/projects/settings)**.\n2. **Put the project on GitHub** (or another repository host) so your host can see it — **[GitHub](/docs/studio/publish/github)** does this from inside Studio.\n3. **Tell the host two things**: the build command is `bunx jx build`, and the folder to publish is `dist`.\n\nThat's it. From then on, every **Commit and sync** in **[Source control](/docs/studio/publish/source-control)** pushes your changes, the host runs the build, and the fresh site goes live — the flow described in **[Publish](/docs/studio/publish)**.\n\n## Netlify\n\nCreate a new site in Netlify and connect it to your repository. In the build settings, set:\n\n- **Build command**: `bunx jx build`\n- **Publish directory**: `dist`\n\nNetlify then builds and publishes on every push, with a free site address until you attach your own domain.\n\n## GitHub Pages\n\nGitHub Pages serves files but doesn't take a build command directly — the build runs in a GitHub Actions workflow instead. In your repository, set Pages to deploy from GitHub Actions, and add a workflow that runs `bunx jx build` on every push and publishes the `dist` folder with GitHub's Pages deploy action. GitHub's own Pages documentation covers the workflow setup; the only Jx-specific parts are the command and the folder.\n\n## Anywhere else\n\nThe same two values — build with `bunx jx build`, serve `dist` — fit Vercel, Render, a plain web server, or your own CI. If a host can't run the build, you can even run `bunx jx build` yourself and upload the `dist` folder by hand: for a site of pages and content, that folder _is_ the site. A project with a database, sign-ins, or server functions has one of the server-capable adapters set, so its `dist/` carries a worker beside the pages — and that needs a host which actually runs it.\n\n:::doc-note\nWith an adapter set, the build writes host-specific files alongside the static output in `dist/` — for Node, Bun, and Cloudflare, the worker that serves `/_jx/*`. The full list of adapters and what each one emits is in [Build output and adapters](/docs/framework/site/deployment).\n:::\n\n## Next\n\n- **[Publish to Cloudflare Pages](/docs/studio/publish/cloudflare)** — the host with a built-in Studio flow\n- **[GitHub](/docs/studio/publish/github)** — get the project into a repository first\n",
    "section": "Studio",
    "slug": "studio/publish/other-hosts",
    "title": "Other hosts",
    "url": "https://jxsuite.com/docs/studio/publish/other-hosts/"
  },
  {
    "description": "Co-edit Jx files live: presence chips, shared cursors on canvas and in code, merged edits and shared saves — and how Studio falls back to solo editing.",
    "markdown": "\n# Real-time collaboration\n\nWhen two people open the same file through the same Studio backend, the tab becomes a shared session: everyone sees everyone's edits as they happen, on the canvas and in the code. There's nothing to turn on — if your setup supports it (see below), co-editing starts by itself the moment a second person opens the file, and a file opened alone behaves exactly as always.\n\n![A co-edited tab with the Live status pill, two presence chips, and a peer's colored selection box on the canvas](../../images/collab-presence.png)\n\n## What you see\n\n- **A status pill** in the toolbar — **Live** while the session is connected. It replaces the usual unsaved-changes dot for this tab.\n- **Presence chips** — one colored circle per collaborator, showing their avatar or initial. Hover one to see who it is and which file they're in; peers elsewhere in the project show up too, labeled with the file they're browsing.\n- **Selections on the canvas** — each peer's selected element is outlined in their color, labeled with their name, and follows them live.\n- **Cursors in Code view** — in the **[Code](/docs/studio/logic/code)** mode the shared text carries every writer's caret and selection in their color, with their name on the caret.\n\n## How co-editing behaves\n\n- **Edits merge.** Everyone edits the same live document — changes apply as they arrive, and simultaneous edits to different parts of a file both land. No locking, no taking turns.\n- **Undo is yours alone.** :kbd[⌘Z] / :kbd[Ctrl+Z] steps back through _your_ edits only — you can't undo what a teammate just did.\n- **Forms sync too.** Page metadata and frontmatter fields co-edit the same way the canvas does.\n- **Code view takes precedence.** While someone is editing the file as text, the text is the truth: structural editing pauses for everyone else (\"Source editing in progress — structural edits are paused\"), and the canvas previews the text edits live. When the last text editor leaves Code view, normal editing resumes.\n- **Read-only guests follow along.** On backends that grant view-only access, those visitors see everything — content, cursors, presence — but their edits are not accepted.\n\n## Syncing is not saving\n\nYour edits reach your collaborators instantly, but the file on disk still changes only when someone saves — the explicit **Save** is unchanged. What _is_ shared is the unsaved state itself: the moment anyone edits, the file counts as unsaved for the whole session, and one person saving saves the shared result for everyone. Committing in **[Source control](/docs/studio/publish/source-control)** saves open co-edited files first, so a commit always captures what the session currently sees.\n\n:::doc-warning\nOn a shared dev server, unsaved co-edits live only in the server's memory. If everyone closes the file without saving, those edits are discarded shortly after — save before you all walk away.\n:::\n\n## Which setups support it\n\n- **A shared dev server** — the built-in case. Everyone who opens the same dev-server URL (see **[The dev server](/docs/framework/build/dev-server)**) co-edits; even two browser windows on your own machine will. The dev server has no user accounts, so collaborators appear with generic names (`local-1`, `local-2`, …) and everyone can write.\n- **A hosted Studio backend** — cloud backends that offer a collaboration endpoint get the same experience, with real identities (name and avatar) and per-person write or read-only permission supplied by the platform.\n- **The [desktop app](/docs/studio/desktop)** — always solo: it edits your local files directly and has no collaboration endpoint.\n\n## Falling back to solo\n\nCollaboration degrades, never blocks:\n\n- A backend without the endpoint simply gives you ordinary solo editing — no errors, no pill.\n- If a session can't sync within a few seconds of opening, the tab proceeds solo.\n- If the connection drops, the pill reads **Offline — changes sync on reconnect**: keep editing, and your changes merge when the connection returns.\n- If the file is replaced underneath the session — a git pull or discard, an outside edit — the session resets and rejoins on the new content automatically.\n\n## Next\n\n- **[Source control](/docs/studio/publish/source-control)** — turn the shared result into a commit\n- **[Code](/docs/studio/logic/code)** — the text view that co-editing shares character by character\n",
    "section": "Studio",
    "slug": "studio/publish/collaboration",
    "title": "Real-time collaboration",
    "url": "https://jxsuite.com/docs/studio/publish/collaboration/"
  },
  {
    "description": "Jx Studio as a native app — open projects with native dialogs, one window per project, automatic background updates, and no dev server to run.",
    "markdown": "\n# The desktop app\n\nThe desktop app is Jx Studio as a native application: the same Studio documented everywhere else in this section, wrapped in its own window with everything it needs bundled inside. You install it, open it, and build — no terminal, no dev server, no browser tab. It edits the plain files in your project folders directly.\n\n![The Studio workspace — the same interface in the desktop app and the browser](../images/hero.png)\n\n## Install and update\n\nDownload the installer for macOS, Windows, or Linux from the **[install page](/docs/start/install)** — it covers each platform's package and what to expect on first launch.\n\nOnce installed, updates take care of themselves. The app checks for a new release shortly after launch and every few hours after that, downloads it in the background, and then shows a small notice — **Version x.y.z is ready** — with a **Restart to update** button. Update whenever suits you; nothing is applied until you restart.\n\nTo see where you stand, click the info button at the bottom of the activity bar. The About dialog shows the app version, its release channel, and the current update status.\n\n## Open a project\n\nA fresh window greets you with the **[welcome screen](/docs/studio/interface/welcome-screen)**: create a **[new project](/docs/studio/projects/create)**, open an existing one, or pick from your recent projects — the recent list is shared across all windows.\n\nTo open an existing project:\n\n1. Choose _File > Open Project…_ or press :kbd[⌘O] (macOS) / :kbd[Ctrl+O] (Windows/Linux) — or click **Open Project...** on the welcome screen.\n2. A native file dialog opens. Select the project's `project.json` file — the file that marks a folder as a Jx project.\n3. The project opens in its own window, with the folder around `project.json` as the project root.\n\nYou can also open a project straight from your file manager: if your system associates it with Jx Studio, double-clicking a `project.json` opens that project.\n\n:::doc-note\n`project.json` is the project's settings file — Studio creates it for every new project. What's inside is described in **[Project settings](/docs/studio/projects/settings)**.\n:::\n\n## One window per project\n\nEach window holds one project. Opening a second project opens a second window rather than replacing what you have, and the window's title tells you which is which. Opening a project that's already open doesn't duplicate it — the existing window comes to the front.\n\n_File > New Window_ (:kbd[⇧⌘N] / :kbd[Ctrl+Shift+N]) opens a fresh welcome window when you want to start something else.\n\n## How it differs from Studio in the browser\n\nStudio itself is identical in both — every page in this documentation applies to each. The differences are around the edges:\n\n- **Nothing to run.** In the browser, Studio is served by a local dev server you start from a terminal (see **[The dev server](/docs/framework/build/dev-server)**). The desktop app carries its own backend — launch it like any other application.\n- **Native dialogs.** Opening projects and picking folders use your operating system's file dialogs instead of the browser's folder picker.\n- **Windows, menus, and file associations.** One window per project, a real _File_ menu, and `project.json` opening from the file manager.\n- **Built-in updates.** The app updates itself in the background; a dev-server setup updates with your package manager.\n\nThe AI assistant, publishing, and everything on the canvas behave the same in both.\n\n## Platform notes\n\nInstallers are provided for macOS (Apple Silicon and Intel), Windows (x64), and Linux (x64) — see the **[install page](/docs/start/install)** for downloads. On NixOS the app is packaged differently (built with `nix build`, running Studio in a Chromium app window), but presents the same Studio.\n\n## Next\n\n- Get it installed: **[Install Jx Studio](/docs/start/install)**\n- Build something: **[Your first project](/docs/start/first-project)**\n- Find your way around: **[Studio tour](/docs/start/studio-tour)**\n",
    "section": "Studio",
    "slug": "studio/desktop",
    "title": "The desktop app",
    "url": "https://jxsuite.com/docs/studio/desktop/"
  },
  {
    "description": "The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.",
    "markdown": "\n# Framework\n\n> This is the conceptual foundation beneath [Jx Studio](/studio). You never have to read it to build with Studio — but if you want to know exactly what the format is, start here.\n\nJx is a schema and runtime for building reactive web applications using plain JSON. A Jx application is a tree of JSON objects whose structure mirrors the DOM API, whose reactivity is powered by `@vue/reactivity`, and whose behavior is declared in `state` entries.\n\n## Core Premise\n\n**Structure and state are data.** The shape of each `state` entry determines its type and behavior — no additional flags required in the common case.\n\nA Jx component is a single `.json` file that can be fully self-describing:\n\n```json\n{\n  \"$id\": \"Counter\",\n  \"state\": {\n    \"count\": 0,\n    \"increment\": {\n      \"$prototype\": \"Function\",\n      \"body\": \"state.count++\"\n    }\n  },\n  \"tagName\": \"my-counter\",\n  \"children\": [\n    { \"tagName\": \"span\", \"textContent\": \"${state.count}\" },\n    { \"tagName\": \"button\", \"textContent\": \"+\", \"onclick\": { \"$ref\": \"#/state/increment\" } }\n  ]\n}\n```\n\n## Design Principles\n\n1. **DOM-First Design** — Property names mirror standard DOM element properties. `tagName`, `className`, `textContent`, `hidden`, `tabIndex` all map directly to their DOM equivalents.\n\n2. **Rule of Least Power** — Declarative JSON over imperative JavaScript wherever possible. `$ref` bindings over template expressions. Template expressions over handler functions. Handler functions only when logic cannot be expressed otherwise.\n\n3. **JSON as the Authoritative Format** — Documents are valid JSON. Fully serializable. No `this` ambiguity. Natively understood by visual builders, IDEs, validators, and bundlers.\n\n4. **Explicit Over Implicit** — Signal scope does not leak across component boundaries. Every dependency must be explicitly declared as a `$prop`.\n\n5. **Standards Alignment** — Where a web platform standard exists, Jx follows it: JSON Schema 2020-12, JSON Pointer (RFC 6901), Web Components v1, CSSOM camelCase.\n\n## Document Format\n\nEvery Jx document has these top-level fields:\n\n| Field      | Required    | Description                                          |\n| ---------- | ----------- | ---------------------------------------------------- |\n| `$schema`  | Recommended | URI identifying the Jx dialect version               |\n| `$id`      | Recommended | Component identifier                                 |\n| `$defs`    | Optional    | Pure JSON Schema type definitions (tooling only)     |\n| `state`    | Optional    | Reactive state: signals, computed values, functions  |\n| `tagName`  | Required    | HTML tag name for the root element                   |\n| `children` | Optional    | Array of child element definitions and/or text nodes |\n\n## Reserved Keywords\n\n| Keyword      | Purpose                                                |\n| ------------ | ------------------------------------------------------ |\n| `$schema`    | Dialect identifier                                     |\n| `$id`        | Component identifier                                   |\n| `$defs`      | JSON Schema type definitions                           |\n| `$ref`       | Reference pointer (JSON Pointer, RFC 6901)             |\n| `$props`     | Explicit prop passing at component boundary            |\n| `$prototype` | Constructor name — Web API, `\"Function\"`, or class     |\n| `$src`       | External module specifier                              |\n| `$switch`    | Dynamic component switching                            |\n| `$map`       | Iteration context namespace                            |\n| `$media`     | Named media breakpoint declarations                    |\n| `$elements`  | Custom element dependency declarations                 |\n| `timing`     | Execution timing: `\"compiler\"`, `\"server\"`, `\"client\"` |\n\nFor the complete specification, see the [full spec document](https://github.com/jxsuite/jx/blob/main/specs/spec.md).\n",
    "section": "Framework",
    "slug": "framework",
    "title": "Framework",
    "url": "https://jxsuite.com/docs/framework/"
  },
  {
    "description": "The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.",
    "markdown": "\n# Documents\n\n> **Studio writes this format for you.** Every page and component you build in Studio is saved as one of these files — open [Code mode](/docs/studio/logic/code) to see the raw source of whatever document you have open.\n\nA Jx document is a single `.json` file that describes one piece of user interface: its structure, its styling, the state it holds, and the behavior attached to it. The tree of JSON objects mirrors the DOM — the browser's own object model — so property names like `tagName` and `textContent` mean exactly what they mean to a browser. Simple documents are fully self-describing: no template language, no companion script file.\n\nThe smallest complete document:\n\n```json\n{\n  \"$schema\": \"https://jxsuite.com/schema/v1\",\n  \"$id\": \"Hello\",\n  \"state\": { \"name\": \"World\" },\n  \"tagName\": \"main\",\n  \"children\": [{ \"tagName\": \"h1\", \"textContent\": \"Hello, ${state.name}!\" }]\n}\n```\n\n## Root fields\n\nEvery document is a JSON object with up to six top-level fields:\n\n| Field      | Required    | Description                                                                                                                             |\n| ---------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |\n| `$schema`  | Recommended | Schema the file is checked against — the dialect URI, or the project's generated bundle (below)                                         |\n| `$id`      | Recommended | Component identifier, used by tooling                                                                                                   |\n| `$defs`    | Optional    | Pure JSON Schema type definitions — tooling only, never rendered                                                                        |\n| `state`    | Optional    | Reactive state: values, computed entries, functions, and data sources — see [State](/docs/framework/concepts/state)                     |\n| `tagName`  | Required    | HTML tag name for the root element                                                                                                      |\n| `children` | Optional    | Array of child [elements](/docs/framework/concepts/elements), text nodes, and [repeaters](/docs/framework/concepts/lists), mixed freely |\n\nOnly `tagName` is required. A document can also carry `$media` (named breakpoints, see [Styling](/docs/framework/concepts/styling)) and `$elements` (custom-element dependencies, see [Components](/docs/framework/concepts/components)).\n\n:::doc-note\n`https://jxsuite.com/schema/v1` names the dialect, which is what you want for a document that travels on its own. Inside a project, point `$schema` at the `document.schema.json` that [`jx schema`](/docs/framework/build/cli) writes into the project root instead. That bundle is the core schema _plus_ every enabled extension's contributions, so your editor's autocomplete and `jx validate` are reading the same rules. See [Schema composition](/docs/extending/extensions/schema-composition).\n\nThe pointer is resolved relative to the file that carries it, so it needs one `../` per folder level: `\"../document.schema.json\"` from `pages/index.json`, `\"../../document.schema.json\"` from `pages/blog/post.json`. Too few and it points at a file that does not exist, which every validator reports as an unresolvable schema rather than as a document error.\n:::\n\n## `$defs` — type definitions\n\n`$defs` holds pure JSON Schema 2020-12 type definitions. They exist for tooling — validation, autocomplete, Studio's input controls — and produce nothing at runtime:\n\n```json\n{\n  \"$defs\": {\n    \"Count\": { \"type\": \"integer\", \"minimum\": 0, \"maximum\": 100 },\n    \"Status\": {\n      \"type\": \"string\",\n      \"enum\": [\"idle\", \"loading\", \"success\", \"error\"]\n    }\n  }\n}\n```\n\nState entries reference these types with `$ref` (`{ \"$ref\": \"#/$defs/Count\" }`). `$defs` entries never contain `default`, `$prototype`, or template strings — runtime values belong in `state`.\n\n## Component or page fragment\n\nWhat a document _is_ follows from its root `tagName` and where the file lives:\n\n- A root `tagName` **containing a hyphen** (`\"my-counter\"`, `\"user-card\"`) defines a **custom element** — a reusable [component](/docs/framework/concepts/components) that other documents instantiate by reference.\n- A root `tagName` that is a **plain HTML tag** (`\"main\"`, `\"section\"`) is a **page fragment** — a tree that renders in place, either as a route in a site's `pages/` directory (see [Routing](/docs/framework/site/routing)) or inlined into another document via `$ref` (see [References](/docs/framework/concepts/references)).\n\nEither kind may declare `state`; the difference is that a custom element gets its own isolated scope and a `$props` boundary (see [Props and scope](/docs/framework/concepts/props-and-scope)).\n\n## Why plain JSON\n\nJx documents are valid JSON — not JavaScript object literals, not JSX, not a template DSL. That distinction is load-bearing:\n\n- JSON serializes and deserializes without executing code.\n- There is no `this` ambiguity — self-references are explicit `$ref` pointers.\n- Visual builders, IDEs, validators, and bundlers understand it natively.\n- JSON Schema tooling (validation, autocomplete, LSP) applies directly.\n\nThe format also follows the **rule of least power**: prefer a `$ref` binding over a template expression, a template expression over a [declarative expression](/docs/framework/concepts/expressions), and a handler [function](/docs/framework/concepts/functions) only when logic cannot be expressed otherwise.\n\n## How it works\n\nJx is a JSON Schema dialect: any 2020-12-compatible validator can check a document against the Jx meta-schema identified by `$schema`. In development, the runtime loads the `.json` file, builds a reactive scope from `state`, and renders the tree with real DOM calls. In production, the [build](/docs/framework/build) compiles the JSON away — emitting plain JavaScript classes, static HTML, and extracted CSS.\n\n## Rules\n\n- A document must be valid JSON — no comments, no trailing commas, no inline code (behavior lives in `state` entries as strings or structured data).\n- `tagName` is the only required root field; `$schema` and `$id` are recommended.\n- `$defs` is tooling-only: no signals, no functions, no runtime artifacts.\n- Jx reserves the keywords `$prototype`, `$props`, `$switch`, `$map`, `$src`, `$export`, `timing`, `default`, `body`, `arguments`, and `name` on top of the standard JSON Schema vocabulary.\n- One document per file; a document nests other documents only by `$ref`, never by pasting their JSON inline.\n\n## Related\n\n- [Components](/docs/framework/concepts/components) — custom elements and the component model\n- [Elements](/docs/framework/concepts/elements) — the objects inside `children`\n- [State](/docs/framework/concepts/state) — the four shapes of a `state` entry\n- [Routing](/docs/framework/site/routing) — how documents in `pages/` become URLs\n- [Code editing](/docs/studio/logic/code) — the Studio surface for raw document source\n",
    "section": "Framework",
    "slug": "framework/concepts/documents",
    "title": "Documents",
    "url": "https://jxsuite.com/docs/framework/concepts/documents/"
  },
  {
    "description": "How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.",
    "markdown": "\n# Elements\n\n> **Studio writes this format for you.** Every element you place on the canvas and edit in the [Properties panel](/docs/studio/design/properties) is stored as one of these objects — this page documents what lands in the file.\n\nAn element definition is a JSON object describing one DOM element. Its keys are the element's real DOM property names — set directly on the created element, with no translation layer in between. If a property exists on the DOM element, you can set it here.\n\n```json\n{\n  \"tagName\": \"div\",\n  \"id\": \"my-element\",\n  \"className\": \"container active\",\n  \"hidden\": false,\n  \"tabIndex\": 0,\n  \"textContent\": \"Hello World\"\n}\n```\n\n## DOM properties\n\nProperty names follow the DOM, not HTML source: `className` (not `class`), `textContent` (not inner text), `tabIndex` (not `tabindex`). Any string-valued property may contain a `${}` template for a reactive value (see [Reactivity](/docs/framework/concepts/reactivity)), or be an object with `$ref` bound to state (see [References](/docs/framework/concepts/references)):\n\n```json\n{\n  \"tagName\": \"div\",\n  \"textContent\": \"${state.count} items remaining\",\n  \"hidden\": \"${state.items.length === 0}\"\n}\n```\n\nTwo properties are **protected**: `id` and `tagName` identify the element and may not be set via `$ref` bindings.\n\n## Custom attributes\n\nAnything that is not a standard DOM property — `data-*`, `aria-*`, `slot` — goes in the `attributes` object, written exactly as it appears in HTML:\n\n```json\n{\n  \"tagName\": \"div\",\n  \"attributes\": {\n    \"data-component\": \"my-widget\",\n    \"aria-label\": \"Interactive counter\",\n    \"slot\": \"header\"\n  }\n}\n```\n\nAttribute values may also be reactive templates (`\"aria-label\": \"${state.count} unread messages\"`).\n\n## Children\n\n`children` is an array of element definitions, rendered in order:\n\n```json\n{\n  \"tagName\": \"div\",\n  \"children\": [\n    { \"tagName\": \"h1\", \"textContent\": \"Title\" },\n    { \"tagName\": \"p\", \"textContent\": \"Content\" }\n  ]\n}\n```\n\nA `children` array may freely mix element objects, bare text nodes, and [repeaters](/docs/framework/concepts/lists) (`$prototype: \"Array\"` members) or [switches](/docs/framework/concepts/switching).\n\n## Text nodes\n\nBare strings and numbers are valid `children` items. They produce DOM `Text` nodes directly, with no wrapper element — this is how text with inline markup is written:\n\n```json\n{\n  \"tagName\": \"p\",\n  \"children\": [\"Hello \", { \"tagName\": \"strong\", \"textContent\": \"world\" }, \"!\"]\n}\n```\n\nThat is the HTML `<p>Hello <strong>world</strong>!</p>`. Template strings in text nodes are reactive: `{ \"children\": [\"Welcome, ${state.name}!\"] }`.\n\n## Slots\n\nCustom elements use the standard HTML `slot` mechanism for content composition. A component's template places `<slot>` elements; content the instance provides is distributed to them by `name`:\n\n```json\n{\n  \"tagName\": \"card-component\",\n  \"children\": [\n    {\n      \"tagName\": \"header\",\n      \"children\": [{ \"tagName\": \"slot\", \"attributes\": { \"name\": \"header\" } }]\n    },\n    { \"tagName\": \"main\", \"children\": [{ \"tagName\": \"slot\" }] }\n  ]\n}\n```\n\nA slot's own `children` act as fallback content, kept when the instance provides nothing for it.\n\n## Annotations\n\nAny element may carry `$title` and `$description` — developer-facing labels that never reach the DOM:\n\n```json\n{\n  \"tagName\": \"section\",\n  \"$title\": \"Hero Section\",\n  \"$description\": \"Primary landing area with headline and call-to-action\",\n  \"children\": []\n}\n```\n\nStudio's [Layers panel](/docs/studio/design/layers) shows `$title` as the element's display name.\n\n## How it works\n\nThe runtime creates the element with `document.createElement(tagName)`, assigns each listed property directly on the element object, and writes `attributes` entries with `setAttribute`. Properties whose values are templates or `$ref` bindings are wrapped in reactive effects, so the DOM updates whenever the underlying state changes. Slot distribution is manual light-DOM distribution: host children are captured before the template renders, then moved to matching `<slot>` elements by `name`.\n\n## Rules\n\n- `tagName` is required on every element definition.\n- `id` and `tagName` are protected — never settable via `$ref`.\n- Standard DOM properties go at the top level; everything else (`data-*`, `aria-*`, `slot`) goes in `attributes`.\n- Bare strings and numbers in `children` become text nodes; when _all_ children are bare strings with no element siblings, prefer `textContent` instead.\n- `$title` and `$description` are plain strings — not reactive, not `$ref`-resolvable, never applied to the DOM.\n- Styling does not use DOM properties — the `style` object has its own grammar, covered in [Styling](/docs/framework/concepts/styling).\n\n## Related\n\n- [Documents](/docs/framework/concepts/documents) — the file these objects live in\n- [Reactivity](/docs/framework/concepts/reactivity) — `${}` templates in any string property\n- [Styling](/docs/framework/concepts/styling) — the `style` object, nesting, and breakpoints\n- [Lists and iteration](/docs/framework/concepts/lists) — repeaters inside `children`\n- [Properties panel](/docs/studio/design/properties) — the Studio surface that edits these objects\n",
    "section": "Framework",
    "slug": "framework/concepts/elements",
    "title": "Elements",
    "url": "https://jxsuite.com/docs/framework/concepts/elements/"
  },
  {
    "description": "How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.",
    "markdown": "\n# Components\n\n> **Studio writes this format for you.** This page documents the underlying JSON — useful when you want to hand-edit a file, review a diff, or understand what the visual tools produce.\n\nA Jx component is a single `.json` file. All state, computed values, and functions are declared in `state`. Simple components need no sidecar file.\n\n## Self-Describing Components\n\n```\n{\n  \"$id\": \"Counter\",\n  \"state\": {\n    \"count\": 0,\n    \"increment\": {\n      \"$prototype\": \"Function\",\n      \"body\": \"state.count++\"\n    }\n  },\n  \"tagName\": \"my-counter\",\n  \"children\": [\n    { \"tagName\": \"span\", \"textContent\": \"${state.count}\" },\n    { \"tagName\": \"button\", \"textContent\": \"+\", \"onclick\": { \"$ref\": \"#/state/increment\" } }\n  ]\n}\n```\n\n## State Shapes\n\nEvery entry in `state` falls into one of four shapes, determinable by inspection:\n\n### Shape 1 — Naked Value\n\nA JSON scalar, array, or plain object with no reserved keys:\n\n```\n{ \"state\": { \"count\": 0, \"name\": \"World\", \"tags\": [] } }\n```\n\n### Shape 2 — Typed Value\n\nAn object with a `default` property and optional `type`:\n\n```\n{\n  \"state\": {\n    \"count\": {\n      \"type\": { \"$ref\": \"#/$defs/Count\" },\n      \"default\": 0,\n      \"description\": \"Current counter value\"\n    }\n  }\n}\n```\n\n### Shape 3 — Computed (Template String)\n\nA string containing `${}` syntax:\n\n```\n{\n  \"state\": {\n    \"fullName\": \"${state.firstName} ${state.lastName}\",\n    \"isEmpty\": \"${state.items.length === 0}\"\n  }\n}\n```\n\n### Shape 4 — Prototype (`$prototype`)\n\nAn object with `$prototype` for functions and data sources:\n\n```\n{\n  \"state\": {\n    \"increment\": {\n      \"$prototype\": \"Function\",\n      \"body\": \"state.count++\"\n    },\n    \"userData\": {\n      \"$prototype\": \"Request\",\n      \"url\": \"/api/users/\",\n      \"method\": \"GET\"\n    }\n  }\n}\n```\n\n## External Sidecars\n\nWhen functions grow complex, extract them to a `.js` file:\n\n```\n{\n  \"state\": {\n    \"increment\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" },\n    \"decrement\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" }\n  }\n}\n```\n\n```\nexport function increment(state) {\n  state.count++;\n}\nexport function decrement(state) {\n  state.count = Math.max(0, state.count - 1);\n}\n```\n\nThe first parameter is always `state` — the component's reactive scope. `this` is never used.\n\n## Custom Elements\n\nA component whose `tagName` contains a hyphen is a custom element:\n\n```\n{\n  \"tagName\": \"user-card\",\n  \"state\": {\n    \"username\": \"Guest\",\n    \"status\": \"offline\"\n  },\n  \"children\": [{ \"tagName\": \"h3\", \"textContent\": \"${state.username}\" }]\n}\n```\n\nCustom elements render to the light DOM (no Shadow DOM). Style scoping uses `data-jx` attributes.\n\n## Props and Encapsulation\n\nProps are passed via `$props` on an instance node — the only mechanism for crossing component boundaries. Register the component in `$elements` and instantiate it by its custom-element tag:\n\n```\n{\n  \"$elements\": { \"my-card\": { \"$ref\": \"./card.json\" } },\n  \"children\": [\n    {\n      \"tagName\": \"my-card\",\n      \"$props\": {\n        \"title\": \"Static string\",\n        \"count\": { \"$ref\": \"#/state/count\" }\n      }\n    }\n  ]\n}\n```\n\nSignal scope is bounded at the component level. No implicit scope leaking.\n",
    "section": "Framework",
    "slug": "framework/concepts/components",
    "title": "Components",
    "url": "https://jxsuite.com/docs/framework/concepts/components/"
  },
  {
    "description": "How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.",
    "markdown": "\n# Props and scope\n\n> **Studio writes this format for you.** Editing an instance's props in the Properties panel — see [Working with components](/docs/studio/design/components) — writes the `$props` objects on this page.\n\nScope is where a name can be seen. Within one document, every `state` entry is visible to every descendant element — no passing required. Across a component boundary, nothing is visible unless it is passed explicitly through `$props`. That single rule makes data flow statically knowable: you can read a file and see exactly what it depends on.\n\n```json\n{\n  \"tagName\": \"my-card\",\n  \"$props\": {\n    \"title\": \"Static string\",\n    \"count\": { \"$ref\": \"#/state/count\" }\n  }\n}\n```\n\n## Component instances\n\nA component instance is created in two steps: **register** the component document under a custom-element tag in the top-level `$elements` map, then place an element node with that tag. `$props` is optional — an instance without it renders the component with its own defaults:\n\n```json\n{\n  \"$elements\": {\n    \"my-counter\": { \"$ref\": \"./components/my-counter.json\" },\n    \"my-card\": { \"$ref\": \"./components/card.json\" }\n  },\n  \"children\": [\n    { \"tagName\": \"my-counter\" },\n    {\n      \"tagName\": \"my-card\",\n      \"$props\": {\n        \"title\": \"Hello\",\n        \"count\": { \"$ref\": \"#/state/count\" }\n      }\n    }\n  ]\n}\n```\n\n:::doc-note\nA bare `{ \"$ref\": \"./card.json\" }` placed directly in `children` is **not** a component instance — it renders an empty `<div>`. Register the document in `$elements` and instantiate it by its tag, as above. See spec §13.\n:::\n\n## Static and bound props\n\nA prop value is either a plain JSON value (fixed for this instance) or a `$ref` (bound to the parent's state). Functions pass the same way, so a child can trigger behavior the parent owns:\n\n```json\n{\n  \"tagName\": \"my-card\",\n  \"$props\": {\n    \"title\": \"Static string\",\n    \"count\": { \"$ref\": \"#/state/count\" },\n    \"onAction\": { \"$ref\": \"#/state/handleAction\" }\n  }\n}\n```\n\nInside `card.json`, `title` and `count` behave like the component's own state entries — typically declared there with defaults, overridden per instance.\n\n## Signal forwarding\n\nWhen a `$props` value is a `$ref` to a reactive state entry, the child receives the **same reactive reference**, not a copy. A write in either scope triggers updates in both — parent and child stay in sync through one shared signal. A plain value, by contrast, is just an initial setting for that instance.\n\n## Scope levels\n\n| Level      | Scope                   | Mirrors                   |\n| ---------- | ----------------------- | ------------------------- |\n| `window`   | Application-wide        | `window` global           |\n| `document` | Document-wide           | `document` object         |\n| Component  | Custom element boundary | CSS custom property scope |\n\nGlobals are reachable from any component via the `window#/` and `document#/` [reference schemes](/docs/framework/concepts/references); everything else is bounded at the component.\n\n## Resolution order\n\nWhen a name is looked up, scopes are consulted in order:\n\n1. `$map/` context — the enclosing [repeater](/docs/framework/concepts/lists) iteration\n2. Local component `state`\n3. Explicitly passed `$props`\n4. `window` globals\n5. `document` globals\n\n## How it works\n\nEach component builds its own reactive scope from its own `state`. When the runtime renders an instance, it resolves each `$props` entry against the **parent's** scope, then layers the results onto the child's scope — plain values as initial settings, `$ref` values as live references into the parent's reactive state. Nothing else crosses over: a template string or `$ref` inside the child can only see the child's scope plus what was passed.\n\n## Rules\n\n- `$props` is the only mechanism for passing state across a component boundary — scope never leaks implicitly.\n- Within a single component, all `state` entries are available to all descendant elements without passing.\n- Signal scope is bounded at the component (custom element) level, like a CSS custom property.\n- Private `#`-prefixed state entries can never be set via `$props`.\n- A `$ref` prop is live in both directions; a plain value is per-instance and static.\n- Every external dependency must appear in `$props` — that is what makes documents statically analyzable.\n\n## Related\n\n- [Components](/docs/framework/concepts/components) — the component model and custom elements\n- [State](/docs/framework/concepts/state) — declaring the entries props override\n- [References](/docs/framework/concepts/references) — `parent#/`, `window#/`, `document#/` schemes\n- [Dynamic switching](/docs/framework/concepts/switching) — swapping whole components on state\n- [Working with components](/docs/studio/design/components) — the Studio props workflow\n",
    "section": "Framework",
    "slug": "framework/concepts/props-and-scope",
    "title": "Props and scope",
    "url": "https://jxsuite.com/docs/framework/concepts/props-and-scope/"
  },
  {
    "description": "The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.",
    "markdown": "\n# State\n\n> **Studio writes this format for you.** Every entry you create in the [State panel](/docs/studio/logic/state) — values, computed entries, data sources, functions — is saved as one of the shapes on this page.\n\n`state` is the root-level object holding everything a document knows at runtime: mutable values, derived values, functions, and data sources. Every entry is reactive by default — change a value and everything bound to it updates. The **shape** of each entry determines what it is; no extra flags are needed in the common case.\n\n```json\n{\n  \"tagName\": \"my-counter\",\n  \"state\": { \"count\": 0 },\n  \"children\": [{ \"tagName\": \"span\", \"textContent\": \"${state.count}\" }]\n}\n```\n\n![Jx Studio State panel listing a component's state and functions](../../images/state-panel.png)\n\n## Shape 1 — naked values\n\nA JSON scalar, array, or plain object with no reserved keys is a mutable reactive value, initialized to exactly what you wrote:\n\n```json\n{\n  \"state\": {\n    \"count\": 0,\n    \"price\": 9.99,\n    \"name\": \"World\",\n    \"active\": false,\n    \"data\": null,\n    \"tags\": [],\n    \"user\": { \"id\": null, \"name\": \"\", \"role\": \"guest\" }\n  }\n}\n```\n\nA plain string counts as a naked value only when it contains no `${}` — otherwise it is Shape 3.\n\n## Shape 2 — typed values\n\nAn object with a `default` property (and no `$prototype`) is a typed value. `default` supplies the initial value; `type` references a JSON Schema, either inline or via `$ref` to `$defs`:\n\n```json\n{\n  \"state\": {\n    \"count\": {\n      \"type\": { \"$ref\": \"#/$defs/Count\" },\n      \"default\": 0,\n      \"description\": \"Current counter value\"\n    }\n  }\n}\n```\n\nSchema keywords are tooling-only — they drive validation, autocomplete, and which Studio control edits the entry, then are stripped before runtime. The `format` keyword picks a specialized Studio input without changing the runtime value:\n\n```json\n{\n  \"state\": {\n    \"bg\": { \"type\": \"string\", \"format\": \"image\", \"default\": \"\" },\n    \"publishDate\": { \"type\": \"string\", \"format\": \"date\", \"default\": \"\" },\n    \"accentColor\": { \"type\": \"string\", \"format\": \"color\", \"default\": \"#000000\" }\n  }\n}\n```\n\nUse the typed form when the value needs constraints, documentation, or a shared type; use naked values when none apply.\n\n## Shape 3 — computed templates\n\nA string containing `${}` is a **computed** value — recalculated automatically whenever the state it reads changes:\n\n```json\n{\n  \"state\": {\n    \"fullName\": \"${state.firstName} ${state.lastName}\",\n    \"displayTitle\": \"${state.score >= 90 ? 'Expert' : 'Beginner'}\",\n    \"scoreLabel\": \"${state.score}%\",\n    \"isEmpty\": \"${state.items.length === 0}\"\n  }\n}\n```\n\nThe text inside `${}` is a pure expression over `state` — no statements, no assignments, no `return`. For computed values built without expression syntax at all, see [Expressions](/docs/framework/concepts/expressions).\n\n## Shape 4 — prototypes\n\nAn object with `$prototype` declares a function or a data source. Functions use `$prototype: \"Function\"` with an inline `body` or an external `$src` (see [Functions](/docs/framework/concepts/functions)); any other constructor name declares a data source (see [Data prototypes](/docs/framework/concepts/data-prototypes)):\n\n```json\n{\n  \"state\": {\n    \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" },\n    \"userData\": {\n      \"$prototype\": \"Request\",\n      \"url\": \"/api/users/\",\n      \"method\": \"GET\"\n    }\n  }\n}\n```\n\nData-source entries always resolve reactively. An object with `$expression` is the fifth, fully declarative shape — a computed value or handler built from structure instead of a code string ([Expressions](/docs/framework/concepts/expressions)).\n\n## Private entries\n\nA `#` prefix marks an entry as private — internal workings that are never exposed to the Studio property panel, never included in tooling exports, and never settable via `$props`:\n\n```json\n{\n  \"state\": {\n    \"count\": 0,\n    \"#cache\": {},\n    \"#lastFetchTime\": null\n  }\n}\n```\n\n## How it works\n\nAt load time the runtime walks `state` once, classifying each entry by inspection in a fixed order: a string containing `${` is computed; any other scalar or array is a naked value; an object is checked for `$expression`, then `$prototype`, then `default`; a plain object with no reserved keys is a naked object value. The whole scope is initialized inside a reactive proxy, computed templates become tracked computed values, and inside function bodies state is read and written directly (`state.count`, `state.items.push(...)` — no getter or setter calls).\n\n## Rules\n\n- Every entry is reactive by default; there is no opt-in flag.\n- Shape follows from inspection alone — a string with `${}` is _always_ computed, an object with `default` and no `$prototype` is _always_ a typed value.\n- Computed templates must be pure expressions: no statements, no assignments, no semicolons, no `return`.\n- `state` inside a template refers only to the current document's scope — never a parent's.\n- Entry names are `camelCase` (`count`, `firstName`, `handleInput`); types in `$defs` are `PascalCase` (`TodoItem`).\n- `#`-prefixed entries are private: hidden from props and tooling exports.\n- On a Function entry, `body` and `$src` are mutually exclusive.\n\n## Related\n\n- [Reactivity](/docs/framework/concepts/reactivity) — how `${}` bindings track dependencies\n- [Expressions](/docs/framework/concepts/expressions) — the declarative Shape 5\n- [Functions](/docs/framework/concepts/functions) — Function entries, sidecars, and statements\n- [Data prototypes](/docs/framework/concepts/data-prototypes) — Request, LocalStorage, collections\n- [State panel](/docs/studio/logic/state) — the Studio surface that writes these entries\n",
    "section": "Framework",
    "slug": "framework/concepts/state",
    "title": "State",
    "url": "https://jxsuite.com/docs/framework/concepts/state/"
  },
  {
    "description": "Template strings, signals, computed values, and reactive bindings in Jx.",
    "markdown": "\n# Reactivity\n\n> **Studio writes this format for you.** The State, Data, and Events panels ([Script & logic](/docs/studio/logic)) generate everything below — this page documents the model if you want to hand-edit or understand it.\n\nTemplate literal syntax `${}` is valid **anywhere a string value appears** in the document tree. All reactivity is powered by `@vue/reactivity`.\n\n## Reactive Element Properties\n\n```json\n{\n  \"tagName\": \"div\",\n  \"textContent\": \"${state.count} items remaining\",\n  \"className\": \"${state.active ? 'card active' : 'card'}\",\n  \"hidden\": \"${state.items.length === 0}\"\n}\n```\n\n## Reactive Style Properties\n\n```json\n{\n  \"tagName\": \"div\",\n  \"style\": {\n    \"color\": \"${state.score > 90 ? 'gold' : 'inherit'}\",\n    \"opacity\": \"${state.loading ? '0.5' : '1'}\"\n  }\n}\n```\n\n## Reactive Attributes\n\n```json\n{\n  \"tagName\": \"button\",\n  \"attributes\": {\n    \"aria-label\": \"${state.count} unread messages\",\n    \"data-state\": \"${state.status}\"\n  }\n}\n```\n\n## How It Works\n\nWhen the compiler encounters `${}` in any string-valued property, it wraps the binding in a reactive effect:\n\n```js\nwatchEffect(() => {\n  el.textContent = `${state.count} items remaining`;\n});\n```\n\nDependencies are tracked automatically by Vue when `state.*` properties are read.\n\n## `$ref` vs Template Strings\n\n| Pattern                       | Use when                                          |\n| ----------------------------- | ------------------------------------------------- |\n| `{ \"$ref\": \"#/state/label\" }` | Binding to a named signal used in multiple places |\n| `\"${state.count} items\"`      | Inline computed binding used in exactly one place |\n\n## Computed State\n\nTemplate strings in `state` become `computed()` values:\n\n```json\n{\n  \"state\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"fullName\": \"${state.firstName} ${state.lastName}\"\n  }\n}\n```\n\n## Signal Access in JavaScript\n\nWithin `body` strings and external `.js` files, read and write state directly:\n\n```js\n// Read\nconst current = state.count;\n\n// Write\nstate.count = current + 1;\n\n// Mutate array (Vue tracks mutations)\nstate.items.push(newItem);\n\n// Mutate nested object\nstate.user.name = \"Alice\";\n```\n\nNo `.get()` or `.set()` calls. No `this`. All component state is accessed via `state`.\n\n## Web API Prototypes\n\nBuilt-in prototypes for common web APIs:\n\n| `$prototype`      | Web API      | Description                   |\n| ----------------- | ------------ | ----------------------------- |\n| `Request`         | Fetch API    | Reactive URL, debounce, abort |\n| `URLSearchParams` | URL API      | Computed `.toString()`        |\n| `FormData`        | FormData API | Field population              |\n| `LocalStorage`    | Storage API  | Reactive persistence          |\n| `SessionStorage`  | Storage API  | Session-scoped storage        |\n| `IndexedDB`       | IDB API      | Store creation, CRUD          |\n| `Array`           | —            | Dynamic mapped lists          |\n\n## Timing\n\n| Value        | When                                            |\n| ------------ | ----------------------------------------------- |\n| `\"client\"`   | Resolved at runtime in the browser (default)    |\n| `\"server\"`   | Resolved at runtime on the server via RPC       |\n| `\"compiler\"` | Resolved at build time, baked into emitted HTML |\n",
    "section": "Framework",
    "slug": "framework/concepts/reactivity",
    "title": "Reactivity",
    "url": "https://jxsuite.com/docs/framework/concepts/reactivity/"
  },
  {
    "description": "$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.",
    "markdown": "\n# References\n\n> **Studio writes this format for you.** Switching any bindable field to **$ref** mode with its field-mode button ([Formulas and expressions](/docs/studio/logic/formulas)) writes the `$ref` objects on this page.\n\nA reference is an object with a single `$ref` key whose string value points at something declared elsewhere — a state entry, a global, an iteration item, or another file. The path **borrows JSON Pointer syntax** (RFC 6901 shape — a `#`-fragment of `/`-separated tokens), but the semantics are Jx-specific: a `$ref` reads a live value off the reactive scope, it does not substitute a schema. (So RFC 6901 `~0`/`~1` escapes are not implemented; within a nested path the segments after the first may be separated by `.` as well as `/`.)\n\n```json\n{\n  \"tagName\": \"p\",\n  \"textContent\": { \"$ref\": \"#/state/count\" }\n}\n```\n\n## Reference schemes\n\nThe prefix of the `$ref` string selects where the lookup happens:\n\n| Scheme           | Example                 | Resolves to                                                                                                   |\n| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Internal `state` | `\"#/state/count\"`       | Value or handler in the current document's `state`                                                            |\n| Window global    | `\"window#/currentUser\"` | `window.currentUser`                                                                                          |\n| Document global  | `\"document#/appConfig\"` | `document.appConfig`                                                                                          |\n| Parent scope     | `\"parent#/sharedState\"` | Named value passed via `$props`                                                                               |\n| Map context      | `\"$map/item\"`           | Current item in a repeater iteration                                                                          |\n| Map index        | `\"$map/index\"`          | Current index in a repeater iteration                                                                         |\n| Event context    | `\"event#/target/value\"` | Property path on the handler event (expressions)                                                              |\n| External file    | `\"./other.json\"`        | A component document — for `$switch` cases and `$elements` registration, not a bare `children` node (see §13) |\n\nPaths navigate nested data with further `/` segments: `\"#/state/user/name\"`, `\"$map/item/title\"`.\n\n## Reactive bindings\n\nWhen a `$ref` resolves to a reactive state entry or computed value, the binding is live — the DOM property updates automatically whenever the value changes. The same syntax binds behavior: pointing an event property at a Function entry attaches it as the handler.\n\n```json\n{\n  \"tagName\": \"button\",\n  \"textContent\": \"+\",\n  \"onclick\": { \"$ref\": \"#/state/increment\" }\n}\n```\n\n## `$ref` or template string?\n\nBoth bind reactively; they differ in intent:\n\n| Pattern                       | Use when                                                 |\n| ----------------------------- | -------------------------------------------------------- |\n| `{ \"$ref\": \"#/state/label\" }` | Binding to a named value — referenced in multiple places |\n| `\"${state.count} items\"`      | Inline computed binding used in exactly one place        |\n\nPrefer `${}` for single-use bindings and `$ref` for reused or named values (see [Reactivity](/docs/framework/concepts/reactivity)).\n\n## The `event#` scheme\n\nInside a declarative [expression](/docs/framework/concepts/expressions) used as an event handler, `event#/` reads a property path off the event itself — no function body required:\n\n```json\n{\n  \"tagName\": \"input\",\n  \"attributes\": { \"placeholder\": \"Item name\" },\n  \"oninput\": {\n    \"$expression\": {\n      \"operator\": \"=\",\n      \"target\": { \"$ref\": \"#/state/name\" },\n      \"value\": { \"$ref\": \"event#/target/value\" }\n    }\n  }\n}\n```\n\n`event#` is resolvable only in handler position. Referencing it from an expression that is not invoked as a handler is a compile-time error.\n\n## Resolution\n\nThe leading token selects exactly one source — it is scheme dispatch, not a cascading fallback:\n\n1. `$map/` — iteration context\n2. `$reduce/acc` — fold accumulator ([expression](/docs/framework/concepts/expressions) `reduce` only)\n3. `$args/` — named-formula parameters (callable bodies only)\n4. `event#/` — handler event context (handler position only)\n5. `#/state/` — the current document's scope (which already includes `$props`, merged in place)\n6. `parent#/` — resolves against that same merged scope\n7. `window#/` / `document#/` — the corresponding global object\n\nA `#/state/…` miss does **not** fall through to `window`/`document` — those are reachable only through an explicit `window#/` or `document#/` ref. (An explicit-scheme miss reads as `undefined`; only a bare, schemeless ref falls back to `null`.)\n\n## How it works\n\nThe runtime resolves each `$ref` at render time against the current scope: `$map/` and `parent#/` look up the enclosing iteration or prop scope, `#/state/` reads the document's reactive proxy, and deeper path segments walk nested objects step by step. Because reads happen inside reactive effects, any binding whose target changes re-runs automatically. An external `.json` `$ref` is loaded and rendered where the runtime supports it — as a [`$switch`](/docs/framework/concepts/switching) case or an `$elements` registration that backs a [component instance](/docs/framework/concepts/props-and-scope) — but a bare `{ \"$ref\": \"./x.json\" }` node is not (see spec §13).\n\n## Rules\n\n- A reference object has exactly one meaningful key: `$ref` with a string value (component instances may add `$props`).\n- `id` and `tagName` are protected element properties — they can never be set via `$ref`.\n- `$map/` references are valid only inside a repeater's `map` template.\n- `event#/` is valid only in an expression used as an event handler.\n- `parent#/` resolves only names explicitly passed via `$props` — there is no implicit parent access.\n- External file references resolve the whole document (via `$switch`/`$elements`); you cannot point into another file's internals, and a bare external `$ref` child is not a component instance (spec §13).\n\n## Related\n\n- [Reactivity](/docs/framework/concepts/reactivity) — `${}` templates, the inline alternative\n- [Expressions](/docs/framework/concepts/expressions) — where `event#` and `$args/` live\n- [Props and scope](/docs/framework/concepts/props-and-scope) — `parent#/` and the component boundary\n- [Lists and iteration](/docs/framework/concepts/lists) — the `$map/` context\n- [Formulas and expressions](/docs/studio/logic/formulas) — the Studio binding menu\n",
    "section": "Framework",
    "slug": "framework/concepts/references",
    "title": "References",
    "url": "https://jxsuite.com/docs/framework/concepts/references/"
  },
  {
    "description": "Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.",
    "markdown": "\n# Expressions\n\n> **Studio writes this format for you.** The **fx** mode on value fields and the formula editor ([Formulas and expressions](/docs/studio/logic/formulas)) generate everything below — this page documents the JSON if you want to hand-edit a file, review a diff, or understand what the chips represent.\n\nAn `$expression` is a computation or state change written as JSON structure instead of JavaScript source. One operator is applied to a `target`, optionally with a `value` — and because the whole thing is data, schema tooling can validate it, Studio can render it as editable chips, and the compiler can analyze it without parsing code.\n\nThe smallest complete expression — a dark-mode toggle handler:\n\n```json\n{\n  \"$expression\": {\n    \"operator\": \"=\",\n    \"target\": { \"$ref\": \"#/state/darkMode\" },\n    \"value\": { \"operator\": \"!\", \"target\": { \"$ref\": \"#/state/darkMode\" } }\n  }\n}\n```\n\nExpressions sit on the middle rung of the escalation ladder. Prefer the least powerful form that does the job:\n\n| Rung                     | Power   | Use when                                  |\n| ------------------------ | ------- | ----------------------------------------- |\n| `$ref` binding           | Lowest  | Reading a state value                     |\n| `${}` template           | Low     | Single-use computed read                  |\n| `$expression`            | Mid     | Declarative computation or state mutation |\n| `$prototype: \"Function\"` | Highest | Logic not expressible as structure        |\n\n## Expression nodes\n\nEvery node has the same three fields:\n\n| Field      | Required          | Description                                                              |\n| ---------- | ----------------- | ------------------------------------------------------------------------ |\n| `operator` | Yes               | A token from the blessed set (see below)                                 |\n| `target`   | Yes               | The operand the operator acts on — a `$ref`, a literal, or a nested node |\n| `value`    | By operator arity | The right-hand operand — a `$ref`, a literal, an array, or a nested node |\n\nNodes nest: a `target` or `value` may itself be a node, with no inner `$expression` wrapper — the wrapper appears only at the state entry or handler boundary. `counter = counter + 1` becomes:\n\n```json\n{\n  \"$expression\": {\n    \"operator\": \"=\",\n    \"target\": { \"$ref\": \"#/state/counter\" },\n    \"value\": { \"operator\": \"+\", \"target\": { \"$ref\": \"#/state/counter\" }, \"value\": 1 }\n  }\n}\n```\n\nOperands resolve exactly like any [`$ref`](/docs/framework/concepts/references) — state paths are always explicit JSON Pointers (`#/state/counter`), never raw `state.counter` strings, and `$map/` paths resolve through iteration context as elsewhere.\n\n## Pure and mutating operators\n\nEvery operator is one of two modes, and the mode is never declared — it follows from the operator:\n\n- **Pure** operators compute and return a value, mutating nothing: unary (`!`, `-`), arithmetic, comparison, logical, conditionals, aggregates, and the method operators below. A pure expression used as a state entry is a computed value, read via `$ref` or `${}` like any other.\n- **Mutating** operators write to their `target` and return nothing: assignment (`=`, `+=`, `-=`, `*=`, `/=`) and the array-mutation methods (`push`, `pop`, `shift`, `unshift`, `splice`). A mutating expression is a handler, bound to events.\n\n```json\n{ \"operator\": \"push\", \"target\": { \"$ref\": \"#/state/cart\" }, \"value\": { \"$ref\": \"$map/item\" } }\n```\n\n```json\n{\n  \"operator\": \"splice\",\n  \"target\": { \"$ref\": \"#/state/cart\" },\n  \"value\": [{ \"$ref\": \"$map/index\" }, 1]\n}\n```\n\n`push`/`unshift` take a single `value`; `splice` takes an argument array (`[start, deleteCount, ...items]`); `pop`/`shift` take none. The full operator set is closed and enumerated in the [operator reference](/docs/framework/reference/operators) — anything outside it is a compile-time error, and logic that needs more escalates to a [function body](/docs/framework/concepts/functions).\n\n## Aggregates\n\n`reduce`, `map`, and `filter` are pure operators over an array `target`. Their `value` is a per-item expression evaluated once per element, with the same `$map/` context that [mapped lists](/docs/framework/concepts/lists) use:\n\n| Reference                   | Bound during aggregation        |\n| --------------------------- | ------------------------------- |\n| `{ \"$ref\": \"$map/item\" }`   | The current array element       |\n| `{ \"$ref\": \"$map/index\" }`  | The current zero-based index    |\n| `{ \"$ref\": \"$reduce/acc\" }` | The accumulator (`reduce` only) |\n\nA cart total — `acc + item.price * item.qty`, seeded by `initial`:\n\n```json\n{\n  \"operator\": \"reduce\",\n  \"target\": { \"$ref\": \"#/state/cart\" },\n  \"initial\": 0,\n  \"value\": {\n    \"operator\": \"+\",\n    \"target\": { \"$ref\": \"$reduce/acc\" },\n    \"value\": {\n      \"operator\": \"*\",\n      \"target\": { \"$ref\": \"$map/item/price\" },\n      \"value\": { \"$ref\": \"$map/item/qty\" }\n    }\n  }\n}\n```\n\n`reduce` requires `initial`; `map` and `filter` must not declare it. Aggregates compose by nesting — a `filter` node can be the `target` of a `reduce`.\n\n## Conditionals\n\n`?:` selects between two results. Its fields follow the ECMAScript conditional: `target` is the test, `value` the result when true, `initial` the result when false — all three required. Else-if chains nest another `?:` in `initial`.\n\n```json\n{\n  \"operator\": \"?:\",\n  \"target\": { \"operator\": \">\", \"target\": { \"$ref\": \"#/state/cart/length\" }, \"value\": 10 },\n  \"value\": \"Cart full\",\n  \"initial\": \"Keep shopping\"\n}\n```\n\n`switch` is value-keyed selection, mirroring element-level [`$switch`](/docs/framework/concepts/switching): `target` is the discriminant, `cases` maps its **string form** to results, `default` is optional.\n\n```json\n{\n  \"operator\": \"switch\",\n  \"target\": { \"$ref\": \"#/state/status\" },\n  \"cases\": { \"loading\": \"Please wait…\", \"error\": { \"$ref\": \"#/state/errorMessage\" } },\n  \"default\": { \"$ref\": \"#/state/data/title\" }\n}\n```\n\nFor fallbacks, prefer `??` (nullish coalescing) over `||` when `0`, `\"\"`, or `false` are legitimate values — it substitutes only for `null` and `undefined`.\n\n## Pure method operators\n\nGenuine `String.prototype`, `Array.prototype`, and `Number.prototype` methods are operators too — the receiver in `target`, arguments in `value` (a bare scalar, or an array for multiple). Only **pure** methods qualify; where the original mutates, the ES2023 change-by-copy name stands in (`toSorted`, not `sort`).\n\n```json\n{ \"operator\": \"toUpperCase\", \"target\": { \"$ref\": \"#/state/name\" } }\n```\n\n```json\n{ \"operator\": \"padStart\", \"target\": { \"$ref\": \"#/state/code\" }, \"value\": [6, \"0\"] }\n```\n\nEvaluation is null-safe: a missing receiver yields `undefined` rather than throwing. The full method table is in the [operator reference](/docs/framework/reference/operators).\n\n## Named formulas and `call`\n\nA pure expression entry with `parameters` is a **named formula** — a reusable computation instead of a computed value. Inside its body, parameters resolve through the `$args/` scheme:\n\n```json\n{\n  \"lineTotal\": {\n    \"parameters\": [\n      { \"name\": \"price\", \"type\": { \"text\": \"number\" } },\n      { \"name\": \"qty\", \"type\": { \"text\": \"number\" }, \"default\": 1 }\n    ],\n    \"$expression\": {\n      \"operator\": \"*\",\n      \"target\": { \"$ref\": \"$args/price\" },\n      \"value\": { \"$ref\": \"$args/qty\" }\n    }\n  }\n}\n```\n\nThe `call` operator invokes it: `target` is the callee pointer, `value` the positional argument list, ordered by the declared `parameters` (omitted arguments take their `default`):\n\n```json\n{\n  \"operator\": \"call\",\n  \"target\": { \"$ref\": \"#/state/lineTotal\" },\n  \"value\": [{ \"$ref\": \"$map/item/price\" }, { \"$ref\": \"$map/item/qty\" }]\n}\n```\n\nA callee may also be a pure standard-library global through `window#/`, gated by a closed allowlist (`Math.*`, `JSON.*`, `Object.keys`/`values`/`entries`/`fromEntries`, `Number.*`, `Array.from`/`isArray`/`of`, `parseFloat`, `structuredClone`, …). Impure platform functions — `fetch`, `alert`, `Math.random`, `Date.now` — are deliberately absent; side effects belong to [Function entries](/docs/framework/concepts/functions).\n\n```json\n{\n  \"operator\": \"call\",\n  \"target\": { \"$ref\": \"window#/Math/max\" },\n  \"value\": [{ \"$ref\": \"#/state/a\" }, { \"$ref\": \"#/state/b\" }, 0]\n}\n```\n\nFormulas declared in `project.json` `state` are available on every page. A catalog of ready-made composite formulas ships with Studio's palette — see the [formula catalog](/docs/framework/reference/formulas).\n\n## Reading the event\n\nHandlers receive the DOM event, and the `event#/` reference scheme reads it without escalating to code — resolvable only inside an expression used as an event handler:\n\n```json\n{\n  \"tagName\": \"input\",\n  \"oninput\": {\n    \"$expression\": {\n      \"operator\": \"=\",\n      \"target\": { \"$ref\": \"#/state/name\" },\n      \"value\": { \"$ref\": \"event#/target/value\" }\n    }\n  }\n}\n```\n\n## Where expressions appear\n\nAn `$expression` is valid in two positions:\n\n1. **As a `state` entry** — a named, reusable operation. A pure entry is a computed value; a mutating entry is a handler you bind with `\"onclick\": { \"$ref\": \"#/state/toggleTheme\" }`, exactly like a Function entry.\n2. **Inline as an event handler value** on any element, in place of a `$ref` to a function.\n\nPrefer the named form when reused; the inline form for single-use handlers.\n\n## How it works\n\nAn expression lowers to the same target a `body` string would — but the function is constructed from the node tree, never parsed from a string, so no `eval` or `new Function` is involved. A mutating expression becomes a handler over the reactive `state` proxy (`(state, event) => { state.darkMode = !state.darkMode; }`); a pure expression becomes a `computed()`, recomputing whenever the state it reads changes — see [Reactivity](/docs/framework/concepts/reactivity).\n\nReferences inside a node resolve in a fixed order:\n\n```\n1. $map/       — iteration context\n2. $reduce/acc — fold accumulator (reduce only)\n3. $args/      — named-formula parameters\n4. event#/     — handler event context\n5. #/state/    — current component scope\n6. parent#/    — explicitly passed props\n7. window#/    — global window properties\n8. document#/  — global document properties\n```\n\nIn production, `?:` and `switch` evaluate only the taken branch. In Studio's editor the interpreter runs with a trace that evaluates every branch, which is how each chip carries a live value badge.\n\n## Rules\n\n- The operator set is **closed**. An unknown operator is a compile-time error; escalate to a Function `body` instead.\n- A mutating node may only appear at the handler boundary — never nested as an operand.\n- Nested nodes take no `$expression` wrapper; it appears only at the entry or handler boundary.\n- State operands are explicit `$ref` pointers, never raw `state.x` strings.\n- `reduce` requires `initial`; `map` and `filter` must not declare it. `?:` requires `target`, `value`, and `initial`.\n- `switch` matches on the discriminant's string form, because JSON keys are strings.\n- `event#/` resolves only in handler position — referencing it from a non-handler entry is a compile-time error.\n- `call` targets a named formula or a blessed global; call depth is capped at 64, and statically detectable call cycles are rejected.\n\n## Related\n\n- [Operator reference](/docs/framework/reference/operators) — every blessed token\n- [Formula catalog](/docs/framework/reference/formulas) — composite formulas in Studio's palette\n- [Statements](/docs/framework/concepts/statements) — multi-step bodies built from expression nodes\n- [Functions and sidecars](/docs/framework/concepts/functions) — the escape hatch to real JavaScript\n- [Reactivity](/docs/framework/concepts/reactivity) — how computed values track their inputs\n- [Formulas and expressions in Studio](/docs/studio/logic/formulas) — the visual editor for all of this\n",
    "section": "Framework",
    "slug": "framework/concepts/expressions",
    "title": "Expressions",
    "url": "https://jxsuite.com/docs/framework/concepts/expressions/"
  },
  {
    "description": "Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.",
    "markdown": "\n# Statements\n\n> **Studio writes this format for you.** The statement editor ([Statements](/docs/studio/logic/statements)) builds these bodies as visual step cards — this page documents the JSON those cards write to disk.\n\nA [Function entry](/docs/framework/concepts/functions)'s `body` may be a **JSON array of statements** instead of a JavaScript source string. Each statement is one step; steps run top to bottom, exactly like the statement list inside a JavaScript function — but as structure, so tooling can validate, inspect, and edit every step.\n\nThe smallest complete structured body — mutate, then notify:\n\n```json\n{\n  \"addItem\": {\n    \"$prototype\": \"Function\",\n    \"body\": [\n      { \"operator\": \"push\", \"target\": { \"$ref\": \"#/state/items\" }, \"value\": \"New item\" },\n      { \"dispatchEvent\": \"items-changed\" }\n    ]\n  }\n}\n```\n\nThere are four statement kinds, each reusing a web-platform name:\n\n| Kind       | Shape                                                     | Source of the name                   |\n| ---------- | --------------------------------------------------------- | ------------------------------------ |\n| Expression | a bare expression node (mutation or `call`)               | ECMAScript ExpressionStatement       |\n| Branch     | `{ \"if\", \"then\", \"else\"? }`                               | JSON Schema conditional keywords     |\n| Multiway   | `{ \"$switch\", \"cases\", \"default\"? }`                      | Element-level `$switch`, ECMA switch |\n| Dispatch   | `{ \"dispatchEvent\", \"detail\"?, \"bubbles\"?, \"composed\"? }` | WHATWG DOM `dispatchEvent`           |\n\n## Expression statements\n\nAny mutating [expression node](/docs/framework/concepts/expressions) — or a `call` — stands alone as a step. No wrapper key; the node appears bare in the array:\n\n```json\n{ \"operator\": \"+=\", \"target\": { \"$ref\": \"#/state/count\" }, \"value\": 1 }\n```\n\nTo capture a result, make the step an assignment whose `value` is a `call` node — there is no dedicated capture field:\n\n```json\n{\n  \"operator\": \"=\",\n  \"target\": { \"$ref\": \"#/state/total\" },\n  \"value\": { \"operator\": \"call\", \"target\": { \"$ref\": \"#/state/computeTotal\" } }\n}\n```\n\n## Branches\n\n`if` holds a **pure** expression operand; `then` and `else` hold statement lists. `else` is optional:\n\n```json\n{\n  \"if\": { \"operator\": \">\", \"target\": { \"$ref\": \"#/state/cart/length\" }, \"value\": 10 },\n  \"then\": [{ \"dispatchEvent\": \"cart-full\" }],\n  \"else\": [{ \"operator\": \"call\", \"target\": { \"$ref\": \"#/state/refreshTotals\" } }]\n}\n```\n\n## Multiway branches\n\n`$switch` mirrors the element-level [`$switch`](/docs/framework/concepts/switching) in statement position: the discriminant is a pure operand, `cases` maps its **string form** to statement lists, and the optional `default` runs when no key matches:\n\n```json\n{\n  \"$switch\": { \"$ref\": \"#/state/status\" },\n  \"cases\": {\n    \"error\": [{ \"dispatchEvent\": \"load-failed\" }],\n    \"ready\": [{ \"operator\": \"=\", \"target\": { \"$ref\": \"#/state/visible\" }, \"value\": true }]\n  },\n  \"default\": []\n}\n```\n\n## Dispatching events\n\n`dispatchEvent` fires a DOM `CustomEvent` — the statement's other keys mirror `CustomEventInit`. `detail` may be a literal, a `$ref`, or an expression node:\n\n```json\n{\n  \"dispatchEvent\": \"cart-changed\",\n  \"detail\": { \"$ref\": \"#/state/cart\" },\n  \"bubbles\": true,\n  \"composed\": true\n}\n```\n\nThe event dispatches from the handler's `event.currentTarget` (or the component instance in compiled custom elements). Declare the events a function fires in its `emits` array so Studio can autocomplete them.\n\n## Parameters\n\nA structured body follows the named-formula pattern. Without `parameters`, the entry is an event handler — statements see `state` and the `event#/` scheme. With `parameters`, it is a callable invoked positionally, its arguments bound to `$args/` names:\n\n```json\n{\n  \"addToCart\": {\n    \"$prototype\": \"Function\",\n    \"parameters\": [\"item\"],\n    \"body\": [\n      {\n        \"operator\": \"push\",\n        \"target\": { \"$ref\": \"#/state/cart\" },\n        \"value\": { \"$ref\": \"$args/item\" }\n      }\n    ]\n  }\n}\n```\n\n## Statements, expressions, or a body string?\n\nThe escalation ladder continues inside Shape 4 — prefer the least powerful form:\n\n| Form                          | Use when                                                                                      |\n| ----------------------------- | --------------------------------------------------------------------------------------------- |\n| A single `$expression`        | One operation — toggle, increment, push, a computed value                                     |\n| `body` as a statement array   | Multiple steps, branching, or dispatching events — still no code                              |\n| `body` as a JavaScript string | Loops, `await` chains, try/catch, browser APIs — anything structural statements can't express |\n\nA statement body stays fully analyzable and visually editable; a source string is opaque to tooling. Reach for the string only when structure genuinely runs out.\n\n## How it works\n\nOne engine serves both halves: an interpreter (`runStatements`) executes statement arrays directly against the reactive `state` proxy, and a compiler (`compileStatements`) emits the genuine ECMAScript forms — `if`/`else` statements, a `switch` over the discriminant's string form, and `dispatchEvent(new CustomEvent(type, init))`. The emitted function is identical in shape to a hand-written handler, so [reactivity](/docs/framework/concepts/reactivity) tracking works unchanged.\n\nStatements execute sequentially. A step whose value is a promise is awaited before the next step runs — async/await semantics without writing `await`.\n\n## Rules\n\n- A `body` is either a statement array or a source string — one representation per entry.\n- Branch tests (`if`) and `$switch` discriminants are **pure** operands: no mutations, no `call` to a mutating entry.\n- `$switch` matches `String(discriminant)` against `cases` keys, because JSON keys are strings.\n- Statements run in order; thenable results are awaited before the next step.\n- There is no capture field — assign a `call`'s result with an `=` statement.\n- `dispatchEvent` needs a dispatch target; outside a handler (no `event`), a compiled custom element dispatches from the component instance.\n- Declare dispatched events in `emits` — it is the declaration tooling reads.\n\n## Related\n\n- [Expressions](/docs/framework/concepts/expressions) — the nodes statements are built from\n- [Functions and sidecars](/docs/framework/concepts/functions) — the entries that own a `body`\n- [Switching](/docs/framework/concepts/switching) — `$switch` at the element level\n- [Statements in Studio](/docs/studio/logic/statements) — the visual step editor\n- [Code editing in Studio](/docs/studio/logic/code) — the JavaScript escape hatch\n",
    "section": "Framework",
    "slug": "framework/concepts/statements",
    "title": "Statements",
    "url": "https://jxsuite.com/docs/framework/concepts/statements/"
  },
  {
    "description": "Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.",
    "markdown": "\n# Functions and sidecars\n\n> **Studio writes this format for you.** The Monaco editor behind every function body ([Code editing](/docs/studio/logic/code)) reads and writes these entries — this page documents the JSON and the JavaScript contract around it.\n\nA Function entry is a `state` entry with `$prototype: \"Function\"` — the top rung of the escalation ladder, where logic becomes real JavaScript. Its code lives either inline in a `body` or in an external `.js` **sidecar** file named by `$src`. Prefer [expressions](/docs/framework/concepts/expressions) and [statements](/docs/framework/concepts/statements) first; reach for a function when structure runs out.\n\nThe smallest complete function — an inline handler:\n\n```json\n{\n  \"state\": {\n    \"count\": 0,\n    \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" }\n  }\n}\n```\n\nBind it to an event like any handler: `\"onclick\": { \"$ref\": \"#/state/increment\" }`.\n\n## Inline handlers\n\nA `body` string is a raw function body. `state` is always in scope; `arguments` names any additional parameters — an event handler names `event`:\n\n```json\n{\n  \"handleInput\": {\n    \"$prototype\": \"Function\",\n    \"arguments\": [\"event\"],\n    \"body\": \"state.value = event.target.value\"\n  }\n}\n```\n\n## Inline computed values\n\nA function with only a `body` (no `arguments`) that returns a value acts as a computed — the framework wraps it in `computed()` when it detects the entry is referenced reactively:\n\n```json\n{\n  \"titleClass\": {\n    \"$prototype\": \"Function\",\n    \"body\": \"return state.score >= 90 ? 'gold' : 'silver'\"\n  }\n}\n```\n\n## External sidecars\n\nWhen a function outgrows a string, move it to a `.js` file and point `$src` at it. Each entry resolves to the named export matching its key (override with `$export`); npm specifiers work too:\n\n```json\n{\n  \"state\": {\n    \"increment\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" },\n    \"decrement\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" },\n    \"validateEmail\": {\n      \"$prototype\": \"Function\",\n      \"$src\": \"npm:@myorg/validators\",\n      \"$export\": \"validateEmail\"\n    }\n  }\n}\n```\n\n```js\nexport function increment(state) {\n  state.count++;\n}\nexport function decrement(state) {\n  state.count = Math.max(0, state.count - 1);\n}\n```\n\nWhen several entries share a `$src`, the module is imported once and its named exports extracted; module caching is automatic.\n\n## Structured bodies\n\nA `body` may also be a JSON array of statements instead of a source string — multi-step logic that stays inspectable and visually editable. That form has its own page: [Statements](/docs/framework/concepts/statements).\n\n## State access from JavaScript\n\nInside `body` strings and sidecar files, `state` is the component's reactive scope — a proxy over every declared state entry and function. Read and write it directly; there are no `.get()`/`.set()` calls:\n\n```js\n// Read\nconst current = state.count;\n\n// Write\nstate.count = current + 1;\n\n// Mutate arrays in place — mutations are tracked\nstate.items.push(newItem);\nstate.items.splice(0, 1);\n\n// Nested objects are tracked too\nstate.user.name = \"Alice\";\n```\n\nEvery write triggers the bindings that read that value — see [Reactivity](/docs/framework/concepts/reactivity). `this` is never used in Jx-managed code; all component access goes through `state`.\n\n## Declaring the interface\n\nOptional metadata makes a function legible to tooling:\n\n| Property      | Description                                                          |\n| ------------- | -------------------------------------------------------------------- |\n| `arguments`   | Parameter names as plain strings (after the implicit `state`)        |\n| `parameters`  | CEM-compatible parameter objects — richer alternative to `arguments` |\n| `returns`     | JSON Schema describing the return value                              |\n| `emits`       | CEM `Event` objects this function dispatches                         |\n| `description` | Documentation string, surfaced in Studio's completions               |\n\n## How it works\n\nAt runtime, the scope builder recognizes the `$prototype: \"Function\"` shape and turns each entry into a callable on the reactive scope. Exports and bodies are invoked with `state` as their first argument; event bindings pass the DOM event second — `(state, event)`. A body-only function referenced from a reactive position is wrapped in `computed()` instead, so it re-evaluates when the state it reads changes.\n\nFunctions marked `timing: \"server\"` are a separate mechanism — a plain `$src`/`$export` entry with no `$prototype`, executed across the RPC boundary. See [Timing](/docs/framework/concepts/timing).\n\n## Rules\n\n- `body` and `$src` are mutually exclusive — declaring both is a compile-time error.\n- The first parameter is always `state`; `arguments`/`parameters` name only what follows it.\n- `this` is never used. All component state goes through the `state` proxy.\n- `$export` defaults to the entry's key name; sidecar exports must be named exports.\n- Function entries use `camelCase` names, like all state entries.\n- Only `$prototype: \"Function\"` may point `$src` at a `.js` file — other prototypes require a `.class.json` (see [Data prototypes](/docs/framework/concepts/data-prototypes)).\n\n## Related\n\n- [Expressions](/docs/framework/concepts/expressions) — the declarative rung below functions\n- [Statements](/docs/framework/concepts/statements) — structured bodies without JavaScript\n- [Timing](/docs/framework/concepts/timing) — server functions and the RPC boundary\n- [Components](/docs/framework/concepts/components) — where state and functions are declared\n- [Code editing in Studio](/docs/studio/logic/code) — the editor for bodies and sidecars\n",
    "section": "Framework",
    "slug": "framework/concepts/functions",
    "title": "Functions and sidecars",
    "url": "https://jxsuite.com/docs/framework/concepts/functions/"
  },
  {
    "description": "Inline styles, nested CSS selectors, and named media breakpoints in Jx.",
    "markdown": "\n# Styling\n\n> **Studio writes this format for you.** The [Design](/docs/studio/design) inspector and Stylebook produce everything below — this page documents the style model for hand-editing and reference.\n\nJx uses JSON objects for styling with camelCase CSS property names, nested selectors, and named media breakpoints.\n\n## Inline Styles\n\nThe `style` property accepts a JSON object:\n\n```json\n{\n  \"tagName\": \"div\",\n  \"style\": {\n    \"backgroundColor\": \"blue\",\n    \"marginTop\": \"10px\",\n    \"fontSize\": \"16px\",\n    \"display\": \"flex\"\n  }\n}\n```\n\n## Nested CSS Selectors\n\nKeys beginning with `:`, `.`, `&`, or `[` are treated as nested selectors:\n\n```json\n{\n  \"style\": {\n    \"backgroundColor\": \"blue\",\n    \":hover\": {\n      \"backgroundColor\": \"darkblue\",\n      \"cursor\": \"pointer\"\n    },\n    \".child\": { \"color\": \"white\" },\n    \"&.active\": { \"outline\": \"2px solid white\" }\n  }\n}\n```\n\nInline properties apply directly to the element. Nested rules are emitted as a scoped `<style>` block using a generated `data-jx` attribute selector.\n\n## Named Media Breakpoints\n\nDeclare breakpoints at root level with `$media`:\n\n```json\n{\n  \"$media\": {\n    \"--sm\": \"(min-width: 640px)\",\n    \"--md\": \"(min-width: 768px)\",\n    \"--lg\": \"(min-width: 1024px)\",\n    \"--dark\": \"(prefers-color-scheme: dark)\"\n  }\n}\n```\n\nUse `@--name` keys in any style object:\n\n```json\n{\n  \"style\": {\n    \"fontSize\": \"14px\",\n    \"@--md\": { \"fontSize\": \"16px\" },\n    \"@--dark\": { \"color\": \"#ccc\" },\n    \"@(min-width: 1280px)\": { \"fontSize\": \"18px\" }\n  }\n}\n```\n\n`@--name` references named breakpoints. `@(condition)` is a literal inline media query.\n\n## Color-Scheme Variants\n\nA `$media` entry whose value is exactly a `prefers-color-scheme` query (like `--dark` above) is a _scheme query_: its `@--dark` blocks respond both to the OS preference and to a visitor-forced scheme, and the compiler wires up `color-scheme` and no-flash persistence automatically. See [Color schemes](/docs/framework/concepts/color-schemes) for the full contract and how to build a switcher.\n\n## Static Style Extraction\n\nThe compiler extracts all static `style` definitions into a single `<style>` block in the document `<head>`, producing clean, efficient CSS output.\n\n## Design Tokens with CSS Custom Properties\n\nDefine tokens in your `project.json` `style` block and use them everywhere:\n\n```json\n{\n  \"style\": {\n    \":root\": {\n      \"--color-primary\": \"#3b82f6\",\n      \"--color-surface\": \"#ffffff\",\n      \"--font-sans\": \"Inter, system-ui, sans-serif\"\n    }\n  }\n}\n```\n\nComponents reference tokens with standard `var()`:\n\n```json\n{\n  \"style\": {\n    \"color\": \"var(--color-primary)\",\n    \"fontFamily\": \"var(--font-sans)\"\n  }\n}\n```\n\nCSS custom properties cascade naturally through the DOM — every component can use them without importing anything.\n",
    "section": "Framework",
    "slug": "framework/concepts/styling",
    "title": "Styling",
    "url": "https://jxsuite.com/docs/framework/concepts/styling/"
  },
  {
    "description": "Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.",
    "markdown": "\n# Color schemes\n\nA Jx design can ship light and dark variants of its tokens, follow the visitor's OS preference by default, and still let the visitor force either scheme with a switcher. The whole mechanism builds on [named media breakpoints](/docs/framework/concepts/styling#named-media-breakpoints) — no separate theme system.\n\n## Declaring scheme support\n\nAdd a **scheme query** to `$media` in `project.json` — an entry whose value is exactly a `prefers-color-scheme` query:\n\n```json\n{\n  \"$media\": {\n    \"--md\": \"(min-width: 768px)\",\n    \"--dark\": \"(prefers-color-scheme: dark)\"\n  }\n}\n```\n\nThen override any design token (or any style property) per scheme with an `@--dark` block. Convention: author the light values as the base and the dark values as overrides.\n\n```json\n{\n  \"style\": {\n    \"--color-bg\": \"#ffffff\",\n    \"--color-text\": \"#18181b\",\n    \"@--dark\": {\n      \"--color-bg\": \"#0a0a0a\",\n      \"--color-text\": \"#fafafa\"\n    }\n  }\n}\n```\n\n`@--dark` blocks work in component and element styles too, exactly like responsive breakpoints:\n\n```json\n{\n  \"tagName\": \"div\",\n  \"style\": {\n    \"boxShadow\": \"0 2px 8px rgba(0,0,0,0.12)\",\n    \"@--dark\": { \"boxShadow\": \"0 2px 8px rgba(0,0,0,0.5)\" }\n  }\n}\n```\n\n## What declaring a scheme query does\n\nDeclaring a scheme query opts the site into the forced-scheme contract:\n\n- Every `@--dark` block is emitted twice: once inside `@media (prefers-color-scheme: dark)` (applies in **auto** mode), and once under `:root[data-color-scheme=\"dark\"]` (applies when the scheme is **forced**). Both copies are specificity-neutral, so your cascade is unchanged.\n- `color-scheme: light dark` is declared on `:root`, with forced-mode overrides — native form controls and scrollbars follow along.\n- A tiny inline script is injected at the top of `<head>` that restores the visitor's persisted choice before first paint, so a forced scheme never flashes.\n\n## The visitor override contract\n\nTwo constants make up the contract:\n\n| Constant            | Where                 | Meaning                                                 |\n| ------------------- | --------------------- | ------------------------------------------------------- |\n| `data-color-scheme` | attribute on `<html>` | `\"light\"` or `\"dark\"` forces that scheme; absent = auto |\n| `jx-color-scheme`   | `localStorage` key    | persisted forced scheme; absent = auto                  |\n\nA switcher only needs to keep the two in sync. Cycling auto → light → dark:\n\n```js\nconst next = { auto: \"light\", light: \"dark\", dark: \"auto\" }[current];\nif (next === \"auto\") {\n  localStorage.removeItem(\"jx-color-scheme\");\n  document.documentElement.removeAttribute(\"data-color-scheme\");\n} else {\n  localStorage.setItem(\"jx-color-scheme\", next);\n  document.documentElement.setAttribute(\"data-color-scheme\", next);\n}\n```\n\nThe pre-paint script (injected automatically) reads the storage key on the next load, so the choice sticks across pages and visits.\n\n:::doc-note\nOnly _pure_ scheme queries participate: `(prefers-color-scheme: dark)` with nothing else attached. A compound query like `(prefers-color-scheme: dark) and (min-width: 768px)` compiles as a plain media query and does not respond to the forced attribute.\n:::\n\n:::doc-tip\nStudio shows an Auto/Light/Dark preview toggle in the tab bar whenever the project declares a scheme query, and the design token editor can author the `@--dark` values for you.\n:::\n",
    "section": "Framework",
    "slug": "framework/concepts/color-schemes",
    "title": "Color schemes",
    "url": "https://jxsuite.com/docs/framework/concepts/color-schemes/"
  },
  {
    "description": "Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.",
    "markdown": "\n# Lists and iteration\n\n> **Studio writes this format for you.** Converting an element with **Repeat…** and binding its data source ([Repeaters](/docs/studio/design/repeaters)) writes the Array pseudo-elements on this page.\n\nA dynamic list is an **Array pseudo-element**: an object with `$prototype: \"Array\"` sitting inside a `children` array. It names the data (`items`) and a template (`map`) rendered once per item. The list re-renders automatically whenever the data changes.\n\n```json\n{\n  \"tagName\": \"ul\",\n  \"children\": [\n    {\n      \"$prototype\": \"Array\",\n      \"items\": { \"$ref\": \"#/state/todoList\" },\n      \"map\": { \"tagName\": \"li\", \"textContent\": { \"$ref\": \"$map/item\" } }\n    }\n  ]\n}\n```\n\n## The iteration context\n\nInside the `map` template, the `$map/` [reference scheme](/docs/framework/concepts/references) reads the current iteration:\n\n| Reference                  | Resolves to                          |\n| -------------------------- | ------------------------------------ |\n| `{ \"$ref\": \"$map/item\" }`  | The current array item               |\n| `{ \"$ref\": \"$map/index\" }` | The current zero-based integer index |\n\nDeeper paths reach into item fields — `\"$map/item/title\"` — and template strings inside the template can read the same context as `${$map.item.title}` or `${$map.index}`:\n\n```json\n{\n  \"$prototype\": \"Array\",\n  \"items\": { \"$ref\": \"#/state/posts\" },\n  \"map\": {\n    \"tagName\": \"article\",\n    \"children\": [{ \"tagName\": \"h2\", \"textContent\": { \"$ref\": \"$map/item/title\" } }]\n  }\n}\n```\n\nThe `map` template is an ordinary element def, so `attributes`, `id`, `className`, `style`, and event handlers all work there and can read the iteration context — which is how a list row gets a per-item link or a selected state:\n\n```json\n{\n  \"$prototype\": \"Array\",\n  \"items\": { \"$ref\": \"#/state/posts\" },\n  \"map\": {\n    \"tagName\": \"a\",\n    \"id\": \"post-${index}\",\n    \"attributes\": {\n      \"href\": \"${item.url}\",\n      \"class\": \"row ${index === state.active ? 'is-active' : ''}\"\n    },\n    \"textContent\": \"${item.title}\"\n  }\n}\n```\n\n## Mixing with sibling elements\n\nThe Array object is a _member_ of `children`, so it can sit among ordinary siblings — a static header row followed by a dynamic list, for example. It renders **wrapper-less**: mapped items become direct children of the parent element, with no container in between.\n\n```json\n{\n  \"tagName\": \"ul\",\n  \"children\": [\n    { \"tagName\": \"li\", \"textContent\": \"Header\" },\n    {\n      \"$prototype\": \"Array\",\n      \"items\": { \"$ref\": \"#/state/todoList\" },\n      \"map\": { \"tagName\": \"li\", \"textContent\": { \"$ref\": \"$map/item\" } }\n    }\n  ]\n}\n```\n\n## Filtering and sorting\n\n`filter` and `sort` reference [functions](/docs/framework/concepts/functions) declared in `state`. The filter function receives each item and returns true to keep it; the sort function receives two items and returns a number, like a standard comparator:\n\n```json\n{\n  \"$prototype\": \"Array\",\n  \"items\": { \"$ref\": \"#/state/allItems\" },\n  \"filter\": { \"$ref\": \"#/state/isVisible\" },\n  \"sort\": { \"$ref\": \"#/state/sortByDate\" },\n  \"map\": { \"tagName\": \"list-item\", \"item\": { \"$ref\": \"$map/item\" } }\n}\n```\n\nFiltering and sorting never mutate the source array — they shape what renders.\n\n## How it works\n\nThe runtime places an invisible anchor where the Array object sits, then renders the mapped items inline ahead of it. The whole render runs inside a reactive effect: when `items` (or a filter or sort dependency) changes, the previous generation of item nodes and their bindings is disposed and the list re-renders in place. Each item's template renders in a child scope carrying `$map` — the surrounding document's `state` remains fully visible inside the template.\n\n## Rules\n\n- The Array object must have `$prototype: \"Array\"`, an `items` source, and a `map` template.\n- `items` must resolve to an array — a state entry, a [data prototype](/docs/framework/concepts/data-prototypes) such as a content collection, or a literal array.\n- The list renders wrapper-less; give structure a container by making the _parent_ the container element (`ul`, `tbody`, a grid `div`).\n- `$map/` references are valid only inside the `map` template.\n- `filter` and `sort` must be `$ref`s to functions in `state`.\n- The legacy form where `children` is _itself_ the Array object is still accepted; Studio normalizes it to a single array member on load.\n\n## Related\n\n- [References](/docs/framework/concepts/references) — the `$map/` scheme and resolution order\n- [State](/docs/framework/concepts/state) — declaring the arrays lists iterate\n- [Data prototypes](/docs/framework/concepts/data-prototypes) — collections and requests as `items` sources\n- [Content collections](/docs/framework/site/content-collections) — site content as list data\n- [Repeaters](/docs/studio/design/repeaters) — the Studio surface that writes this format\n",
    "section": "Framework",
    "slug": "framework/concepts/lists",
    "title": "Lists and iteration",
    "url": "https://jxsuite.com/docs/framework/concepts/lists/"
  },
  {
    "description": "The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.",
    "markdown": "\n# Data prototypes\n\n> **Studio writes this format for you.** The source editors in the State panel ([Data sources](/docs/studio/logic/data-sources)) generate these entries field by field — this page documents the underlying model.\n\nA `$prototype` entry declares where a state value comes from instead of what it is. The prototype names a class — a built-in Web-API wrapper, a content loader, or your own — and the entry's remaining keys are its configuration. The resolved value is reactive: when the source changes, everything bound to it updates.\n\nThe smallest complete data prototype — an HTTP request whose URL tracks another state value:\n\n```json\n{\n  \"state\": {\n    \"userData\": {\n      \"$prototype\": \"Request\",\n      \"url\": \"/api/users/\",\n      \"urlParams\": { \"$ref\": \"#/state/userId\" },\n      \"method\": \"GET\"\n    }\n  }\n}\n```\n\n## Built-in Web-API prototypes\n\nThese resolve automatically, with no imports or `$src`, and each maps to a genuine Web API:\n\n- `Request` — HTTP fetch with reactive URL parameters, debouncing, and manual mode.\n- `LocalStorage` — a persistent key-value entry that survives closing the browser.\n- `SessionStorage` — the same, scoped to the visit.\n- `Cookie` — a cookie with `maxAge`, `path`, `domain`, `secure`, and `sameSite`.\n- `IndexedDB` — a browser database store with indexes and CRUD helpers.\n- `FormData` — form fields assembled for submission.\n- `URLSearchParams` — a computed query string.\n- `Set` / `Map` — reactive wrappers over the corresponding collections.\n- `Blob` — binary data from parts and a MIME type.\n- `Array` — a reactive array driving a [mapped list](/docs/framework/concepts/lists).\n\nField-level documentation for each source lives in [Data sources](/docs/studio/logic/data-sources). A storage entry is as small as:\n\n```json\n{\n  \"theme\": { \"$prototype\": \"LocalStorage\", \"key\": \"theme\", \"default\": \"light\" }\n}\n```\n\n## Content prototypes\n\nContent loaders are built in too, and resolve at build time (`timing: \"compiler\"` — see [Timing](/docs/framework/concepts/timing)):\n\n- `MarkdownFile` — parses one `.md` file into frontmatter and a content tree.\n- `MarkdownCollection` — globs and parses many `.md` files.\n- `ContentCollection` — a schema-validated, multi-format content source.\n- `ContentEntry` — a single entry within a collection.\n\n```json\n{\n  \"posts\": {\n    \"$prototype\": \"MarkdownCollection\",\n    \"src\": \"./content/posts/*.md\",\n    \"timing\": \"compiler\"\n  }\n}\n```\n\nThese names map internally to `.class.json` implementations shipped with Jx — no configuration needed.\n\n## External classes\n\nAny other prototype name is an **external class**. Its `$src` must point to a `.class.json` file — a JSON Schema document describing the class's parameters, fields, and methods, optionally delegating to a JS module via `$implementation`:\n\n```json\n{\n  \"forecast\": {\n    \"$prototype\": \"WeatherForecast\",\n    \"$src\": \"./lib/WeatherForecast.class.json\",\n    \"location\": \"Lancaster, PA\",\n    \"days\": 5\n  }\n}\n```\n\nThe class contract is small: the constructor receives one configuration object (the entry minus reserved keys), and the value resolves through `instance.resolve()` (async), then `instance.value`, then the instance itself. A class may also expose `subscribe`/`unsubscribe` for push updates, and its methods may declare `returns` schemas so tooling knows, for example, that instances can feed mapped iteration. Authoring your own `.class.json` classes is covered in depth in the Extending section.\n\n## Import maps\n\nTo avoid repeating `$src` on every entry, declare a top-level `imports` map from prototype names to `.class.json` paths:\n\n```json\n{\n  \"imports\": {\n    \"WeatherForecast\": \"@acme/weather/WeatherForecast.class.json\",\n    \"GeoLocation\": \"./lib/GeoLocation.class.json\"\n  },\n  \"state\": {\n    \"forecast\": { \"$prototype\": \"WeatherForecast\", \"location\": \"Lancaster, PA\" },\n    \"coords\": { \"$prototype\": \"GeoLocation\", \"address\": \"123 Main St\" }\n  }\n}\n```\n\n`imports` in `site.json` cascade to every page; page-level entries win on collision, and an explicit `imports` entry may even override a built-in prototype mapping.\n\n## How it works\n\nThe scope builder injects any import-mapped `$src`, constructs the class with the entry's configuration, and wraps the resolved value in a reactive reference automatically. Resolution order for `$src` is: explicit `$src` → page `imports` → site `imports` → built-in mappings → unknown-prototype warning. Configuration values that are `$ref`s are reactive — a `Request` whose `urlParams` reference state re-fetches when that state changes. The `timing` field decides where resolution happens: in the browser, on the server, or at build time — see [Timing](/docs/framework/concepts/timing).\n\n## Rules\n\n- A non-Function `$src` **must** point to a `.class.json` file — direct `.js` sources are for `$prototype: \"Function\"` only (see [Functions and sidecars](/docs/framework/concepts/functions)).\n- Reserved keys (`$prototype`, `$src`, `$export`, `timing`, `default`, `description`) are never passed to the constructor.\n- Import-map values must end in `.class.json`; anything else is warned about and skipped.\n- An entry's own `$src` always beats the import map.\n- Built-in prototypes need no `imports` entry and are unaffected by the map unless explicitly overridden.\n\n## Related\n\n- [Timing](/docs/framework/concepts/timing) — client, server, and compiler resolution\n- [State](/docs/framework/concepts/state) — the grammar prototypes live inside\n- [Lists](/docs/framework/concepts/lists) — iterating over a source's resolved array\n- [Reactivity](/docs/framework/concepts/reactivity) — how resolved values propagate\n- [Data sources in Studio](/docs/studio/logic/data-sources) — every field, source by source\n- [Data explorer in Studio](/docs/studio/logic/data-explorer) — inspecting resolved values live\n",
    "section": "Framework",
    "slug": "framework/concepts/data-prototypes",
    "title": "Data prototypes",
    "url": "https://jxsuite.com/docs/framework/concepts/data-prototypes/"
  },
  {
    "description": "Where Jx data entries resolve: in the browser, on the server across the RPC boundary, or at build time with values baked into the HTML.",
    "markdown": "\n# Timing: client, server, compiler\n\n> **Studio writes this format for you.** The **Timing** field on a data source's editor ([Data sources](/docs/studio/logic/data-sources)) sets the values below — this page documents what each one means.\n\nThe `timing` key on a state entry declares **where** its value is resolved. There are three values, and each moves the work to a different machine:\n\n| Value        | When it resolves                                          |\n| ------------ | --------------------------------------------------------- |\n| `\"client\"`   | At runtime, in the visitor's browser (the default)        |\n| `\"server\"`   | At runtime, on the server, called over an RPC boundary    |\n| `\"compiler\"` | At build time — the result is baked into the emitted HTML |\n\nThe smallest complete server-timed entry — a function that runs on the server, whose return value lands in state:\n\n```json\n{\n  \"state\": {\n    \"metrics\": {\n      \"$src\": \"./dashboard.server.js\",\n      \"$export\": \"fetchMetrics\",\n      \"timing\": \"server\"\n    }\n  }\n}\n```\n\n## Client\n\n`\"client\"` is the default — omit `timing` and the entry resolves in the browser. All the Web-API [data prototypes](/docs/framework/concepts/data-prototypes) (`Request`, `LocalStorage`, `IndexedDB`, …) are client-timed unless told otherwise.\n\n## Server: the RPC function boundary\n\n`timing: \"server\"` designates a cross-process function call. The entry names an async export in a server-side module via `$src` and `$export` — no `$prototype` is used. The function receives `(args, env)`: the caller's arguments object, and the platform's environment bindings (Cloudflare Workers `env`, a Node `process.env` wrapper, …):\n\n```js\nexport async function fetchMetrics(args, env) {\n  const db = env.DB; // e.g. a Cloudflare D1 binding\n  const { data } = await db.prepare(\"SELECT * FROM metrics\").all();\n  return data;\n}\n```\n\nA function that needs no bindings simply ignores the second parameter.\n\n### Arguments\n\nAn optional `arguments` field passes named parameters — a single object, not a positional list. Values may be static or reactive `$ref`s; any reactive value makes the call re-run when it changes:\n\n```json\n{\n  \"metrics\": {\n    \"$src\": \"./dashboard.server.js\",\n    \"$export\": \"fetchMetrics\",\n    \"timing\": \"server\",\n    \"arguments\": {\n      \"userId\": { \"$ref\": \"#/state/userId\" },\n      \"filter\": \"active\"\n    }\n  }\n}\n```\n\n### The security boundary\n\nServer credentials stay in the server process. The `env` parameter gives the function its platform bindings — databases, KV namespaces, secrets, email workers — and none of it crosses to the client: the browser receives only the function's serialized return value.\n\n:::doc-warning\nThe boundary is enforced by the **compiled** output. During development, the runtime may execute a server entry client-side (falling back to the dev server's proxy when the module can't load in a browser), so don't treat dev behavior as proof that a secret is hidden — build and deploy to exercise the real boundary.\n:::\n\n## Compiler\n\n`timing: \"compiler\"` resolves the entry during the build. The result is baked into the emitted HTML and the entry is stripped from the shipped state — visitors download the finished value, not the machinery that produced it. This is the natural timing for content that changes only when you rebuild, and the content prototypes (`MarkdownFile`, `MarkdownCollection`, `ContentCollection`) use it by design:\n\n```json\n{\n  \"posts\": {\n    \"$prototype\": \"MarkdownCollection\",\n    \"src\": \"./content/posts/*.md\",\n    \"timing\": \"compiler\"\n  }\n}\n```\n\n## How it works\n\nFor each `timing: \"server\"` entry, the [compiler](/docs/framework/build) emits two artifacts: a client-side `POST /_jx/server/<export>` fetch that stores the JSON response in a signal (wrapped in an effect when any argument is reactive), and a server-side Hono handler that imports the export from `$src` and serves that route.\n\nWhen `build.adapter` is set in `project.json`, every server entry across the whole site is collected, deduplicated by export name, and bundled into a single worker (`dist/_worker.js` plus a `_routes.json` limiting invocation to `/_jx/*` on Cloudflare Pages). Without an adapter, a standalone per-document handler is generated instead. During development the [dev server](/docs/framework/build/dev-server) stands in for both.\n\nCompiler-timed entries never produce runtime artifacts at all — the site build resolves them, bakes the values into the static HTML, and strips the entries from the document.\n\n## Rules\n\n- `\"client\"` is the default whenever `timing` is absent.\n- A server entry uses `$src` + `$export` with **no** `$prototype`, and the export must be an async named export.\n- The server function's signature is `(args, env)`; `arguments` is one named-values object.\n- A reactive `$ref` in `arguments` makes the RPC call reactive.\n- `env` and everything reachable through it stay server-side; only the return value is serialized to the browser.\n- `timing: \"compiler\"` values are fixed at build time — rebuild the site to refresh them.\n\n## Related\n\n- [Data prototypes](/docs/framework/concepts/data-prototypes) — the entries `timing` applies to\n- [Functions and sidecars](/docs/framework/concepts/functions) — client-side functions by contrast\n- [The build](/docs/framework/build) — where server bundling and baking happen\n- [Dev server](/docs/framework/build/dev-server) — server timing during development\n- [Data sources in Studio](/docs/studio/logic/data-sources) — the Timing field in the editor\n",
    "section": "Framework",
    "slug": "framework/concepts/timing",
    "title": "Timing: client, server, compiler",
    "url": "https://jxsuite.com/docs/framework/concepts/timing/"
  },
  {
    "description": "Swap whole components in and out of the page from state with $switch and cases — client-side views, wizards, and routing in pure JSON.",
    "markdown": "\n# Dynamic switching\n\n> **Studio writes this format for you.** There is no dedicated panel for `$switch` yet — add it in [Code mode](/docs/studio/logic/code) and the canvas renders whichever case is active.\n\nA `$switch` node swaps what renders in one spot based on a state value: the node binds `$switch` to a [reference](/docs/framework/concepts/references), and `cases` maps each possible value to what should render. When the state changes, the old case is torn down and the new one renders in its place — view switching, wizard steps, or a client-side router in pure JSON.\n\n```json\n{\n  \"tagName\": \"main\",\n  \"children\": [\n    {\n      \"$switch\": { \"$ref\": \"#/state/currentRoute\" },\n      \"cases\": {\n        \"home\": { \"$ref\": \"./views/home.json\" },\n        \"about\": { \"$ref\": \"./views/about.json\" },\n        \"profile\": { \"$ref\": \"./views/profile.json\" }\n      }\n    }\n  ]\n}\n```\n\n## External cases\n\nA case value that is a `$ref` to a `.json` file loads that [document](/docs/framework/concepts/documents) on demand — a case is fetched only when its key first becomes active, so unvisited views cost nothing up front. Each external case is an independent component with its own `state` scope.\n\n## Inline cases\n\nA case value may also be an inline element definition. Inline cases render in the surrounding document's scope, so they can bind its state directly:\n\n```json\n{\n  \"$switch\": { \"$ref\": \"#/state/status\" },\n  \"cases\": {\n    \"loading\": { \"tagName\": \"p\", \"textContent\": \"Loading…\" },\n    \"error\": { \"tagName\": \"p\", \"textContent\": \"${state.errorMessage}\" },\n    \"success\": { \"$ref\": \"./views/results.json\" }\n  }\n}\n```\n\n## Driving the switch\n\nThe selector is ordinary state, so anything that writes state switches the view — an event handler, an [expression](/docs/framework/concepts/expressions), or a data source:\n\n```json\n{\n  \"state\": { \"currentRoute\": \"home\" },\n  \"children\": [\n    {\n      \"tagName\": \"button\",\n      \"textContent\": \"About\",\n      \"onclick\": {\n        \"$expression\": {\n          \"operator\": \"=\",\n          \"target\": { \"$ref\": \"#/state/currentRoute\" },\n          \"value\": \"about\"\n        }\n      }\n    }\n  ]\n}\n```\n\n## How it works\n\nThe runtime renders the `$switch` node as a container element (its `tagName`, or `div` when none is given) and watches the bound reference inside a reactive effect. On every change it clears the container and renders the matching case: inline cases render immediately with the current scope; external cases are fetched, their scope is built, and the result is appended — with stale responses discarded if the key changed again mid-load. A key with no matching case renders nothing.\n\n## Rules\n\n- `$switch` must be a `$ref` — typically `#/state/...`; a literal value is meaningless and does not render.\n- Case keys are matched against the resolved value as strings; there is no default case — an unmatched value renders an empty container.\n- External cases have isolated scope; to share data with a case, make it an inline element, or lift shared state to `window#/` globals.\n- Switching fully tears down the outgoing case — its DOM and reactive bindings are disposed, and per-component state re-initializes on the next visit.\n- For URL-driven pages prefer file-based [routing](/docs/framework/site/routing); `$switch` is for switching _within_ a page.\n\n## Related\n\n- [References](/docs/framework/concepts/references) — the `$ref` schemes `$switch` binds\n- [State](/docs/framework/concepts/state) — declaring the selector value\n- [Props and scope](/docs/framework/concepts/props-and-scope) — why external cases are isolated\n- [Routing](/docs/framework/site/routing) — URL-based page selection for sites\n- [Code editing](/docs/studio/logic/code) — the Studio surface for authoring `$switch`\n",
    "section": "Framework",
    "slug": "framework/concepts/switching",
    "title": "Dynamic switching",
    "url": "https://jxsuite.com/docs/framework/concepts/switching/"
  },
  {
    "description": "Where Jx runs document code, why compiled sites need no unsafe-eval, and how table permissions, sessions, and secrets are enforced on the server.",
    "markdown": "\n# Security\n\nA Jx site has two security surfaces. The **evaluation surface** comes with the format: a document contains executable code — `${}` templates and function `body`/`$src` entries — so where that code runs decides the posture. The **data and session boundary** appears only once a site has a database or sign-ins, and decides what the deployed worker will do on a visitor's behalf.\n\n## Compiled sites run no eval\n\n`jx build` produces plain HTML/CSS plus per-island ES modules. It does **not** emit `new Function` or `eval` — a `${}` template is spliced verbatim into an emitted module as a real template literal, and expressions and statements lower to genuine JavaScript. So a compiled static or island page runs under a **strict Content-Security-Policy with no `'unsafe-eval'`**. This is enforced by a compiler test (`packages/compiler/tests/no-eval.test.ts`).\n\nShip compiled output to production and the eval requirement disappears — that is the main reason the compiler exists.\n\n## The interpreter needs `'unsafe-eval'`\n\nThe interpreting runtime — the dev server, the Studio canvas, and `@jxsuite/runtime` used directly as a library — compiles `${}` templates and inline `body` functions with `new Function` on the fly. Any page that hosts the interpreter must allow `'unsafe-eval'` in its CSP.\n\n:::doc-warning\n`${}` templates are **full JavaScript**, not a sandbox. A template has the component's `state` in scope, but also the entire global environment, and it can assign or call side effects. Do not render a template built from untrusted input in the interpreting runtime.\n:::\n\n## Treat documents as code\n\nA Jx document is executable input. Loading and rendering an untrusted document in the interpreter runs its code; **compiling** an untrusted document runs its code at build time (template text becomes code in the bundle). Give a `.json` document the same trust you would give a `.js` file from the same source.\n\nIf you compile documents you did not author — user-submitted content merged into the tree, for example — sanitize or escape `${` sequences first.\n\n## The data and session boundary\n\nA site with a database and sign-ins has a second security surface: the `/_jx/*` routes the [generated worker](/docs/framework/site/deployment) serves. Nothing there trusts the browser.\n\n**Every table declares its own rules.** A `data` table's `permissions` carry one rule per action — `read`, `insert`, `update`, `delete` — each of them `public`, `none`, `authenticated`, `owner`, or `role:<name>`. The defaults are read `public` and every write `none`, so a table you have just declared is readable by anyone and writable by no one until you say otherwise. Rules are evaluated on the server for every request; there is no client-side check to bypass.\n\n**Authorization fails closed.** Anything beyond `public` and `none` needs the auth extension: it mounts first and publishes the session hooks the data mount authorizes against. Without it those rules deny — a missing auth mount answers 401, never a silent grant. The session lookup itself is fail-closed too: an unreachable auth backend reads as signed out rather than as trusted.\n\n**Ownership cannot be forged.** When a table declares an `ownerField`, the server stamps that column with the signed-in user's id on every session-granted insert, and scopes `owner` reads and writes to rows that match — the value in the request body is not consulted. Roles work the same way: they live in the `user` table's `role` column, which sign-up input can never set.\n\n**Secrets stay names.** `project.json` records env-var _names_ (`urlEnv`, `secretEnv`, `clientIdEnv`), never values. Values reach code only through the server's `env` — from the git-ignored `.dev.vars` locally, from your host's secret store in production — which is also why a `timing: \"server\"` entry is the right home for anything holding a credential. See [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\n:::doc-warning\nA table with `insert: \"public\"` is an open write endpoint: anyone on the internet can post rows to it, and there is no rate limiting or CAPTCHA in the contract today. Prefer `authenticated` inserts unless you knowingly want a public drop-box.\n:::\n\nThe extension-author side of the same model — what a mount may and may not do — is [Security and secrets](/docs/extending/extensions/security).\n\n## The dev server's network controls\n\nThe dev server binds loopback by default and gates its remote-code-execution routes (`/__jx_resolve__`, `/__jx_server__`), extension mounts, and the whole `/__studio/*` API behind an Origin/Host check, with realpath path containment. See [Dev server internals](/docs/extending/embedding/dev-server) and `@jxsuite/server` §4.2 for the full model.\n\n## Related\n\n- [References](/docs/framework/concepts/references) — `$ref` binding vs. `${}` templates\n- [Dev server internals](/docs/extending/embedding/dev-server) — the network security model\n- [Auth and secrets](/docs/studio/data/auth-and-secrets) — sessions, roles, and where secret values live\n- [Data tables](/docs/studio/data/tables) — where the permission rules are declared\n- [Security and secrets](/docs/extending/extensions/security) — the same boundary from an extension author's side\n",
    "section": "Framework",
    "slug": "framework/concepts/security",
    "title": "Security",
    "url": "https://jxsuite.com/docs/framework/concepts/security/"
  },
  {
    "description": "How a Jx site is laid out on disk: pages, layouts, components, content collections, public assets, and the project.json that ties them together.",
    "markdown": "\n# Site architecture\n\n> **Studio manages this structure for you** — [Browse your project](/docs/studio/projects/browse) maps to `pages/`, `components/`, and `content/`; the [content-type builder](/docs/studio/projects/content-types) writes the `content` section; [Project settings](/docs/studio/projects/settings) edits `project.json`. This section documents the on-disk layout for reference.\n\nA Jx site is a folder of plain JSON and Markdown files with a conventional layout. Only `project.json` and `pages/` are required — everything else is optional and additive.\n\n## Project anatomy\n\n```\nmy-site/\n├── project.json           # Site configuration (required)\n├── pages/                 # File-based routing (required)\n│   ├── index.json         # → /\n│   ├── about.json         # → /about\n│   └── blog/\n│       ├── index.json     # → /blog\n│       └── [slug].json    # → /blog/:slug (dynamic)\n├── layouts/               # Shared page shells\n│   └── base.json\n├── components/            # Reusable Jx components\n├── content/               # Content collections\n│   └── blog/\n│       └── hello-world.md\n├── data/                  # Static data files\n├── public/                # Static assets (copied verbatim)\n└── dist/                  # Build output (generated)\n```\n\n| Directory     | Purpose                                                        | Required  |\n| ------------- | -------------------------------------------------------------- | --------- |\n| `pages/`      | File-based routing. Each page file becomes a route.            | **Yes**   |\n| `layouts/`    | Layout components, referenced by pages via `$layout`.          | No        |\n| `components/` | Reusable components, referenced via `$ref` or `$elements`.     | No        |\n| `content/`    | Content collections with schema validation.                    | No        |\n| `data/`       | Static data files loaded at build time. No schema enforcement. | No        |\n| `public/`     | Static assets copied verbatim to `dist/`. No processing.       | No        |\n| `dist/`       | Build output. Ignored by git.                                  | Generated |\n\nFiles and directories whose names start with `_` inside `pages/` are excluded from routing, so components can live next to the pages that use them (`pages/blog/_blog-card.json`).\n\n## Configuration\n\n`project.json` at the project root is the only required configuration file. It names the site, sets the default layout and language, declares global `<head>` entries, breakpoints, and design tokens, and holds the `content`, `redirects`, `copy`, and `build` sections — plus any section an enabled extension contributes: `connections` and `data` for databases, `auth` for visitor accounts, `search` for the build-time search index. Site-level `state` and `$defs` cascade into every page. See [project.json](/docs/framework/site/project-json).\n\n## Routing\n\nEvery file in `pages/` becomes a route automatically: `pages/about.json` serves `/about`, `pages/blog/[slug].json` is a dynamic route with a `slug` parameter, and `pages/docs/[...path].json` catches everything under `/docs/`. Dynamic pages declare the concrete paths they generate with `$paths` — usually by pointing at a content collection. See [Routing](/docs/framework/site/routing).\n\n## Layouts\n\nLayouts are ordinary Jx documents that provide the shared page shell — navigation, footer, and the `<slot>` elements where page content lands. Pages opt in with `$layout`, or inherit the site default. Named slots, nesting, and state merging follow the same rules as custom elements. See [Layouts](/docs/framework/site/layouts).\n\n## Content collections\n\nThe `content` section of `project.json` turns folders of Markdown, JSON, or CSV files into typed, queryable collections with JSON Schema validation. Pages query them with `ContentCollection` and `ContentEntry` state entries, and schema `$ref`s link entries across collections — see [Content collections](/docs/framework/site/content-collections) and [Relationships](/docs/framework/site/relationships).\n\n## Markdown pages and content\n\nMarkdown is a first-class authoring format: content entries and even whole pages (`pages/index.md`) can be written in Jx Markdown, with frontmatter for metadata and directives for embedding components. See [Jx Markdown](/docs/framework/site/jx-markdown).\n\n## SEO and metadata\n\nPages declare `<head>` entries with `$head`, merged in a fixed order with layout- and site-level entries; titles, canonical URLs, sitemaps, and structured data are handled at build time. See [SEO and metadata](/docs/framework/site/seo).\n\n## Images\n\nImages referenced from pages and content are optimized during the build — resized to multiple widths and re-encoded to modern formats, configured by the `images` section of `project.json`. See [Images](/docs/framework/site/images).\n\n## Redirects\n\nThe `redirects` map in `project.json` declares old-URL-to-new-URL rules, including `:param` patterns and per-rule HTTP status codes. See [Redirects](/docs/framework/site/redirects).\n\n## Databases, accounts, and server functions\n\nNot everything a site shows has to be a file. With the `@jxsuite/connector` extension enabled, `connections` names the databases the site talks to and `data` declares the tables inside them, served over `/_jx/data`; with `@jxsuite/auth`, the `auth` section gives visitors accounts and sessions at `/_jx/auth` and unlocks the table permission rules that depend on knowing who is asking. Separately, any `state` entry marked `timing: \"server\"` compiles into its own route at `/_jx/server/<export>`, so secrets and privileged calls stay off the client. The connection-backed sections require a server-capable `build.adapter` — the build stops without one — and server functions need somewhere to run for the same reason. See [Databases](/docs/studio/data), [Auth and secrets](/docs/studio/data/auth-and-secrets), and [Timing](/docs/framework/concepts/timing).\n\n## Building and deploying\n\n`bunx jx build` discovers routes, compiles each page, and emits static HTML, CSS, and minimal JS into `dist/` — deployable to any static host. With `build.adapter` set, the same command also bundles the site's server tier — `timing: \"server\"` functions and extension mounts such as `/_jx/data` and `/_jx/auth` — into one self-contained worker. Pages are prerendered either way; the worker only answers `/_jx/*` and, on some adapters, serves the static files. See [the build pipeline](/docs/framework/build) and [Deployment](/docs/framework/site/deployment); Studio itself never runs this step — it [commits and pushes your source](/docs/studio/publish), and your host builds on push.\n",
    "section": "Framework",
    "slug": "framework/site",
    "title": "Site architecture",
    "url": "https://jxsuite.com/docs/framework/site/"
  },
  {
    "description": "Every key in a Jx site's project.json — identity, defaults, head entries, style tokens, content types, redirects, build — and what cascades into pages.",
    "markdown": "\n# project.json\n\n> **Studio writes this file for you** — [Project settings](/docs/studio/projects/settings) edits the identity, defaults, and build keys; the [content-type builder](/docs/studio/projects/content-types) writes `content`; the [design-token editor](/docs/studio/design/tokens) writes `style`.\n\n`project.json` at the project root is the only required configuration file in a Jx site. A minimal real one (abbreviated from jxsuite.com):\n\n```json\n{\n  \"$schema\": \"./project.schema.json\",\n  \"name\": \"Jx Suite\",\n  \"url\": \"https://jxsuite.com\",\n  \"defaults\": { \"layout\": \"./layouts/base.json\", \"lang\": \"en\" },\n  \"build\": { \"outDir\": \"./dist\", \"trailingSlash\": \"always\" },\n  \"extensions\": [\"@jxsuite/parser\"]\n}\n```\n\n## Key reference\n\n| Key          | Type     | Purpose                                                                                  |\n| ------------ | -------- | ---------------------------------------------------------------------------------------- |\n| `name`       | `string` | Site name — the default `<title>` and `$site.name`                                       |\n| `url`        | `string` | Production URL, used for canonical URLs and the sitemap                                  |\n| `defaults`   | `object` | `layout`, `lang`, and `charset` applied to every page                                    |\n| `$head`      | `array`  | Global `<head>` entries injected into every page                                         |\n| `$media`     | `object` | Named breakpoints available in every style object                                        |\n| `style`      | `object` | Design tokens (`--` keys) and global element rules                                       |\n| `state`      | `object` | Site-wide state, merged read-only into every page                                        |\n| `$defs`      | `object` | Shared JSON Schema type definitions for the whole project                                |\n| `content`    | `object` | Content types — see [Content collections](/docs/framework/site/content-collections)      |\n| `imports`    | `object` | `$prototype` name → class path, cascaded to all pages                                    |\n| `extensions` | `array`  | Extension packages providing formats, sections, and classes                              |\n| `redirects`  | `object` | Source path → destination — see [Redirects](/docs/framework/site/redirects)              |\n| `copy`       | `object` | Extra source → destination file mappings copied into the build output                    |\n| `build`      | `object` | `outDir`, `trailingSlash`, `adapter` — see [Deployment](/docs/framework/site/deployment) |\n| `images`     | `object` | Image optimization settings — see [Images](/docs/framework/site/images)                  |\n\nEnabled extensions add sections of their own alongside these. The first-party ones:\n\n| Key           | Type     | Contributed by       | Purpose                                                                                    |\n| ------------- | -------- | -------------------- | ------------------------------------------------------------------------------------------ |\n| `connections` | `object` | `@jxsuite/connector` | Named database connections — see [Connections](/docs/studio/data/connections)              |\n| `data`        | `object` | `@jxsuite/connector` | Dynamic tables served over `/_jx/data` — see [Data tables](/docs/studio/data/tables)       |\n| `auth`        | `object` | `@jxsuite/auth`      | Visitor accounts and sessions — see [Auth and secrets](/docs/studio/data/auth-and-secrets) |\n| `search`      | `object` | `@jxsuite/search`    | Build-time search index — see [Search](/docs/framework/site/search)                        |\n\n## Identity and defaults\n\n`name` and `url` identify the site: `name` is the fallback `<title>` and is exposed to every page as `$site.name`; `url` anchors canonical URLs and sitemap entries. `defaults.layout` wraps every page that doesn't declare its own `$layout` (a page can opt out with `\"$layout\": false`), and `defaults.lang` sets `<html lang>` site-wide.\n\n## Global head entries\n\n`$head` entries are injected into every page's `<head>`, before layout- and page-level entries:\n\n```json\n{\n  \"$head\": [\n    { \"tagName\": \"link\", \"attributes\": { \"rel\": \"icon\", \"href\": \"/favicon.svg\" } },\n    {\n      \"tagName\": \"meta\",\n      \"attributes\": { \"name\": \"viewport\", \"content\": \"width=device-width, initial-scale=1\" }\n    }\n  ]\n}\n```\n\nA page's own `$head` appends to (and, for singletons like `<title>`, overrides) the site-level entries. The full merge order is covered in [SEO and metadata](/docs/framework/site/seo).\n\n## Breakpoints and style tokens\n\n`$media` names media queries once so every component can respond to `\"@--md\"` without redeclaring the query. `style` holds the global stylesheet: `--`-prefixed keys compile to `:root {}` custom properties, plain camelCase properties style the page root, and nested objects are element selectors applied site-wide. Declaring a scheme query like `\"--dark\": \"(prefers-color-scheme: dark)\"` additionally enables the visitor-forced [color-scheme contract](/docs/framework/concepts/color-schemes) — `@--dark` token overrides in `style` become the dark variant of the design.\n\n```json\n{\n  \"$media\": { \"--md\": \"(max-width: 768px)\", \"--sm\": \"(max-width: 640px)\" },\n  \"style\": {\n    \"--color-accent\": \"#3b82f6\",\n    \"--radius\": \"8px\",\n    \"fontFamily\": \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n    \"backgroundColor\": \"var(--color-bg-primary)\",\n    \"table\": { \"width\": \"100%\", \"borderCollapse\": \"collapse\" }\n  }\n}\n```\n\nEvery component in the project can reference `var(--color-accent)` and use `\"@--md\": { … }` overrides without importing anything. See [Styling](/docs/framework/concepts/styling) for the style object format and [Design tokens](/docs/studio/design/tokens) for the Studio editor.\n\n## Site state and shared types\n\n`state` declares site-wide data. Every page receives it two ways: merged directly into the page's own `state` (the page wins when it declares the same key), and under the read-only `$site` entry — `$site.name`, `$site.url`, plus each site state key.\n\n```json\n{\n  \"state\": {\n    \"siteName\": \"My Site\",\n    \"socialLinks\": [{ \"label\": \"GitHub\", \"url\": \"https://github.com/example\" }]\n  }\n}\n```\n\n`$defs` holds reusable [JSON Schema type definitions](/docs/framework/concepts/documents) — shapes for API responses, CMS payloads, or shared value types — that any document in the project can reference, the project-wide counterpart of a document's own `$defs`.\n\n## Content types\n\nThe `content` section defines content collections — folders of Markdown, JSON, or CSV entries with a schema (abbreviated from jxsuite.com, which publishes these docs from the repo's `docs/` folder):\n\n```json\n{\n  \"content\": {\n    \"docs\": {\n      \"source\": \"../../docs\",\n      \"format\": \"Markdown\",\n      \"$elements\": [{ \"$ref\": \"./components/doc-note.json\" }],\n      \"schema\": { \"type\": \"object\", \"required\": [\"title\"] }\n    }\n  }\n}\n```\n\nThe keys and query prototypes are covered in [Content collections](/docs/framework/site/content-collections).\n\n## Redirects\n\n`redirects` maps old paths to new ones — a plain string for a 301, or an object to pick the status:\n\n```json\n{\n  \"redirects\": {\n    \"/docs/get-studio\": \"/docs/start/install/\",\n    \"/legacy/post/:slug\": { \"destination\": \"/blog/:slug\", \"status\": 301 }\n  }\n}\n```\n\nPatterns and status codes are covered in [Redirects](/docs/framework/site/redirects).\n\n## Copying extra files\n\n`copy` maps files from anywhere in the repository into the build output — useful for publishing artifacts that live outside `public/`:\n\n```json\n{\n  \"copy\": {\n    \"../../packages/schema/project-schema.json\": \"schema/project/v1/index.json\"\n  }\n}\n```\n\nSources resolve relative to the project root; destinations are paths inside `dist/`. The copies run after `public/` assets, so a mapping can overwrite a public file of the same name.\n\n## Build\n\n`build` controls output: `outDir` (default `./dist`), `trailingSlash`, an optional deployment `adapter`, and sitemap generation. See [the build pipeline](/docs/framework/build) and [Deployment](/docs/framework/site/deployment).\n\n## Extensions and imports\n\n`extensions` lists extension packages (bare package names or relative paths) that contribute format classes such as `Markdown` and `Csv`, project sections such as `content`, and `$prototype` classes. `imports` maps a `$prototype` name to a class file for all pages at once; a page-level `imports` entry wins on collision. See [Data prototypes](/docs/framework/concepts/data-prototypes) and [Dependencies and imports](/docs/studio/projects/dependencies).\n\n## Extension-contributed sections\n\nAn extension can own a whole top-level key. Listing the package in `extensions` is what turns that key on: `jx schema` composes each enabled extension's schema fragment into `project.schema.json`, so an unlisted extension's section is an unknown key that `jx validate` rejects. Re-run `jx schema` whenever the list changes.\n\n```json\n{\n  \"extensions\": [\"@jxsuite/parser\", \"@jxsuite/connector\", \"@jxsuite/auth\"],\n  \"connections\": {\n    \"main\": { \"provider\": \"sqlite\" }\n  },\n  \"data\": {\n    \"comments\": {\n      \"connection\": \"main\",\n      \"permissions\": { \"read\": \"public\", \"insert\": \"authenticated\", \"update\": \"owner\" },\n      \"ownerField\": \"author_id\",\n      \"schema\": {\n        \"type\": \"object\",\n        \"properties\": { \"message\": { \"type\": \"string\" }, \"author_id\": { \"type\": \"string\" } },\n        \"required\": [\"message\"]\n      }\n    }\n  },\n  \"auth\": {\n    \"connection\": \"main\",\n    \"methods\": { \"emailPassword\": true },\n    \"redirects\": { \"afterSignIn\": \"/\", \"afterSignOut\": \"/\" }\n  },\n  \"build\": { \"adapter\": \"cloudflare-pages\" }\n}\n```\n\nTwo rules run through all of them:\n\n- **Names, never values.** Sections carry identifiers and env-var _names_ (`urlEnv`, `secretEnv`, `clientIdEnv`); the secret values live in the git-ignored `.dev.vars` locally and your host's secret store in production. See [Auth and secrets](/docs/studio/data/auth-and-secrets).\n- **A section served at runtime needs an adapter.** `connections`/`data` and `auth` are served by extension mounts under `/_jx/`, so `build.adapter` must name a server-capable target — the build fails on static. See [Build output and adapters](/docs/framework/site/deployment).\n\nStudio writes these sections for you from _Settings > Connections_, _Data Tables_, and _Authentication_ — see [Databases](/docs/studio/data).\n\n## What documents inherit\n\nSite-level declarations cascade into every page automatically — no imports needed:\n\n- `$head` entries are prepended to every page's `<head>`.\n- `$media` breakpoints and `style` tokens are available in every component.\n- `defaults.lang` sets `<html lang>` everywhere.\n- `state` merges into page state (page wins) and is exposed as `$site`.\n- `imports` and `$elements` merge with page-level entries (page wins on collision).\n\nEverything else is deliberate: files in `data/` load via an explicit [`$ref`](/docs/framework/concepts/references), collection data requires `ContentCollection` or `ContentEntry` declarations, and components receive outside data only through [`$props`](/docs/framework/concepts/props-and-scope) — scope never leaks across a component boundary.\n",
    "section": "Framework",
    "slug": "framework/site/project-json",
    "title": "project.json",
    "url": "https://jxsuite.com/docs/framework/site/project-json/"
  },
  {
    "description": "File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.",
    "markdown": "\n# Routing\n\nEvery file in the `pages/` directory automatically becomes a route — there is no routing configuration. `.json` pages are native; other extensions become routable when an enabled extension claims them, which is how jxsuite.com serves `pages/index.md` as its front page (see [Jx Markdown](/docs/framework/site/jx-markdown)).\n\n## Static routes\n\nThe file path determines the URL path:\n\n| File                         | URL                |\n| ---------------------------- | ------------------ |\n| `pages/index.json`           | `/`                |\n| `pages/about.json`           | `/about`           |\n| `pages/about/index.json`     | `/about`           |\n| `pages/blog/index.json`      | `/blog`            |\n| `pages/blog/first-post.json` | `/blog/first-post` |\n\n`index` files map to their parent directory, so `pages/about.json` and `pages/about/index.json` produce the same URL — use whichever keeps the folder tidy, but not both.\n\nFiles and directories whose names start with `_` are excluded from routing entirely. That's the convention for co-locating components next to the pages that use them: `pages/blog/_blog-card.json` is available for `$ref` but never becomes `/blog/_blog-card`.\n\n## Dynamic routes\n\nBracket syntax in filenames creates parameterized routes:\n\n| File                         | URL pattern      | Example match               |\n| ---------------------------- | ---------------- | --------------------------- |\n| `pages/blog/[slug].json`     | `/blog/:slug`    | `/blog/hello-world`         |\n| `pages/[category]/[id].json` | `/:category/:id` | `/products/42`              |\n| `pages/docs/[...path].json`  | `/docs/*`        | `/docs/api/runtime/install` |\n\n`[param]` matches exactly one path segment. `[...param]` is a catch-all: it matches the whole remaining path, slashes included — one catch-all page can serve an entire subtree.\n\n## Generating pages with $paths\n\nA static build has to know every URL in advance, so a dynamic page declares which paths it generates with a top-level `$paths`. The most common shape drives the route from a [content collection](/docs/framework/site/content-collections) — this is (abbreviated) how the docs page you are reading is generated, from `pages/docs/[...slug].json`:\n\n```json\n{\n  \"$layout\": \"./layouts/docs.json\",\n  \"$paths\": { \"contentType\": \"docs\", \"param\": \"slug\" },\n  \"state\": {\n    \"page\": {\n      \"$prototype\": \"ContentEntry\",\n      \"contentType\": \"docs\",\n      \"id\": { \"$ref\": \"#/$params/slug\" }\n    }\n  },\n  \"children\": [{ \"tagName\": \"article\", \"children\": \"${state.page.$children ?? []}\" }]\n}\n```\n\nThe compiler iterates `$paths` at build time and emits one HTML page per entry, substituting each entry's value into the route pattern. A dynamic page without `$paths` generates nothing (the build warns and skips it).\n\n`$paths` takes three shapes:\n\n```json\n{ \"contentType\": \"blog\", \"param\": \"slug\", \"field\": \"id\" }\n```\n\nOne page per collection entry. `param` names the route parameter (default `slug`); `field` picks which entry field supplies the value (default `id`, the entry id).\n\n```json\n{ \"values\": [\"en\", \"fr\", \"de\"], \"param\": \"lang\" }\n```\n\nAn explicit list of values.\n\n```json\n{ \"$ref\": \"./data/products.json\", \"param\": \"id\", \"field\": \"sku\" }\n```\n\nA JSON array file, one page per item, with `field` selecting the property to use.\n\nFor catch-all routes the parameter value may itself contain slashes: nested content entries get path-based ids (like `framework/site/routing`), so a single `[...slug]` page fans out into the whole tree of URLs.\n\n:::doc-tip\nYour project's generated `document.schema.json` validates `$paths` against exactly these shapes plus any your extensions contribute, so a misspelled key or a source belonging to an extension you have not enabled is flagged in the editor and by `jx validate` — rather than building zero pages and warning at the end of the log. Run [`jx schema`](/docs/framework/build/cli) after changing `extensions` to keep that set current.\n:::\n\n## Route priority\n\nWhen several routes could match the same URL, the more specific one wins:\n\n1. Static routes beat dynamic routes — `/about` beats `/[slug]`.\n2. Named parameters beat catch-alls — `/blog/[slug]` beats `/blog/[...path]`.\n3. More specific paths beat less specific ones — `/blog/[slug]` beats `/[...path]`.\n\n## Params at runtime\n\nInside a dynamic page, the current route's parameters are addressable as `#/$params/<name>`. The usual pattern binds a parameter into a `ContentEntry` id, as in the example above:\n\n```json\n{ \"id\": { \"$ref\": \"#/$params/slug\" } }\n```\n\nEach generated page resolves the reference to its own concrete value — `/blog/hello-world` sees `slug` as `\"hello-world\"`. The compiler also injects a read-only `$page` state entry carrying `params`, `title`, and `url`, alongside the site-level `$site` context (see [project.json](/docs/framework/site/project-json)). In static builds all of this resolves at compile time; nothing route-related ships to the browser.\n\n:::doc-tip\nIn Studio, opening a dynamic page shows a params picker in the tab bar so the canvas previews a real entry instead of a placeholder.\n:::\n\n## Related\n\n- [Content collections](/docs/framework/site/content-collections) — the entries that drive `$paths`\n- [Layouts](/docs/framework/site/layouts) — the shells dynamic pages render into\n- [Deployment](/docs/framework/site/deployment) — how generated routes are served\n",
    "section": "Framework",
    "slug": "framework/site/routing",
    "title": "Routing",
    "url": "https://jxsuite.com/docs/framework/site/routing/"
  },
  {
    "description": "Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.",
    "markdown": "\n# Layouts\n\n> **Studio writes this format for you** — layouts appear alongside pages and components in [Browse your project](/docs/studio/projects/pages-layouts-components), and you edit them on the same canvas.\n\nA layout is an ordinary Jx document that provides the shell shared across pages — navigation, footer, wrappers — with HTML `<slot>` elements marking where page content is injected. It is the same slot mechanism [components](/docs/framework/concepts/components) use, run at compile time instead of DOM time.\n\n## Layout documents\n\nA minimal layout registers its chrome components via `$elements` (paths relative to the layout file) and places one default `<slot>`:\n\n```json\n{\n  \"$elements\": [{ \"$ref\": \"../components/site-header.json\" }],\n  \"tagName\": \"html\",\n  \"children\": [\n    {\n      \"tagName\": \"body\",\n      \"children\": [\n        { \"tagName\": \"site-header\" },\n        { \"tagName\": \"main\", \"children\": [{ \"tagName\": \"slot\" }] }\n      ]\n    }\n  ]\n}\n```\n\nA layout doesn't have to own `<html>` — jxsuite.com's docs layout is a flex `<div>` with a sidebar; the compiler still wraps the result in a full HTML document with the merged `<head>`.\n\n## Declaring a layout\n\nPages opt in with a top-level `$layout`, resolved from the **project root** (not the page's folder):\n\n```json\n{\n  \"$layout\": \"./layouts/base.json\",\n  \"children\": [{ \"tagName\": \"h1\", \"textContent\": \"About us\" }]\n}\n```\n\n- If a page omits `$layout`, the site default from `project.json` `defaults.layout` applies.\n- `\"$layout\": false` renders the page with no layout at all — useful for standalone landing pages and embeds.\n\nThe page's `children` are distributed into the layout's `<slot>` positions when the page compiles.\n\n## Named slots\n\nLayouts can define several regions by naming their slots:\n\n```json\n{\n  \"tagName\": \"body\",\n  \"children\": [\n    {\n      \"tagName\": \"aside\",\n      \"children\": [{ \"tagName\": \"slot\", \"attributes\": { \"name\": \"sidebar\" } }]\n    },\n    { \"tagName\": \"main\", \"children\": [{ \"tagName\": \"slot\" }] }\n  ]\n}\n```\n\nPages target a named slot with the standard `slot` attribute; children without one go to the default slot:\n\n```json\n{\n  \"$layout\": \"./layouts/docs.json\",\n  \"children\": [\n    {\n      \"tagName\": \"nav\",\n      \"attributes\": { \"slot\": \"sidebar\" },\n      \"children\": [{ \"tagName\": \"a\", \"href\": \"/docs/intro\", \"textContent\": \"Intro\" }]\n    },\n    { \"tagName\": \"article\", \"children\": [{ \"tagName\": \"h1\", \"textContent\": \"Documentation\" }] }\n  ]\n}\n```\n\nPer the HTML spec, a `<slot>`'s own children are fallback content — they render only when the page supplies nothing for that slot.\n\n## Nesting\n\nA layout may declare its own `$layout`, composing shells:\n\n```json\n{\n  \"$layout\": \"./layouts/base.json\",\n  \"children\": [\n    {\n      \"tagName\": \"aside\",\n      \"children\": [{ \"tagName\": \"slot\", \"attributes\": { \"name\": \"sidebar\" } }]\n    },\n    { \"tagName\": \"article\", \"children\": [{ \"tagName\": \"slot\" }] }\n  ]\n}\n```\n\nSaved as `layouts/blog-post.json`, this wraps blog pages in blog chrome while inheriting the site shell from `base.json`. Inner layouts resolve first, so the page's content threads through every level.\n\n## What merges\n\nCompiling a page against its layout produces one document:\n\n- **Children** — page children are distributed into the layout's slots.\n- **State** — the layout's `state` is kept and the page's is merged over it; **the page wins** on any shared key. Layouts can carry real state of their own — jxsuite.com's docs layout loads the sidebar nav with a `ContentEntry`.\n- **`$media`, `style`, `attributes`** — merged the same way, page over layout.\n- **`$head`** — merged site → layout → page, later entries winning for singletons like `<title>`; see [SEO and metadata](/docs/framework/site/seo).\n\nLayouts also receive the compiler-injected contexts: `$page` (`title`, `url`, `params`) and `$site` (`name`, `url`, plus site state) — see [project.json](/docs/framework/site/project-json).\n\n## Related\n\n- [Routing](/docs/framework/site/routing) — which pages exist and how they resolve\n- [Components](/docs/framework/concepts/components) — the runtime slot mechanism layouts reuse\n- [SEO and metadata](/docs/framework/site/seo) — the `$head` merge in detail\n",
    "section": "Framework",
    "slug": "framework/site/layouts",
    "title": "Layouts",
    "url": "https://jxsuite.com/docs/framework/site/layouts/"
  },
  {
    "description": "The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.",
    "markdown": "\n# Content collections\n\n> **Studio writes this format for you** — the [content-type builder](/docs/studio/projects/content-types) edits the `content` section visually, and entries show up in [Browse your project](/docs/studio/projects/browse) with schema-driven forms.\n\nContent collections are the data layer for content-driven sites: they turn folders of Markdown, JSON, or CSV files into typed, queryable data with JSON Schema validation. Each collection is a **content type**, declared in the `content` section of `project.json`.\n\nFormats other than JSON come from extension packages, so a site using Markdown or CSV content enables the parser in `project.json`: `\"extensions\": [\"@jxsuite/parser\"]`.\n\n## Defining a content type\n\n```json\n{\n  \"content\": {\n    \"blog\": {\n      \"source\": \"./content/blog/\",\n      \"format\": \"Markdown\",\n      \"schema\": {\n        \"type\": \"object\",\n        \"properties\": { \"title\": { \"type\": \"string\" }, \"pubDate\": { \"type\": \"string\" } },\n        \"required\": [\"title\", \"pubDate\"]\n      }\n    }\n  }\n}\n```\n\nEach definition takes four keys:\n\n- **`source`** — a directory of entry files, a single file containing many entries (one CSV or JSON file), or an `https://` URL. jxsuite.com's `docs` type points outside the project entirely (`\"source\": \"../../docs\"`) to publish this documentation from the repo.\n- **`format`** — the name of a format class provided by an enabled extension (`\"Markdown\"`, `\"Csv\"`); `\"json\"` is the only built-in. When omitted, the format is derived from the source file's extension; directory and remote sources require an explicit `format`, and remote sources need a format that supports remote loading (such as `Csv`).\n- **`$elements`** — components available inside entries as directives, e.g. jxsuite.com registers `doc-note` and `doc-tip` for these docs. See [Jx Markdown](/docs/framework/site/jx-markdown).\n- **`schema`** — a JSON Schema every entry's data must satisfy.\n\n## Entry ids\n\nEvery entry has an `id`, derived from the filesystem:\n\n- **Flat directories** — the filename without extension: `content/blog/hello-world.md` → `hello-world`.\n- **Nested directories** — a path-based id with `/` separators: `docs/framework/site/routing.md` → `framework/site/routing`, with a trailing `/index` stripped. Path-based ids pair naturally with `[...param]` catch-all routes — see [Routing](/docs/framework/site/routing).\n- **JSON files** — an array file yields one entry per item (each item's `id` field, or an indexed fallback); an object file is a single entry named after the file.\n\n## Media beside your content\n\nEntries reference images the way any markdown editor expects — relative to the file itself:\n\n```markdown\n![A diagram of the pipeline](./images/diagram.png)\n```\n\nKeep the images in the collection directory (`content/blog/images/diagram.png`), and the entry reads correctly in VS Code, Obsidian, GitHub, and on the built site alike. When the collection loads, that reference is remapped to the collection's own URL — `/content/blog/images/diagram.png` — which the build copies into `dist/` and the dev server serves straight from the source file. It works even when the source lives outside the project, which is how these docs ship their screenshots from `docs/images/`.\n\nOnly files an entry actually references are published; unreferenced siblings and the entry files themselves never reach `dist/`.\n\n:::doc-note\nRemapping applies to element `src`/`poster` values and to frontmatter fields your schema declares as `\"format\": \"uri-reference\"` (that's also what gives Studio a [media picker](/docs/studio/projects/media) for the field). A path that doesn't resolve to a real file beside the entry is left exactly as written, with a build warning naming the entry — so root-relative paths into `public/` keep working unchanged.\n:::\n\n## Querying with ContentCollection\n\nPages read a collection through a `state` entry with `$prototype: \"ContentCollection\"`:\n\n```json\n{\n  \"state\": {\n    \"posts\": {\n      \"$prototype\": \"ContentCollection\",\n      \"contentType\": \"blog\",\n      \"filter\": { \"draft\": false },\n      \"sort\": { \"field\": \"pubDate\", \"order\": \"desc\" },\n      \"limit\": 10\n    }\n  }\n}\n```\n\n- **`filter`** — either a shorthand object (each key must equal its value, as above) or an array of rules: `{ \"field\": \"tags\", \"op\": \"contains\", \"value\": \"intro\" }`. Operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `not contains`, `empty`, `not empty`. Use `\"field\": \"id\"` to match the entry id.\n- **`sort`** — a rule or array of rules, `{ \"field\": \"pubDate\", \"order\": \"desc\" }`; `asc` is the default.\n- **`limit`** — maximum number of entries.\n\nThe result is an array of entries, rendered with a [repeater](/docs/framework/concepts/lists):\n\n```json\n{\n  \"tagName\": \"ul\",\n  \"children\": {\n    \"$prototype\": \"Array\",\n    \"of\": { \"$ref\": \"#/state/posts\" },\n    \"map\": { \"tagName\": \"li\", \"textContent\": \"${item.data.title}\" }\n  }\n}\n```\n\n## Single entries with ContentEntry\n\n`$prototype: \"ContentEntry\"` fetches one entry by id — usually bound to a route parameter:\n\n```json\n{\n  \"state\": {\n    \"post\": {\n      \"$prototype\": \"ContentEntry\",\n      \"contentType\": \"blog\",\n      \"id\": { \"$ref\": \"#/$params/slug\" }\n    }\n  }\n}\n```\n\nBy default `id` matches the entry id; set `\"field\"` to match a data field instead (e.g. `\"field\": \"sku\"` to look up a product by SKU).\n\nA resolved entry has this shape:\n\n```json\n{\n  \"id\": \"hello-world\",\n  \"data\": { \"title\": \"Hello World\", \"pubDate\": \"2024-01-15\" },\n  \"body\": \"# Hello\\n\\nThis is my first post.\",\n  \"$children\": [{ \"tagName\": \"h1\", \"textContent\": \"Hello\" }]\n}\n```\n\nFrontmatter and data fields live under `data` (`${state.post.data.title}`); for Markdown entries, `body` is the raw source and `$children` is the parsed Jx tree, rendered with `\"children\": \"${state.post.$children ?? []}\"`.\n\nMarkdown headings in `$children` carry automatic anchor `id`s — the heading text lowercased, punctuation stripped, spaces hyphenated, with `-2`, `-3` suffixes deduplicating repeats in document order. The entry's table of contents (`_meta.toc`: `depth`, `text`, `id` per heading) uses the same ids, so TOC links, search results, and hand-written `#fragment` URLs all land on the rendered section.\n\nFenced code blocks in `$children` arrive syntax-highlighted: recognized languages become token spans carrying `--shiki-light`/`--shiki-dark` color variables that follow the site's [color scheme](/docs/framework/concepts/color-schemes). See [Jx Markdown](/docs/framework/site/jx-markdown) for the language set.\n\n## Schema validation\n\nEvery entry is validated against its content type's `schema` when collections load — at build time and on the dev server. Missing required fields and type mismatches are reported with the content type and entry id, so a bad frontmatter key fails loudly instead of rendering an empty spot. The same schema drives Studio's [frontmatter forms](/docs/studio/editing/frontmatter) and the content-type builder's field editor.\n\n## Relationships\n\nA schema field can reference another content type:\n\n```json\n{ \"author\": { \"$ref\": \"#/content/authors\" } }\n```\n\nAn entry then stores the target's id (`author: jane-doe` in frontmatter), and loading resolves it to the full entry — templates read `${state.post.data.author.data.name}`. Array fields whose `items` carry the `$ref` are to-many. Resolution rules, editing support, and modeling patterns are covered in [Relationships](/docs/framework/site/relationships).\n\n## Related\n\n- [Routing](/docs/framework/site/routing) — generating one page per entry with `$paths`\n- [Jx Markdown](/docs/framework/site/jx-markdown) — the entry format for prose content\n- [project.json](/docs/framework/site/project-json) — where the `content` section lives\n",
    "section": "Framework",
    "slug": "framework/site/content-collections",
    "title": "Content collections",
    "url": "https://jxsuite.com/docs/framework/site/content-collections/"
  },
  {
    "description": "Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.",
    "markdown": "\n# Jx Markdown\n\n> **Studio writes this format for you.** The [writing surface](/docs/studio/editing/writing) and [frontmatter panel](/docs/studio/editing/frontmatter) edit `.md` files visually — this page documents the syntax for when you hand-edit one or read a diff.\n\nA Jx Markdown file is a Markdown document that transpiles to the same JSON structure the compiler and runtime consume everywhere else. It combines three layers:\n\n1. **YAML frontmatter** — top-level document properties (`tagName`, `state`, `style`, …)\n2. **Directives** — explicit elements using [remark-directive](https://github.com/remarkjs/remark-directive) syntax\n3. **Standard Markdown** — headings, paragraphs, lists, and links mapped to HTML elements\n\nMarkdown support is opt-in: the project must enable the parser extension in `project.json` (`\"extensions\": [\"@jxsuite/parser\"]`). There is no implicit `.md` handling without it.\n\n## A minimal component\n\n```markdown\n---\ntagName: my-greeting\nstate:\n  name:\n    type: string\n    default: World\n---\n\n:::div\n\n# Hello, ${state.name}!\n\n:::\n```\n\nThe frontmatter declares the document schema; the body declares the element tree. A `.md` file whose frontmatter has a `tagName` containing a hyphen is a component; a file without one is a content document (a blog post, a docs page) that produces a plain element tree. Any content file can add component schema later without changing how it's processed.\n\n## Frontmatter\n\nAll top-level Jx document keys go in the YAML frontmatter, `$` prefixes included: `tagName`, `$id`, `state`, `$media`, `$defs`, `$elements`, `$layout`, `$paths`, `$handlers`, `imports`, `observedAttributes`. Any other keys pass through to the document unchanged — which is how pages set `title` or `$head`.\n\n## Directives\n\nThree directive forms cover the element tree:\n\n**Container directives** wrap children. Outer containers use _more_ colons than inner ones (minimum three), and the closing fence matches the opening count:\n\n```markdown\n::::section{className=\"hero\"}\n:::h1\nWelcome\n:::\n::::\n```\n\n**Leaf directives** (two colons) are self-closing:\n\n```markdown\n::img{src=\"/photo.jpg\" alt=\"A photo\"}\n```\n\n**Text directives** (one colon) sit inline within prose:\n\n```markdown\nClick :a[here]{href=\"/about\"} for details.\n```\n\nThe directive name is the element's `tagName` — any HTML tag or any registered custom element.\n\n## Attributes\n\nAttributes use the HTML-like `{key=\"value\"}` syntax. Three characters can't start a directive attribute key — `$`, `:`, and `@` — so Jx defines drop-the-prefix conventions that the transpiler reverses:\n\n| You write                                                      | The document gets                                                        |\n| -------------------------------------------------------------- | ------------------------------------------------------------------------ |\n| `prototype`, `ref`, `component`, `props`, `switch`, `elements` | `$prototype`, `$ref`, `$component`, `$props`, `$switch`, `$elements`     |\n| `--title`, `--description`                                     | `$title`, `$description` (element annotations, dropped from HTML output) |\n| `style.hover.color` (see below)                                | `style[\":hover\"].color`                                                  |\n\nDOM properties like `src`, `id`, and `export` are not mapped — they pass through as-is. Attributes matching `aria-*`, `data-*`, or `slot` route into the element's `attributes` object; everything else becomes a top-level DOM property.\n\nNested objects are written as dot-separated keys. Props on a component instance use `props.*`:\n\n```markdown\n:::pricing-card{props.plan=\"Pro\" props.price.ref=\"#/state/proPrice\"}\n:::\n```\n\nwhich expands to:\n\n```json\n{\n  \"tagName\": \"pricing-card\",\n  \"$props\": {\n    \"plan\": \"Pro\",\n    \"price\": { \"$ref\": \"#/state/proPrice\" }\n  }\n}\n```\n\n## Style attributes\n\nElement styles are `style.*` dot-path attributes. CSS pseudo-classes drop their `:` prefix and named media queries drop their `@` prefix — the transpiler restores both:\n\n```markdown\n::button{style.padding=\"8px 16px\" style.hover.backgroundColor=\"blue\" style.--dark.color=\"#f0f0f0\"}\nClick me\n```\n\n```json\n{\n  \"tagName\": \"button\",\n  \"style\": {\n    \"padding\": \"8px 16px\",\n    \":hover\": { \"backgroundColor\": \"blue\" },\n    \"@--dark\": { \"color\": \"#f0f0f0\" }\n  }\n}\n```\n\nRoot-level styles go in frontmatter under `style`, where YAML allows `:hover` and `@--dark` keys to be written directly.\n\n## Repeaters\n\nAn [array repeater](/docs/studio/design/repeaters) has no `tagName`, so it serializes as a directive named after its `$prototype`, with the nested block as the `map` template:\n\n```markdown\n# Recent posts\n\n:::Array{items.ref=\"#/state/posts\"}\n:li{children.0=\"${$map/item/title}\"}\n:::\n```\n\n## Standard Markdown\n\nPlain Markdown maps to the elements you'd expect: headings to `h1`–`h6`, paragraphs to `p`, emphasis to `em`/`strong`, links to `a`, images to `img`, lists to `ul`/`ol` + `li`, fenced code to `pre` > `code`, tables to `table` structures. Mixing prose and directives in one file is the normal case, not a special one.\n\nFenced code with a recognized language tag (`json`, `ts`, `js`, `bash`, `html`, `css`, `yaml`, `md`, and their aliases) is syntax-highlighted at build time: each token becomes a `span` carrying its light and dark colors as `--shiki-light`/`--shiki-dark` CSS variables, so highlighting follows the site's [color scheme](/docs/framework/concepts/color-schemes) automatically. Unknown languages fall back to plain text — no fence ever breaks.\n\n:::doc-note\nThese docs are themselves Jx Markdown: every aside like this one is a container directive (`:::doc-note`, `:::doc-tip`, `:::doc-warning`) rendered by a component the docs site registers via `$elements` on its content collection.\n:::\n\n## Markdown or JSON?\n\nBoth formats produce the same document, and Studio edits both transparently — so choose by what the file mostly contains:\n\n| Prefer Markdown                        | Prefer JSON                                |\n| -------------------------------------- | ------------------------------------------ |\n| Content-heavy pages (blog posts, docs) | Complex interactive components             |\n| Components with significant prose      | Components with many state functions       |\n| Landing pages, marketing content       | Deeply nested element hierarchies          |\n| Quick prototyping                      | Components with complex `$prototype` usage |\n\nTwo hard limits to know: there is no runtime `.md` format (files always transpile to JSON first), and event-handler bodies or computed expressions live in frontmatter `state` definitions — never inline in the directive body.\n\n## Related\n\n- [Content collections](/docs/framework/site/content-collections) — Markdown entries with validated frontmatter\n- [Components](/docs/framework/concepts/components) — the JSON document model both formats produce\n- [Writing](/docs/studio/editing/writing) — editing Markdown visually in Studio\n",
    "section": "Framework",
    "slug": "framework/site/jx-markdown",
    "title": "Jx Markdown",
    "url": "https://jxsuite.com/docs/framework/site/jx-markdown/"
  },
  {
    "description": "Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.",
    "markdown": "\n# Relationships\n\n> **Studio writes this format for you.** The [content-type builder](/docs/studio/projects/content-types) creates reference fields visually — this page documents the schema they produce and how references resolve at build time.\n\nA relationship links one content entry to another: a blog post to its author, a product to its tags. You declare it once, in the field's JSON Schema inside the `content` section of `project.json` — that single declaration drives validation, build-time resolution, and Studio's entry pickers.\n\n## The reference form\n\nA reference field's schema points at the target content type with a `#/content/<type>` pointer:\n\n```json\n{\n  \"content\": {\n    \"authors\": { \"source\": \"./content/authors/\", \"format\": \"Markdown\" },\n    \"blog\": {\n      \"source\": \"./content/blog/\",\n      \"format\": \"Markdown\",\n      \"schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"title\": { \"type\": \"string\" },\n          \"author\": { \"$ref\": \"#/content/authors\" }\n        }\n      }\n    }\n  }\n}\n```\n\nCardinality is expressed with plain JSON Schema — there is no separate relationship syntax:\n\n| Cardinality | Field schema                                                 |\n| ----------- | ------------------------------------------------------------ |\n| To-one      | `{ \"$ref\": \"#/content/authors\" }`                            |\n| To-many     | `{ \"type\": \"array\", \"items\": { \"$ref\": \"#/content/tags\" } }` |\n\n## What entries store\n\nEntries store the target's **entry id** — a string for to-one, an array of strings for to-many. For a Markdown collection the id is the entry's filename, so a post in `content/blog/` referencing `content/authors/jane-doe.md` looks like:\n\n```yaml\n---\ntitle: My Post\nauthor: jane-doe\ntags: [intro, tooling]\n---\n```\n\nAnything other than a string (or array of strings, for to-many) fails the field's validation.\n\n## Resolution\n\nReferences resolve when content loads — at build, in the dev server, and in Studio preview alike. The stored id is replaced in place with the full referenced entry, so by the time a page sees the data, `author` is no longer `\"jane-doe\"` but the whole author entry (`id`, `data`, body). Templates walk straight through:\n\n```json\n{ \"tagName\": \"span\", \"textContent\": \"${state.post.data.author.data.name}\" }\n```\n\nFor a to-many field, each id in the array is replaced by its entry the same way.\n\nAn id that matches no entry in the target type is left untouched — the raw string stays in the data. Guard templates accordingly, or keep ids and filenames in sync.\n\n## Beyond content\n\nThe same reference form generalizes: a pointer targets any project section whose extension declares it referenceable, as `#/<sectionKey>/<name>`. File-based content resolves at load time as described above; connection-backed table sections (from the connector extension) store the id in a column and resolve at request time instead. One direction is disallowed outright: a **content** schema cannot reference a **table** section — content is loaded once at build time while table rows are live, so model that association from the table side.\n\n## Related\n\n- [Content collections](/docs/framework/site/content-collections) — defining content types and their schemas\n- [Content types in Studio](/docs/studio/projects/content-types) — building these schemas visually\n- [References](/docs/framework/concepts/references) — `$ref` inside documents, a different mechanism\n",
    "section": "Framework",
    "slug": "framework/site/relationships",
    "title": "Relationships",
    "url": "https://jxsuite.com/docs/framework/site/relationships/"
  },
  {
    "description": "Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.",
    "markdown": "\n# SEO and metadata\n\nEvery page's `<head>` is assembled declaratively at build time — no imperative code. Metadata comes from three places (`project.json`, the layout, the page), template strings pull values from state, and the build adds a sitemap and `robots.txt` on top.\n\n## Page-level `$head`\n\nA page declares metadata as an array of head elements under `$head`, and its title as a top-level `title` property:\n\n```json\n{\n  \"title\": \"My Blog Post — My Site\",\n  \"$head\": [\n    {\n      \"tagName\": \"meta\",\n      \"attributes\": {\n        \"name\": \"description\",\n        \"content\": \"A great blog post about things\"\n      }\n    }\n  ]\n}\n```\n\nEach entry is `{ \"tagName\": ..., \"attributes\": ... }` — any head element works: `meta`, `link`, `script`, `style`. Use the `title` property rather than a `<title>` entry; the merge always writes the computed title last, so a literal `<title>` in `$head` is overridden.\n\n## Templated metadata\n\n`title` and `$head` attribute values support template strings, evaluated against the page's resolved state. Content-driven pages take their metadata straight from the content entry — no duplication. This is the docs page template on jxsuite.com:\n\n```json\n{\n  \"$paths\": { \"contentType\": \"docs\", \"param\": \"slug\", \"field\": \"id\" },\n  \"title\": \"${state.page.data.title} — Jx Suite\",\n  \"$head\": [\n    {\n      \"tagName\": \"meta\",\n      \"attributes\": {\n        \"name\": \"description\",\n        \"content\": \"${state.page.data.description}\"\n      }\n    }\n  ]\n}\n```\n\nThe injected site and page context is available too: `${state.$site.name}`, `${state.$site.url}`, `${state.$page.url}`, and route params via `${state.$page.params.slug}`.\n\n## Merge order\n\nThe build assembles each page's `<head>` from four layers, later entries winning:\n\n1. **Built-in defaults** — `<meta charset>` and a standard viewport tag\n2. **Site** — `$head` in `project.json` (favicon, fonts, global meta)\n3. **Layout** — the layout document's `$head`\n4. **Page** — the page's `$head`\n\nDuplicates are detected by element identity, so a page-level entry replaces the site-level one rather than appearing twice:\n\n| Element                     | Deduplication key       |\n| --------------------------- | ----------------------- |\n| `<title>`, `<meta charset>` | singleton               |\n| `<meta name=\"...\">`         | `name`                  |\n| `<meta property=\"...\">`     | `property` (Open Graph) |\n| `<link rel=\"...\">`          | `rel` + `href`          |\n| `<script src=\"...\">`        | `src`                   |\n\nTwo tags are added automatically: `<link rel=\"canonical\">` (built from `url` in `project.json` plus the page route, when `url` is set) and the `<html lang>` attribute (from `defaults.lang`, `\"en\"` if unset).\n\n## Sitemap\n\nWhen `url` is set in `project.json`, the build emits `dist/sitemap.xml` from the route table — one `<url>` per compiled page, with:\n\n- `<loc>` — absolute, built from `url` + the route, identical to the page's canonical URL\n- `<lastmod>` — the page source file's modification date (`YYYY-MM-DD`)\n\nDynamic routes appear as their expanded concrete URLs; pages generated from one template share that template file's `<lastmod>`. Redirect sources are not pages and never appear.\n\nTo opt a single page out (a thank-you page, a draft), set `\"$sitemap\": false` at the page root. To disable the sitemap entirely, set `\"build\": { \"sitemap\": false }`. Without `url` the sitemap is skipped with a build warning — absolute `<loc>` values can't be built.\n\n## robots.txt\n\nAfter `public/` is copied into `dist/`, the build appends a `Sitemap: <url>/sitemap.xml` line to `dist/robots.txt`. If you shipped no `robots.txt`, a permissive default is created (`User-agent: *` / `Allow: /`); if yours already has a `Sitemap:` line, it is left untouched.\n\n## Related\n\n- [project.json](/docs/framework/site/project-json) — site-level `$head`, `url`, and `defaults`\n- [Layouts](/docs/framework/site/layouts) — where layout-level head entries come from\n- [Content collections](/docs/framework/site/content-collections) — the entry data that feeds templated metadata\n",
    "section": "Framework",
    "slug": "framework/site/seo",
    "title": "SEO and metadata",
    "url": "https://jxsuite.com/docs/framework/site/seo/"
  },
  {
    "description": "The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.",
    "markdown": "\n# Images\n\nThe build optimizes images automatically: every eligible `<img>` gets a responsive `srcset` of resized, format-converted variants, plus lazy-loading attributes. It's on by default — you configure it (or turn it off) under the `images` key in `project.json`.\n\n## Configuration\n\nAll properties are optional; these are the defaults:\n\n```json\n{\n  \"images\": {\n    \"optimize\": true,\n    \"widths\": [320, 640, 960, 1280, 1920],\n    \"formats\": [\"webp\", \"avif\"],\n    \"quality\": { \"webp\": 80, \"avif\": 65, \"jpeg\": 80, \"png\": 80 },\n    \"sizes\": \"(max-width: 768px) 100vw, 50vw\",\n    \"lazyLoad\": true,\n    \"service\": \"build\"\n  }\n}\n```\n\n| Property        | What it controls                                                                              |\n| --------------- | --------------------------------------------------------------------------------------------- |\n| `optimize`      | Master switch — `false` disables all image processing                                         |\n| `widths`        | Pixel widths for the responsive variants                                                      |\n| `formats`       | Output formats (`\"webp\"`, `\"avif\"`, `\"jpeg\"`, `\"png\"`)                                        |\n| `quality`       | Per-format compression quality (0–100)                                                        |\n| `sizes`         | Default CSS `sizes` attribute injected alongside `srcset`                                     |\n| `lazyLoad`      | Adds `loading=\"lazy\"` and `decoding=\"async\"`                                                  |\n| `service`       | `\"build\"` = Sharp at build time; `\"cloudflare\"` = transform URLs served by Cloudflare (below) |\n| `remoteDomains` | Https hostnames whose remote images also get transform srcsets — `\"cloudflare\"` service only  |\n\n## What the build does\n\nFor each eligible image, the pipeline (powered by [Sharp](https://sharp.pixelplumbing.com/)):\n\n1. **Filters widths** to those at or below the image's natural width — no upscaling; the original width is always included as a breakpoint.\n2. **Generates one variant per width × format** and writes it to `dist/images/_optimized/{stem}-{width}-{hash}.{format}` (e.g. `hero-640-a1b2c3d4.webp`).\n3. **Mutates the `<img>`** in the compiled HTML: a `srcset` listing the variants in the best configured format (AVIF when configured, otherwise the first format), plus `sizes` from config (unless the node already sets one), and `loading=\"lazy\"` / `decoding=\"async\"` when `lazyLoad` is on. The original `src` stays as the fallback.\n\nImages embedded in pre-rendered Markdown content go through the same transformation, so a `![hero](./images/hero.jpg)` in a blog post is optimized like any hand-placed `<img>`.\n\n## Which images are eligible\n\nProcessed: `<img>` nodes with a static, local `src` — a string, not a `${...}` expression or a `$ref` — pointing at a raster file (`.jpg`, `.jpeg`, `.png`, `.webp`, `.avif`, `.tiff`) that exists on disk.\n\nSkipped automatically:\n\n- External URLs (`http://`, `https://`, `//`, `data:`)\n- SVGs and GIFs\n- Dynamic `src` values (template expressions or bindings)\n- Anything carrying a `data-no-optimize` attribute\n\n## Where srcs resolve\n\nIn pages, layouts, and components, image paths resolve against the **site root**, not the referring file: a `/`-prefixed src like `/images/hero.jpg` resolves into `public/` (so it's `public/images/hero.jpg` on disk, and works verbatim at runtime too), while a relative src like `content/blog/images/hero.jpg` resolves from the project root.\n\n**Content entries are the exception** — they resolve against themselves, so a collection stays readable in a markdown editor:\n\n```markdown\n![A diagram](./images/diagram.png)\n```\n\nA relative reference in an entry is remapped to the collection's own URL — `content/blog/images/diagram.png` becomes `/content/blog/images/diagram.png` — and the build copies the file there. See [Content collections](/docs/framework/site/content-collections).\n\n## Per-image overrides\n\nIndividual images can override the global config through ordinary attributes:\n\n```json\n{\n  \"tagName\": \"img\",\n  \"attributes\": {\n    \"src\": \"/images/hero.jpg\",\n    \"alt\": \"Hero image\",\n    \"sizes\": \"(max-width: 640px) 80vw, 40vw\",\n    \"loading\": \"eager\"\n  }\n}\n```\n\n- `sizes` — replaces the configured default for this image\n- `loading=\"eager\"` — keeps `loading=\"lazy\"` off an above-the-fold image\n- `data-no-optimize` — skips the pipeline entirely\n\n## Caching\n\nVariants are cached in `.cache/images/` (with a `manifest.json`) so unchanged images aren't re-encoded on the next build. The cache key combines the source file's content hash with a hash of the optimization config — changing either the image or the `widths`/`formats`/`quality` settings invalidates the entry, as do missing variant files. The cache survives `dist/` cleanup; add `.cache/` to `.gitignore`, or commit it to speed up CI builds.\n\nThe cache is self-pruning: after a fully successful build, entries no build step touched (deleted or replaced source images, superseded configs) are dropped and their variant files deleted, so a persisted cache — and the `dist/images/_optimized/` copy made from it — stays bounded to the images the site actually uses, even when the cache lives forever (for example in a CI cache). Builds that end with errors skip pruning — a page that failed to compile never touched its images, and evicting them would force a pointless re-encode.\n\n## The Cloudflare service\n\nSetting `\"service\": \"cloudflare\"` replaces the build-time pipeline with pure markup: eligible images get a `srcset` of `/cdn-cgi/image/...` transform URLs (one per configured width, `format=auto` so Cloudflare negotiates AVIF/WebP per browser, quality from `quality.webp`), and no variants are generated at build time. Remote `https` images from hostnames listed in `remoteDomains` get the same treatment.\n\nThis requires the site to be served through a Cloudflare zone with Image Transformations enabled (dashboard: _Images > Transformations_). The URLs do not resolve on `*.pages.dev` or `*.workers.dev` preview hosts — previews fall back to the untouched originals.\n\n## Related\n\n- [Build output and adapters](/docs/framework/site/deployment) — where the variants land in `dist/`\n- [Media in Studio](/docs/studio/projects/media) — browsing and placing images visually\n- [project.json](/docs/framework/site/project-json) — the full configuration reference\n",
    "section": "Framework",
    "slug": "framework/site/images",
    "title": "Images",
    "url": "https://jxsuite.com/docs/framework/site/images/"
  },
  {
    "description": "Map old URLs to new ones in project.json: the redirects map, status codes, and the meta-refresh HTML and _redirects files the build emits.",
    "markdown": "\n# Redirects\n\nWhen a page moves, the old URL should keep working. Declare redirects once in `project.json` and the build emits them in two forms — an HTML fallback that works on any static host, and a `_redirects` file for hosts that do server-side redirects.\n\n## The redirects map\n\n`redirects` maps source paths to destinations. These are real entries from jxsuite.com:\n\n```json\n{\n  \"redirects\": {\n    \"/docs/get-studio\": \"/docs/start/install/\",\n    \"/docs/components\": \"/docs/framework/concepts/components/\",\n    \"/docs/spec\": \"/docs/framework/\"\n  }\n}\n```\n\nEach value is either a plain destination string, or an object when you need a specific status code:\n\n```json\n{\n  \"redirects\": {\n    \"/old-page\": \"/new-page\",\n    \"/moved-permanently\": { \"destination\": \"/new-location\", \"status\": 301 },\n    \"/temporary-redirect\": { \"destination\": \"/other-page\", \"status\": 302 }\n  }\n}\n```\n\nThe string form defaults to `301` (permanent). Use `302` for temporary moves; on hosts that support rewrites, `200` serves the destination without redirecting.\n\n## What the build emits\n\nFor every static source path, the build writes an HTML page at that path (`/old-page` becomes `dist/old-page/index.html`) containing an instant meta refresh, a canonical link to the destination, and a visible fallback link:\n\n```html\n<meta http-equiv=\"refresh\" content=\"0;url=/new-page\" /> <link rel=\"canonical\" href=\"/new-page\" />\n```\n\nIt also writes a single `dist/_redirects` file — the Netlify/Cloudflare format, one rule per line:\n\n```\n/old-page /new-page 301\n/blog/:slug /posts/:slug 301\n```\n\nPattern sources using `:param` or `*` wildcards go into `_redirects` only — a wildcard can't be expressed as files on disk, so no HTML fallback is emitted for them.\n\n## Platform behavior\n\n- **Netlify and Cloudflare Pages** read `_redirects` and answer with a true server-side redirect and your configured status code — including pattern rules. The HTML fallbacks are shadowed and harmless.\n- **GitHub Pages and other plain static hosts** ignore `_redirects`; the meta-refresh HTML serves as the redirect. Static sources work; pattern rules don't.\n\nRedirect sources are not pages: they never appear in the [sitemap](/docs/framework/site/seo), and the canonical link on each fallback page points crawlers at the destination.\n\n:::doc-warning\nA redirect source that collides with a real page will overwrite it in `dist/` — the build does not currently warn about conflicts. Check the route table before pointing a redirect at an existing path.\n:::\n\n## Related\n\n- [Build output and adapters](/docs/framework/site/deployment) — where `_redirects` fits in the `dist/` contract\n- [Routing](/docs/framework/site/routing) — how real pages claim their URLs\n- [Other hosts](/docs/studio/publish/other-hosts) — per-host publishing recipes\n",
    "section": "Framework",
    "slug": "framework/site/redirects",
    "title": "Redirects",
    "url": "https://jxsuite.com/docs/framework/site/redirects/"
  },
  {
    "description": "Enable the search section to index content collections at build time, then wire a search UI with the Search prototype or the headless client.",
    "markdown": "\n# Site search\n\nJx sites get full-text search from the `@jxsuite/search` extension: the build emits a JSON index from your content collections, and a small headless client (MiniSearch under the hood, ~7 kB) answers queries entirely in the browser. No server, no third-party service — results work on any static host, and section matches deep-link straight to the heading (`/docs/framework/site/#assets`).\n\n## Enabling the section\n\nAdd the extension and a `search` section to `project.json`, then regenerate schemas with `jx schema`:\n\n```json\n{\n  \"extensions\": [\"@jxsuite/parser\", \"@jxsuite/search\"],\n  \"search\": {\n    \"collections\": {\n      \"docs\": { \"basePath\": \"/docs/\", \"boost\": { \"title\": 4, \"heading\": 2 } }\n    }\n  }\n}\n```\n\nPer collection:\n\n| Key            | Default                        | Meaning                                                        |\n| -------------- | ------------------------------ | -------------------------------------------------------------- |\n| `basePath`     | _(required)_                   | URL prefix mapping entry ids to routes                         |\n| `fields`       | `[\"title\", \"heading\", \"text\"]` | Document fields the index searches                             |\n| `boost`        | `{}`                           | Per-field score boosts                                         |\n| `sections`     | `true`                         | Also index one document per heading, with `#anchor` deep links |\n| `sectionDepth` | `3`                            | Deepest heading level that gets its own section document       |\n\nTop-level: `output` (default `/search-index.json`) sets where the index is written; `engine` is `minisearch` (the only engine today — the field exists so future engines slot in without reshaping the section).\n\n`bunx jx build` then emits the index into `dist/` alongside your pages. The index holds two kinds of documents per entry: the whole page, and one per heading section — so a query can land on \"the _Assets_ section of _Site architecture_\" rather than just the page.\n\n## Querying from page state\n\nThe `Search` prototype gives any page reactive results with zero client wiring:\n\n```json\n\"state\": {\n  \"q\": \"\",\n  \"results\": { \"$prototype\": \"Search\", \"query\": { \"$ref\": \"#/state/q\" }, \"limit\": 8 }\n}\n```\n\nBind an input to `state.q` and map over `state.results`. Each result group is a page with its matching sections:\n\n```json\n{\n  \"slug\": \"framework/site\",\n  \"title\": \"Site architecture\",\n  \"url\": \"/docs/framework/site/\",\n  \"score\": 12.4,\n  \"hits\": [{ \"heading\": \"Assets\", \"url\": \"/docs/framework/site/#assets\", \"score\": 9.1 }]\n}\n```\n\nIn compiled sites the def lowers to plain client code that lazily loads the bundled client and fetches the index on first use — nothing is downloaded until the visitor actually searches. Options: `limit` (max rows, default 8), `group` (set `false` for the flat row list described below), `index` (override the index URL).\n\n## Building a search UI component\n\nInteractive components aren't lowered, so inside a compiled component you use the headless client directly through its `$src` state conventions — the export names double as state keys:\n\n```json\n\"state\": {\n  \"searchQuery\": \"\",\n  \"searchResults\": [],\n  \"searchReady\": false,\n  \"searchActive\": 0,\n  \"searchInit\": { \"$prototype\": \"Function\", \"$src\": \"npm:@jxsuite/search/client\", \"parameters\": [\"state\"] },\n  \"runSearch\": { \"$prototype\": \"Function\", \"$src\": \"npm:@jxsuite/search/client\", \"parameters\": [\"state\", \"e\"] },\n  \"onMount\": { \"$prototype\": \"Function\", \"arguments\": [\"state\"], \"body\": \"state.searchInit(state);\" }\n}\n```\n\n- `searchInit(state)` preloads the index and flips `state.searchReady` (re-running any pending query).\n- `runSearch(state, e)` reads the input event, stores flat rows on `state.searchResults`, publishes the row count as `state.searchCount`, and resets `state.searchActive` — wire it to your input's `oninput`.\n\nThe build bundles `npm:@jxsuite/search/client` (MiniSearch included) into `/assets/` automatically — declare the dependency in your project's `package.json` and import it like any module.\n\nFor full control, import the core API from the same module: `preload(indexUrl?)`, `isReady()`, and the synchronous `query(text, { limit, group, pageCap })`.\n\n## Rendering a result row\n\nFlat rows (`group: false`, what `runSearch` uses) arrive presentation-ready. Each row is one page **or** one of its heading sections, and carries a breadcrumb plus pre-highlighted text — so a component can render matches without doing any string work of its own:\n\n```json\n{\n  \"url\": \"/docs/framework/site/#assets\",\n  \"title\": \"Site architecture\",\n  \"heading\": \"Assets\",\n  \"crumbs\": [\"Framework\", \"Site architecture\"],\n  \"titleTokens\": [{ \"t\": \"Assets\", \"m\": true }],\n  \"excerptTokens\": [\n    { \"t\": \"…referenced from a page are copied into \", \"m\": false },\n    { \"t\": \"assets\", \"m\": true },\n    { \"t\": \"/ at build time…\", \"m\": false }\n  ],\n  \"score\": 9.1\n}\n```\n\nA token's `m` flag marks a run that matched a query term — whole words, so a prefix search for `intro` highlights `introduction`. Map over the tokens and style the matched runs; nothing is injected as HTML:\n\n```json\n{\n  \"$prototype\": \"Array\",\n  \"items\": { \"$ref\": \"$map/item/titleTokens\" },\n  \"map\": {\n    \"tagName\": \"span\",\n    \"attributes\": { \"class\": \"${item.m ? 'hl' : ''}\" },\n    \"textContent\": \"${item.t}\"\n  }\n}\n```\n\n`crumbs` is the slug's ancestor trail, with the page title appended on a section row. `excerptTokens` cover a ~160-character window around the first body match, elided with `…` when it doesn't reach an edge. At most `pageCap` rows (default 3) come from any one page, so a long page can't flood the list.\n\n:::doc-tip\nGrouped results (`group: true`, the default and what the `Search` prototype returns) keep the older nested shape — a page with a `hits` array of its matching sections. Use flat rows for a search palette, grouped for a results page organized by document.\n:::\n\n:::doc-note\nOne index per site is the assumption: the first `preload` wins the in-page singleton. Multiple collections are fine — they share the one index and results carry their `collection` name.\n:::\n",
    "section": "Framework",
    "slug": "framework/site/search",
    "title": "Site search",
    "url": "https://jxsuite.com/docs/framework/site/search/"
  },
  {
    "description": "What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.",
    "markdown": "\n# Build output and adapters\n\n`bunx jx build` turns a project into an ordinary folder of web files. The output is standard static HTML, CSS, and JS — deployable anywhere — and an optional adapter adds the platform-specific pieces for hosts that run a server.\n\nRun it from the project root (or pass the root as an argument). `--verbose` prints per-route progress; `--no-clean` skips wiping the output directory first. The other CLI commands are covered in [the jx CLI](/docs/framework/build/cli).\n\n## The dist/ contract\n\nEverything lands in one directory:\n\n```\ndist/\n├── index.html                # One HTML file per route\n├── about/index.html\n├── blog/hello-world/index.html\n├── components/               # Compiled component JS + CSS sidecars\n├── images/_optimized/        # Responsive image variants\n├── sitemap.xml               # When url is set in project.json\n├── robots.txt                # From public/, with a Sitemap: line appended\n├── _redirects                # From the redirects map\n├── favicon.svg               # public/ is copied in verbatim\n└── worker.js                 # Only with a server adapter (see below)\n```\n\nPages only load JavaScript for components that actually need it — a fully static component ships as pre-rendered HTML and CSS with no script. Files from `public/` are copied verbatim, and the `copy` map in `project.json` can place additional files at chosen output paths.\n\n## Build options\n\nThe `build` section of `project.json`:\n\n| Property        | Default    | What it does                                                       |\n| --------------- | ---------- | ------------------------------------------------------------------ |\n| `outDir`        | `\"./dist\"` | Output directory                                                   |\n| `trailingSlash` | `\"always\"` | URL shape: `\"always\"` or `\"never\"` (below)                         |\n| `sitemap`       | `true`     | Set `false` to skip [sitemap generation](/docs/framework/site/seo) |\n| `adapter`       | _(none)_   | `\"cloudflare-workers\"`, `\"cloudflare-pages\"`, `\"node\"`, or `\"bun\"` |\n\n### trailingSlash\n\n`\"always\"` (the default) writes every route as a directory index — `/about` becomes `dist/about/index.html`, so the canonical served URL is `/about/`. `\"never\"` writes flat files — `dist/about.html` — for hosts that serve extensionless or `.html` URLs. Pick whichever matches how your host serves files, and keep it stable: changing it changes every URL on the site.\n\n## Adapters\n\nWithout an adapter (the **Static** choice in Studio), the build is just `dist/` — HTML, CSS, JS, and assets for any static host or CDN. Setting `build.adapter` additionally packages the site's server tier — state entries with `timing: \"server\"`, plus the server mounts of enabled extensions — into a single deployable worker:\n\n| Adapter                | Extra output                                                              |\n| ---------------------- | ------------------------------------------------------------------------- |\n| _(none / static)_      | Nothing — plain `dist/`                                                   |\n| `\"cloudflare-workers\"` | `dist/worker.js`, a Hono server that also serves the static assets        |\n| `\"cloudflare-pages\"`   | `dist/_worker.js` + `dist/_routes.json`, only when there is a server tier |\n| `\"node\"` / `\"bun\"`     | `dist/worker.js`, a Hono server for that runtime                          |\n\n### What the worker serves\n\nThree route families, and nothing else:\n\n| Route                  | Served by                                                       |\n| ---------------------- | --------------------------------------------------------------- |\n| `/_jx/auth/*`          | The auth extension — sign-up, sign-in, sign-out, session lookup |\n| `/_jx/data/*`          | The connector extension — table CRUD                            |\n| `/_jx/server/<export>` | One route per `timing: \"server\"` state entry                    |\n\n**Pages are never rendered per request.** Every route is prerendered at build time and served as static HTML; the worker answers the `/_jx/*` API surface and falls through to the assets for everything else. Interactivity arrives by hydration, so a page that shows signed-in content ships its signed-out state in the HTML and swaps once the session resolves in the browser — see [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\nDetails worth knowing:\n\n- Server functions are collected from every component and page, deduplicated by export name, and bundled once — there are no per-route server files when an adapter is set.\n- The worker is **self-contained**: hono, extension mounts, database connectors, and your server modules are inlined at build time, so `dist/` deploys and runs with no `node_modules` and no deploy-time bundling step.\n- On Cloudflare Pages, `_routes.json` limits worker invocation to `/_jx/*`, so static assets are served without waking the worker. A Pages site with no server functions and no mounts gets no worker at all — the deployment stays purely static.\n- A project with dynamic sections (a non-empty `data`/`connections` or `auth` section, served by extension mounts) **must** set a server-capable adapter. On static the build stops with an error naming the offending sections, since a static site has nothing to serve live data with.\n- Switching hosts means switching the adapter — your source never changes.\n\n## Hooking up a host\n\nThe recipe is the same everywhere: build command `bunx jx build`, publish directory `dist`. [Other hosts](/docs/studio/publish/other-hosts) walks through Netlify and GitHub Pages, and [Cloudflare Pages](/docs/studio/publish/cloudflare) is built into Studio's publish flow.\n\n## Related\n\n- [The build pipeline](/docs/framework/build) — what happens between source and `dist/`\n- [Redirects](/docs/framework/site/redirects) — the `_redirects` file in the output\n- [SEO and metadata](/docs/framework/site/seo) — `sitemap.xml` and `robots.txt` generation\n- [Databases](/docs/studio/data) — the tables behind `/_jx/data`, and why they need an adapter\n- [Timing](/docs/framework/concepts/timing) — how a `timing: \"server\"` entry becomes a `/_jx/server/` route\n",
    "section": "Framework",
    "slug": "framework/site/deployment",
    "title": "Build output and adapters",
    "url": "https://jxsuite.com/docs/framework/site/deployment/"
  },
  {
    "description": "What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.",
    "markdown": "\n# How compilation works\n\n`jx build` turns a Jx project — JSON documents in `pages/`, `layouts/`, and `components/` — into a production site in `dist/`. The compiler erases the Jx abstractions at build time: no JSON documents, no interpreter, and no framework code ship to production. A page only gets JavaScript when the compiler can prove it needs some, and the proof runs in one direction — everything is static until something dynamic is found.\n\nThe only client-side dependencies a page can end up with are `@vue/reactivity` (~7 kB gzip) and `lit-html` (~3 kB gzip), loaded through an import map — and only on pages that need them.\n\n## What `jx build` does\n\nRunning `jx build` (see [CLI commands](/docs/framework/build/cli)) orchestrates one pipeline:\n\n1. **Load `project.json`** and register the extensions it declares (`extensions`), which contribute file formats such as Markdown pages.\n2. **Discover routes** by scanning `pages/`: `pages/index.json` → `/`, `pages/about.json` → `/about`, `pages/blog/[slug].json` → `/blog/:slug`, `pages/docs/[...path].json` → catch-all. Files starting with `_` are not routed. `.json` pages are native; other extensions (like `.md`) route through a format registered by an enabled extension.\n3. **Expand dynamic routes** — a `[param]` page's `$paths` definition produces one concrete route per entry.\n4. **Compile each route**: resolve its layout, merge `$head` from site + layout + page, inject the read-only `$site`/`$page` context, resolve build-time data, transform images for responsive output, then hand the assembled document to the compiler.\n5. **Emit `dist/`**: one `index.html` per route (with `build.trailingSlash: \"always\"`, the default), compiled component modules and CSS under `dist/components/`, `public/` copied verbatim, plus `sitemap.xml` (when `url` is set in `project.json`), `_redirects`, and — when `build.adapter` is set — a bundled server worker (`worker.js`, or `_worker.js` + `_routes.json` for Cloudflare Pages).\n\nThe build finishes with a summary (`Done: 12 routes → 34 files`) and exits non-zero if any route failed.\n\n## Static detection\n\nThe routing decision is a single recursive tree walk — `isDynamic()` — that never executes your code. A document compiles to plain HTML unless the walk finds one of:\n\n- a `state` entry that exists at runtime (a plain value, a `$prototype` entry, or anything with a `default`)\n- a `${…}` template string in a property, style, or attribute value\n- a `$ref` binding on an element property\n- a `$switch` node\n- a mapped array (`$prototype: \"Array\"`)\n- any child that is itself dynamic\n\nThree kinds of state are exempt because they are resolved during the build and then removed:\n\n- the injected `$site` and `$page` context (read-only, never reactive)\n- entries with `timing: \"compiler\"` — resolved at build time and baked into the HTML\n- schema-only definitions (pure type information produces no output)\n\n## What compiles away\n\nSite and page context looks like state, but it is data the compiler already has. This heading:\n\n```json\n{\n  \"tagName\": \"h1\",\n  \"textContent\": \"${state.$site.name} — ${state.$page.title}\"\n}\n```\n\ncompiles to literal HTML with the template evaluated and gone:\n\n```html\n<h1>Acme Realty — About us</h1>\n```\n\nThe same applies to `timing: \"compiler\"` data and to content collections: template strings over build-time values are evaluated against the resolved data, mapped arrays over resolved content are unrolled into repeated markup, and the now-spent state entries are stripped. What reaches `isDynamic()` afterwards is a plain tree — so a blog index that lists every post can still be a zero-JS page.\n\n## The output tiers\n\n### Fully static\n\nWhen nothing dynamic survives the build-time resolution, the page is plain HTML with its CSS in the head — zero JavaScript, no import map, no module scripts. This is the default outcome, not an optimization you opt into.\n\n### Islands in a static shell\n\nA dynamic subtree inside an otherwise-static document doesn't drag the whole page into the client tier. The compiler cuts the subtree out, compiles it to a small custom-element module, and leaves a placeholder tag in the HTML:\n\n```html\n<jx-island-0></jx-island-0>\n<script type=\"module\" src=\"./_islands/jx-island-0.js\"></script>\n```\n\nThe module upgrades the element in place. Components you author with a hyphenated `tagName` (say `site-counter`) work the same way: the page HTML stays static, and `dist/components/site-counter.js` loads only on pages that use the element. The import map added to those pages resolves `@vue/reactivity` and `lit-html` for the island modules.\n\n### Dynamic pages\n\nWhen the page document itself is dynamic — it declares live `state`, binds `$ref`s, or uses runtime template strings — the compiler still pre-renders the full HTML, marks bound elements with `data-bind` attributes, and emits one small module that wires reactivity onto the existing DOM: reactive state, `computed()` for derived values, `effect()` per binding, and event handler hookup. There is no client-side re-render of the initial view; the JS only maintains what changes.\n\nTwo more outputs sit alongside these tiers: `.class.json` documents compile to ES class modules, and `timing: \"server\"` entries generate server handlers — a per-route `_server.js`, or a single site-wide worker when `build.adapter` is set.\n\nAcross all three tiers the emitter indents nested children so the generated HTML stays readable — except inside `pre`, `textarea`, and the rest of the tags browsers render with `white-space: pre`. There, and everywhere below them (`white-space` inherits), children are concatenated with no separator, because indentation between elements would be content rather than formatting. This is what keeps a syntax-highlighted code block — one `<span>` per token — rendering as the code you wrote.\n\n## CSS extraction\n\nStyle never ships as JavaScript. During compilation, every static `style` definition — the project-level `style` from `project.json`, the layout's, the page's, and each node's — is extracted into a single `<style>` block in the page `<head>`, with `$media` breakpoint names expanded to real media queries. Components additionally emit a `dist/components/<tag>.css` sidecar, and styles found in component slot content are collected into the page's style block. See [Styling](/docs/framework/concepts/styling) for the authoring model.\n\n## Why is my page shipping JavaScript?\n\nWork the static-detection list backwards:\n\n- **Live `state` on the page document** — even `{\"count\": 0}` — makes the page dynamic. If the data never changes after the build, move it to `timing: \"compiler\"` or reference `$site`/`$page` context instead.\n- **A `${…}` template string** whose inputs exist only at runtime keeps its binding alive. Templates over build-time data compile away.\n- **`$switch`, `$ref` bindings, and mapped arrays** over runtime state need the client tier; mapped arrays over build-resolved content do not.\n- **Interactive components** cost their own module on the pages that use them — the rest of the page stays static.\n\n## Related\n\n- [CLI commands](/docs/framework/build/cli) — `jx build` flags and the rest of the CLI\n- [The dev server](/docs/framework/build/dev-server) — the development-time counterpart\n- [Site architecture](/docs/framework/site) — the directory layout the build consumes\n- [Reactivity](/docs/framework/concepts/reactivity) — what the dynamic tier compiles from\n- [Components](/docs/framework/concepts/components) — the component model behind islands\n",
    "section": "Framework",
    "slug": "framework/build",
    "title": "How compilation works",
    "url": "https://jxsuite.com/docs/framework/build/"
  },
  {
    "description": "The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.",
    "markdown": "\n# The dev server\n\nDuring development you don't run the compiled `dist/` output — you run your source files through the Jx dev server, a Bun-native server from `@jxsuite/server`. It serves the project directory as-is, reloads the browser when files change, executes server-side code on your behalf, and backs Studio's file operations.\n\n## Starting it\n\nThe whole server is one call, `createDevServer`:\n\n```js\n// server.js\nimport { createDevServer } from \"@jxsuite/server\";\n\nawait createDevServer({\n  root: import.meta.dir,\n  port: 3000,\n});\n```\n\nRun it with `bun run server.js` and open `http://localhost:3000/`. The full options surface:\n\n| Option       | Default    | What it does                                                                                                                                     |\n| ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `root`       | (required) | Project root to serve; every file operation is contained to it.                                                                                  |\n| `port`       | `3000`     | Listen port.                                                                                                                                     |\n| `builds`     | `[]`       | `Bun.build` entries (`entrypoints`, `outdir`, optional `match`, `label`) bundled at startup and selectively rebuilt when a changed file matches. |\n| `watch`      | `true`     | File watching + live reload. Pass `false` to disable, or an options object for the watcher.                                                      |\n| `studio`     | `true`     | Mounts the `/__studio/*` API that Jx Studio talks to.                                                                                            |\n| `middleware` | —          | `(req, url) => Response \\| null` — your own routes, checked before static file serving.                                                          |\n\n:::doc-note\nFor a site project, [`jx dev`](/docs/framework/build/cli) is the front door: it runs this server under Bun with a site-aware wrapper that builds the project up front, serves the built pages from `dist/`, and rebuilds before each live-reload broadcast. A hand-written `server.js` like the one above is for embedding the server with custom options.\n:::\n\n## Live reload\n\nThe server watches `root` (ignoring `node_modules/`, `dist/`, `.git/`, and friends) and exposes a Server-Sent Events endpoint at `/__reload`. Every `.html` file it serves gets a one-line client injected before `</body>`:\n\n```html\n<script>\n  new EventSource(\"/__reload\").onmessage = () => location.reload();\n</script>\n```\n\nSave a file and every connected page reloads. When the changed file matches a `builds` entry's `match` pattern, that bundle is rebuilt first, so the reload picks up fresh output. The one exception is the Studio editor itself: Studio pages never get the reload script, because Studio refreshes edited files in place and a full reload would discard open tabs and undo history.\n\n## How Studio is served\n\nStudio is a static web app plus a REST API — the dev server provides both. With `studio: true` (the default), the server mounts `/__studio/*`: project metadata, file listing, read/write/delete/rename, component discovery, content search, code formatting and linting for the function-body editor, and a realtime co-editing WebSocket at `/__studio/collab`. Every filesystem operation is validated to stay under `root`, so path traversal is rejected.\n\nStudio's UI assets are ordinary static files under the served root; opening a project in Studio activates its directory on the server, which then also resolves project files, and `public/` contents at the site root, exactly as the production build would. The desktop app doesn't use this server — it embeds its own loopback-only, token-gated variant — but it speaks the same API.\n\n## Running server-side code: the two proxies\n\nProduction builds compile server-side work into generated handlers. In development there is no build, so the runtime hands that work to the dev server through two POST endpoints. You don't call either one yourself — they exist so documents behave the same in dev as after `jx build`.\n\n### Module resolution (`POST /__jx_resolve__`)\n\nWhen a document uses an external class — a `$prototype` entry with a `$src` — the browser can't always resolve it: the module may need Node-only APIs like the filesystem, or live behind CORS. The runtime posts the entry (its `$src`, `$prototype`, `$export`, and config) to the dev server, which imports the module server-side (`.js` directly, `.class.json` via its `$implementation`), instantiates the class with the config, resolves it, and returns the value as JSON. Reactive entries re-resolve when their inputs change.\n\n### Server functions (`POST /__jx_server__`)\n\nFunctions marked `timing: \"server\"` never ship to the browser. In development the runtime posts the call instead:\n\n```json\n{\n  \"$src\": \"./dashboard.server.js\",\n  \"$export\": \"fetchMetrics\",\n  \"arguments\": { \"userId\": 42 }\n}\n```\n\nThe server imports the module, invokes the export with the arguments object, and returns the result as JSON. In production the same calls hit the generated server handler — see [How compilation works](/docs/framework/build).\n\nExtensions that declare server mounts (for example the data API) are served under `/_jx/*` with the same wire contract as the generated production worker, so data-backed documents work identically in both environments.\n\n## Static files and npm packages\n\nAnything the other routes don't claim is served from disk: files under `root` at their natural URLs, then files under the active Studio project, then the project's `public/` directory mapped to the site root — mirroring where assets live in production. Bare npm specifiers in URLs (such as `@jxsuite/parser/…`) are resolved through `node_modules`, bundled on demand with `Bun.build`, and cached for the life of the server. All responses are sent with `Cache-Control: no-cache` so a plain reload never serves a stale bundle.\n\n## Related\n\n- [CLI commands](/docs/framework/build/cli) — what `bun create @jxsuite` scaffolds and what `jx` can run\n- [How compilation works](/docs/framework/build) — what replaces the proxies in production\n- [Site architecture](/docs/framework/site) — the directory layout the server serves\n",
    "section": "Framework",
    "slug": "framework/build/dev-server",
    "title": "The dev server",
    "url": "https://jxsuite.com/docs/framework/build/dev-server/"
  },
  {
    "description": "Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.",
    "markdown": "\n# CLI commands\n\nJx has two command-line surfaces: `bun create @jxsuite`, which scaffolds a new project once, and the `jx` CLI, which ships with `@jxsuite/compiler` (a devDependency of every scaffolded project) and operates on an existing project. Every `jx` command takes an optional project root as its first positional argument and defaults to the current directory.\n\n## `bun create @jxsuite`\n\nScaffold a new Jx project.\n\n```\nbun create @jxsuite <directory> [--template <id>]\n```\n\n| Flag              | What it does                                                                                                                                                                                                   |\n| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `--template <id>` | Skip the template prompt. Accepts a built-in template id (`blank`, `desktop-first`, `mobile-first`, `mobile-app`) or a starter id; built-in ids win over starter ids, and an unknown id falls back to `blank`. |\n\nThe scaffolder then prompts for a project name, description, production URL, a template (when `--template` wasn't given — the built-in variants plus the starter sites), and a deployment adapter: `static` (default), `cloudflare-pages`, `node`, `bun`, or `cloudflare-workers`.\n\nA blank scaffold produces:\n\n- `project.json` — bound to `./project.schema.json`, with a viewport `$head`, `$media` breakpoints matching the template (desktop-first `max-width` queries or mobile-first `min-width` queries), a `build` block (`outDir: \"./dist\"`, `format: \"directory\"`, `trailingSlash: \"always\"`, plus `adapter` when not static), `extensions: [\"@jxsuite/parser\"]`, an empty `content` section, base `style`, and `defaults` pointing at `./layouts/base.json`\n- `package.json` — `@jxsuite/parser` as a dependency, `@jxsuite/compiler` and `@jxsuite/runtime` as devDependencies, and `build`/`dev` scripts (plus a `deploy` script and `wrangler` for the Cloudflare adapters)\n- `layouts/base.json`, `pages/index.md`, empty `components/`, `content/`, and `public/` directories, and a `.gitignore`\n- `wrangler.jsonc` for the Cloudflare adapters; the `mobile-app` template overlays an app-shell layout and home page\n\nChoosing a starter instead copies the starter site verbatim (minus build artifacts), then re-stamps `project.json` with your name, URL, description, and adapter, and rebuilds `package.json` with current dependency ranges.\n\n## `jx build`\n\nBuild the site to the configured `outDir` (default `dist/`). See [How compilation works](/docs/framework/build) for what happens inside.\n\n```\njx build [root] [--verbose] [--no-clean]\n```\n\n| Flag         | What it does                             |\n| ------------ | ---------------------------------------- |\n| `--verbose`  | Print detailed build progress.           |\n| `--no-clean` | Don't remove the output directory first. |\n\nPrints a summary (`Done: 12 routes → 34 files`) on success; lists route errors and exits `1` if any page failed to compile.\n\n## `jx dev`\n\nStart the dev server for a project: builds the site, serves the built pages with live reload (edits rebuild before the browser refreshes), and exposes the Studio backend API. Requires [Bun](https://bun.sh) and `@jxsuite/server` in the project's devDependencies — both part of every scaffolded project; `jx dev` prints an install hint when either is missing.\n\n```\njx dev [root] [--port <n>]\n```\n\n| Flag         | What it does                        |\n| ------------ | ----------------------------------- |\n| `--port <n>` | Port to listen on (default `3000`). |\n\nSee [The dev server](/docs/framework/build/dev-server) for what runs underneath.\n\n## `jx preview`\n\nServe an already-built `dist/` directory — a dependency-free static server for checking the production build locally. Run `jx build` first.\n\n```\njx preview [root] [--port <n>]\n```\n\n| Flag         | What it does                        |\n| ------------ | ----------------------------------- |\n| `--port <n>` | Port to listen on (default `4173`). |\n\nDirectory URLs map to their `index.html` (with or without a trailing slash) and `404.html` is served for missing routes when present.\n\n## `jx schema`\n\nGenerate the project's JSON Schema entry documents — `project.schema.json` and `document.schema.json` — by composing the core schemas with the fragments shipped by each extension in `project.json`'s `extensions`. The outputs are written into the project root as committed, **self-contained** artifacts: the core and fragment resources are embedded under `$defs` keyed by their canonical `$id`s, so editors resolve them offline with no `node_modules`, network, or editor configuration. No flags.\n\n```\njx schema [root]\n```\n\nRe-run it whenever you change the `extensions` list so editors and `jx validate` see the current schema.\n\n## `jx validate`\n\nValidate the whole project tree (run `jx schema` first). No flags.\n\n```\njx validate [root]\n```\n\nChecks, in order: that both committed entry documents are self-contained (every `$ref` a root-relative JSON Pointer that resolves in the same file — otherwise it asks you to regenerate with `jx schema`, and stops there, since the remaining checks compile those files); `project.json` against the generated `project.schema.json`; every document under `components/`, `pages/`, and `layouts/` against the bundled document schema; every project-local `*.class.json` against the class schema; and each enabled extension's schema fragments compile standalone.\n\nPrints `Project is valid (N files checked)` on success; otherwise lists each file's violations with their instance paths and exits `1`.\n\nThe output shape is meant to be read by whatever wrote the file — a person or a generator: one line naming the file, then one `- <instance path>: <message>` line per violation. Paired with the non-zero exit it drops straight into a write-check-fix loop. Because `jx schema` embeds every core and fragment resource under `$defs` keyed by its canonical `$id`, the document checks resolve from the committed entry documents alone — no network fetch and no module resolution — so an editor's inline squiggles and this command are looking at exactly the same schema.\n\n## `jx db push`\n\nAdditively sync the tables declared in `project.json`'s `data` section to their `connections` databases, through each connection's connector. Credentials come from the environment merged with the project's `.dev.vars` file. After a real (non-dry-run) apply, each connector's binding fragments are merged into `wrangler.jsonc`, preserving your keys.\n\n```\njx db push [root] [--dry-run] [--connection <name>]\n```\n\n| Flag                  | What it does                                     |\n| --------------------- | ------------------------------------------------ |\n| `--dry-run`           | Print the SQL statements without executing them. |\n| `--connection <name>` | Restrict the push to one `connections` entry.    |\n\nThe sync is additive-only — it creates missing tables and columns and never drops or rewrites existing ones. Per-connection output lists the tables, statements, and warnings; a missing `connections` section or an unknown connection name is an error.\n\nThe CLI talks to each connection exactly as declared: a `d1` connection reaches the real D1 database (through its binding, or the D1 HTTP API with `CLOUDFLARE_API_TOKEN` plus the account and database ids). Studio's **Push Schema** button runs through the dev server, which substitutes a local stand-in for any provider that declares one — so a D1 connection pushed from Studio lands in `.jx/data/<connection>.sqlite`, while the same connection pushed from a terminal lands in D1 itself.\n\n:::doc-warning\n`jx db push` covers the `data` section only. The auth extension's account tables (`user`, `session`, `account`, `verification`) come from a separate push step that Studio's **Push Schema** button and the dev server run, and the dev server also creates them on its first request to `/_jx/auth` — the CLI push does not include them today. A database whose schema only ever came from `jx db push` has no account tables, and sign-in fails against it at runtime.\n:::\n\n## What the scaffolded scripts run\n\n| Script                     | Runs                                             |\n| -------------------------- | ------------------------------------------------ |\n| `bun run build`            | `jx build`                                       |\n| `bun run dev`              | `jx dev`                                         |\n| `bun run preview`          | `jx preview`                                     |\n| `bun run deploy` (CF only) | `wrangler deploy` / `wrangler pages deploy dist` |\n\n## Related\n\n- [How compilation works](/docs/framework/build) — what `jx build` produces and why\n- [The dev server](/docs/framework/build/dev-server) — local development without a build\n- [Site architecture](/docs/framework/site) — the project layout these commands operate on\n- [Data tables](/docs/studio/data/tables) — the tables and push plan behind `jx db push`\n",
    "section": "Framework",
    "slug": "framework/build/cli",
    "title": "CLI commands",
    "url": "https://jxsuite.com/docs/framework/build/cli/"
  },
  {
    "description": "Why a Jx project is tractable for a language model, the three places an agent meets one, and the generate-validate-fix loop that keeps its work honest.",
    "markdown": "\n# Working with agents\n\nA Jx project is a folder of JSON documents, and that one fact changes what it means to hand a website to a language model. The model edits the same `pages/index.json` a person edits in Studio — not a generated intermediate, not a pile of framework code standing in for the page. There is one artifact, and everybody works on it: you, Studio, and the agent.\n\nThis page explains why that shape suits a model, where agents plug in, and the loop that turns a plausible answer into a checked one.\n\n## Why documents are tractable where code is not\n\nPrompt-to-code tools produce programs. A program is \"correct\" only in the sense that it runs, and finding out whether it runs means executing it, in an environment, with a browser attached. A Jx document is a different kind of output:\n\n- **The shape is bounded.** Every Jx file validates against one of three published JSON Schemas — one for documents, one for `project.json`, one for `.class.json`. A key either exists in the schema or it does not, and an invented `\"styles\"` where the schema says `\"style\"` is caught by a validator rather than by a blank page.\n- **Edits are structural, not textual.** A page is a tree of addressable nodes, so \"add a button to the header\" is an insertion at a path, not a search-and-replace over source text. That is how Studio's assistant edits the canvas live, and why an external agent can change one property without touching the rest of the file.\n- **Every change is a reviewable diff.** JSON diffs on property boundaries, so a small change in `git diff` is a small change in the page. Machine-written work needs review more than hand-written work does, and this is the format that makes review cheap.\n\nNone of this makes a model correct. It makes a model **checkable** — and that is the property you can build a workflow on.\n\n## Three ways an agent meets a Jx project\n\n### Studio's built-in assistant\n\nA chat sidebar inside the editor that works on your project rather than talking about it: it creates pages and components, edits the document on the canvas while you watch, and answers questions by reading your files. It runs against an AI provider you connect, and everything it writes as a Jx document is validated before it lands. See **[AI assistant](/docs/studio/ai)**.\n\n### An external coding agent\n\nClaude Code, Cursor, or your own script, working on the project folder like any other repository. Nothing is Jx-specific about the setup — the agent reads and writes files — but a briefing pays for itself, because the schema encodes rules the model would otherwise guess at (style is an object, IDL properties sit on the element, layouts use `slot`). See **[Authoring rules for agents](/docs/framework/agents/authoring-rules)**.\n\n### Validation in CI\n\n`jx validate` exits non-zero on any violation, so the command an agent runs after each edit is the command your pipeline can run on the pull request. Agent work and hand-written work pass through one gate, and the gate does not care which is which.\n\n## The generate-validate-fix loop\n\n**1. Generate the project's schemas — `jx schema`.**\n\nRun it once per project, and again after any change to `project.json`'s `extensions` array. It composes the core schemas with the fragments each enabled extension ships and writes two entry documents into the project root, `project.schema.json` and `document.schema.json`. Both are **self-contained single-resource schemas**: every referenced resource is embedded under `$defs` and every `$ref` is a root-relative JSON Pointer, so they resolve with no `node_modules` and no network. One resolution behavior for every consumer — your editor, Studio's Monaco, and `jx validate` alike. See [Schema composition](/docs/extending/extensions/schema-composition).\n\n**2. Let the agent write files.**\n\n`project.json` binds itself with `\"$schema\": \"./project.schema.json\"`, so an editor with JSON language support autocompletes and underlines as the agent types. Documents under `pages/`, `layouts/`, and `components/` carry no `$schema` key of their own — `document.schema.json` is the schema they are checked against, in step 3.\n\n**3. Check the work — `jx validate`.**\n\nThe deterministic gate, run after every edit. It walks the project in five passes:\n\n1. Both committed entry documents, for self-containment — a `$ref` that is a relative path, a URI, or a pointer to nothing means they are stale, and the fix is to re-run `jx schema`. This runs first and stops the walk, because every later pass compiles one of these files.\n2. `project.json` against the generated entry schema.\n3. Every document under `components/`, `pages/`, and `layouts/` against the bundled document schema.\n4. Every project-local `*.class.json` against the class schema.\n5. Each enabled extension's schema fragments, compiled standalone.\n\nClean runs print `Project is valid (N files checked in <root>)`. Failures print `Project is INVALID`, then one block per file:\n\n```\npages/about.json:\n  - /children/0: must NOT have additional properties\n```\n\n**4. Feed the failures back verbatim.**\n\nEach line already names the file, the JSON pointer into it, and what is wrong — a complete fix instruction with nothing left to infer. Loop until validation is clean, then run `jx build`.\n\n:::doc-tip\nWire steps 1 and 3 into the agent's own instructions (\"run `jx validate` after every file you write; do not report success until it passes\") rather than running them yourself. An agent that can check its own work will, and the loop closes without you in it.\n:::\n\n## What an agent can build\n\nThe whole framework is reachable from JSON, so the ceiling is higher than \"a static page\":\n\n- **Pages, layouts, and components** — routing from the `pages/` tree, shared shells with `slot`, reusable custom elements. See [Site architecture](/docs/framework/site).\n- **Content collections** — folders of Markdown, JSON, or CSV files with schema validation, queried into pages. See [Content collections](/docs/framework/site/content-collections).\n- **Databases** — `connections` and `data` sections in `project.json` become real tables (D1, Supabase, or SQLite) with CRUD and form actions. See [Databases](/docs/studio/data).\n- **Server functions** — a state entry with `timing: \"server\"` compiles into a server handler your secrets can live behind. See [Timing](/docs/framework/concepts/timing).\n- **Accounts and sessions** — the auth extension adds sign-in, sessions, and per-table permissions. See [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\nOne boundary is worth telling the agent up front: **pages are prerendered at build time**, and interactivity arrives as hydration islands. There is no per-request page rendering, and the route set is fixed when the build runs — a page that must exist per visitor is a client-side view over server data, not server-rendered HTML.\n\nThe other thing to say up front is the adapter. A project with connection-backed data tables **must** set a server-capable `build.adapter` — `cloudflare-workers`, `cloudflare-pages`, `node`, or `bun` — and the build fails on static. Server functions want one too, since that is what packages them into a worker a host will actually run. See [Build output and adapters](/docs/framework/site/deployment).\n\n## Related\n\n- [Authoring rules for agents](/docs/framework/agents/authoring-rules) — the briefing to hand an external coding agent\n- [Machine-readable docs](/docs/framework/agents/machine-readable) — the schema and documentation endpoints an agent can fetch\n- [CLI commands](/docs/framework/build/cli) — every flag of `jx schema`, `jx validate`, and the rest\n- [Schema composition](/docs/extending/extensions/schema-composition) — how the entry documents are assembled\n- [AI assistant](/docs/studio/ai) — the agent that ships inside Studio\n",
    "section": "Framework",
    "slug": "framework/agents",
    "title": "Working with agents",
    "url": "https://jxsuite.com/docs/framework/agents/"
  },
  {
    "description": "A briefing to paste into a coding agent: the three schemas, the five state shapes, $ref forms, style and attribute rules, and how it checks its own work.",
    "markdown": "\n# Authoring rules for agents\n\nAn external coding agent — Claude Code, Cursor, a script against an API — needs no Jx-specific tooling to work on a project. It reads and writes files like any repository. What it does need is a briefing, because a model that has never seen Jx will reach for the conventions of frameworks it has seen: a CSS string where the format wants an object, an `attributes` bag where the property belongs on the element.\n\nEverything below is the briefing. Paste it into a system prompt, drop it in the agent's project instructions file, or point the agent at this page. The monorepo keeps a version of it as a `/jx` slash command at [`.claude/commands/jx.md`](https://github.com/jxsuite/jx/blob/main/.claude/commands/jx.md), which you can copy into your own project's `.claude/commands/`.\n\n:::doc-tip\nWhatever form you use, include the **check your work** section at the end. A briefing without a verification step tells the agent what good looks like but gives it no way to find out whether it got there.\n:::\n\n## The three schemas\n\nEvery Jx file validates against one of three JSON Schema 2020-12 documents, published at stable URLs:\n\n| File                                  | Schema                                  |\n| ------------------------------------- | --------------------------------------- |\n| Components, pages, layouts (`*.json`) | `https://jxsuite.com/schema/v1`         |\n| `project.json`                        | `https://jxsuite.com/schema/project/v1` |\n| Class definitions (`*.class.json`)    | `https://jxsuite.com/schema/class/v1`   |\n\nInside a project, prefer the generated local copies over the network: `jx schema` writes `project.schema.json` and `document.schema.json` into the project root, composed from the core schemas plus the fragments each enabled extension ships. Each is a self-contained single-resource schema — every `$ref` a root-relative JSON Pointer into the same file — so an editor resolves it offline with no `node_modules`, no network, and no configuration (see [Machine-readable docs](/docs/framework/agents/machine-readable) and [Schema composition](/docs/extending/extensions/schema-composition)). `project.json` binds itself with `\"$schema\": \"./project.schema.json\"`. Documents carry no `$schema` key in the shipped starters — `jx validate` checks them against `document.schema.json` either way — so add one only if you want editor autocomplete. It resolves relative to the file that carries it, so count the folder levels: `\"../document.schema.json\"` from `pages/index.json`, `\"../../document.schema.json\"` from `pages/blog/post.json`. A pointer with too few `../` names a file that does not exist, and validators report the unresolvable schema instead of checking the document at all.\n\n## Project structure\n\n```\nmy-site/\n  project.json       # Required. Site config, global styles, collections, build settings\n  pages/             # File-based routing. Each file = a route. [slug].json = dynamic\n  layouts/           # Shared page shells. Use { \"tagName\": \"slot\" } for content insertion\n  components/        # Reusable custom elements\n  content/           # Markdown/JSON/CSV content collections\n  public/            # Static assets copied verbatim to dist/\n```\n\nFiles and folders prefixed with `_` inside `pages/` are excluded from routing, so a page's private components can sit beside it.\n\n## The document\n\nA Jx document is a JSON object describing a reactive web component:\n\n```json\n{\n  \"$id\": \"TaskList\",\n  \"tagName\": \"task-list\",\n  \"$defs\": {},\n  \"state\": {},\n  \"style\": {},\n  \"children\": []\n}\n```\n\nOnly `tagName` is required. A `tagName` containing a hyphen makes the document a custom element.\n\n### State — five shapes, detected by structure\n\n**1. Naked value** — a scalar, array, or plain object with no reserved keys:\n\n```json\n\"count\": 0,\n\"items\": [],\n\"user\": { \"name\": \"\", \"email\": \"\" }\n```\n\n**2. Typed value** — has `default`, optionally `type`:\n\n```json\n\"status\": { \"type\": { \"type\": \"string\", \"enum\": [\"idle\", \"loading\"] }, \"default\": \"idle\" }\n```\n\n**3. Computed** — a string containing `${}`:\n\n```json\n\"fullName\": \"${state.firstName} ${state.lastName}\",\n\"itemCount\": \"${state.items.length} items\"\n```\n\n**4. Function** — `$prototype: \"Function\"`, with an inline `body` or an external `.js` sidecar:\n\n```json\n\"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" },\n\"handleInput\": { \"$prototype\": \"Function\", \"arguments\": [\"event\"], \"body\": \"state.value = event.target.value\" },\n\"validate\": { \"$prototype\": \"Function\", \"$src\": \"./validators.js\", \"$export\": \"validateEmail\" }\n```\n\n**5. Data source** — `$prototype: <ClassName>`:\n\n```json\n\"userData\": { \"$prototype\": \"Request\", \"url\": \"/api/users\", \"method\": \"GET\" },\n\"posts\": { \"$prototype\": \"ContentCollection\", \"contentType\": \"blog\", \"sort\": { \"field\": \"pubDate\", \"order\": \"desc\" } }\n```\n\nSee [State](/docs/framework/concepts/state) and [Data prototypes](/docs/framework/concepts/data-prototypes).\n\n### `$ref` bindings\n\n| Pattern   | Example                          | Meaning                               |\n| --------- | -------------------------------- | ------------------------------------- |\n| State     | `{ \"$ref\": \"#/state/count\" }`    | Reactive binding to state             |\n| `$defs`   | `{ \"$ref\": \"#/$defs/TodoItem\" }` | Type definition reference             |\n| Parent    | `{ \"$ref\": \"parent#/theme\" }`    | A value passed in via `$props`        |\n| Map item  | `{ \"$ref\": \"$map/item\" }`        | Current item in an iteration          |\n| Map index | `{ \"$ref\": \"$map/index\" }`       | Current zero-based index              |\n| External  | `{ \"$ref\": \"./card.json\" }`      | Another Jx document                   |\n| Window    | `{ \"$ref\": \"window#/Math/max\" }` | A path resolved off the global object |\n\nUse `${}` templates for inline one-off bindings, `$ref` objects for named or reused signals. See [References](/docs/framework/concepts/references).\n\n### Children\n\nStatic — an array of element objects and text strings, mixed freely:\n\n```json\n\"children\": [\n  { \"tagName\": \"h1\", \"textContent\": \"Hello\" },\n  { \"tagName\": \"p\", \"children\": [\"Welcome to \", { \"tagName\": \"strong\", \"textContent\": \"Jx\" }] }\n]\n```\n\nDynamic list — `$prototype: \"Array\"`:\n\n```json\n\"children\": {\n  \"$prototype\": \"Array\",\n  \"items\": { \"$ref\": \"#/state/todos\" },\n  \"map\": {\n    \"tagName\": \"li\",\n    \"className\": \"${$map.item.done ? 'completed' : ''}\",\n    \"textContent\": { \"$ref\": \"$map/item/text\" }\n  }\n}\n```\n\nSee [Lists and iteration](/docs/framework/concepts/lists).\n\n### `$switch` and `cases`\n\n```json\n{\n  \"$switch\": { \"$ref\": \"#/state/currentView\" },\n  \"cases\": {\n    \"home\": { \"$ref\": \"./views/home.json\" },\n    \"settings\": { \"$ref\": \"./views/settings.json\" }\n  }\n}\n```\n\nSee [Dynamic switching](/docs/framework/concepts/switching).\n\n### Style\n\nAn object of camelCase CSS properties. Nested selectors are keys prefixed with `:`, `.`, `&`, or `[`; breakpoints are `@--name` (a named breakpoint from `$media`) or `@(query)` (a literal media query):\n\n```json\n\"style\": {\n  \"display\": \"flex\",\n  \"gap\": \"1rem\",\n  \"padding\": \"clamp(1rem, 3vw, 2rem)\",\n  \":hover\": { \"backgroundColor\": \"#f0f0f0\" },\n  \"&.active\": { \"borderColor\": \"blue\" },\n  \"@--md\": { \"flexDirection\": \"row\" },\n  \"@(prefers-reduced-motion: reduce)\": { \"transition\": \"none\" }\n}\n```\n\nSee [Styling](/docs/framework/concepts/styling).\n\n### Properties and attributes\n\nIDL properties — `href`, `src`, `textContent`, `className`, `hidden`, `disabled`, `value`, `checked` — go directly on the element object. Everything else (`aria-*`, `data-*`, `role`, `slot`) goes in `attributes`:\n\n```json\n{\n  \"tagName\": \"a\",\n  \"href\": \"/about\",\n  \"textContent\": \"About\",\n  \"attributes\": { \"aria-label\": \"About page\", \"data-section\": \"nav\" }\n}\n```\n\n### Event handlers\n\nReference a function from state:\n\n```json\n{ \"tagName\": \"button\", \"textContent\": \"Add\", \"onclick\": { \"$ref\": \"#/state/handleAdd\" } }\n```\n\n### Components and props\n\n`$elements` is an **array** of `$ref` objects (paths relative to the current file) and bare npm specifiers for web-component libraries. The registered tag is the referenced document's own `tagName`. Instantiate it by tag and pass data with `$props` — a bare `{ \"$ref\": \"./card.json\" }` in `children` is not a component instance:\n\n```json\n{\n  \"$elements\": [\n    { \"$ref\": \"./components/user-card.json\" },\n    \"@shoelace-style/shoelace/components/button/button.js\"\n  ],\n  \"children\": [\n    {\n      \"tagName\": \"user-card\",\n      \"$props\": { \"name\": \"Ada\", \"count\": { \"$ref\": \"#/state/count\" } }\n    }\n  ]\n}\n```\n\n`props.*` attributes (`\"attributes\": { \"props.name\": \"Ada\" }`) are an equivalent shorthand for **string** values only, and the key must be lowercase because HTML lowercases attribute names. Anything bound or non-string belongs in `$props`. See [Props and scope](/docs/framework/concepts/props-and-scope).\n\n## Pages\n\nPages are documents with a few extra top-level fields:\n\n```json\n{\n  \"title\": \"About us\",\n  \"$layout\": \"./layouts/base.json\",\n  \"$head\": [\n    { \"tagName\": \"meta\", \"attributes\": { \"name\": \"description\", \"content\": \"About our team\" } }\n  ],\n  \"tagName\": \"main\",\n  \"children\": []\n}\n```\n\n- `$layout` — a path resolved from the **project root**, or `false` for no layout. Omit it to use `defaults.layout` from `project.json`.\n- `$head` — merges with the layout's and the project's entries; the page wins on conflicts.\n- `$paths` — the concrete routes a `[param]` page generates. One source shape, checked strictly by the generated document schema: `{ contentType, param, field }` (needs `@jxsuite/parser`), `{ values, param }`, `{ \"$ref\": \"./data.json\", param, field }`, or a bare array of parameter objects. A key that is not part of one of these is an error, not a silently empty build.\n- `tagName` is optional on a page that uses a layout.\n\nDynamic route (`pages/blog/[slug].json`):\n\n```json\n{\n  \"$paths\": { \"contentType\": \"blog\", \"param\": \"slug\" },\n  \"state\": {\n    \"post\": {\n      \"$prototype\": \"ContentEntry\",\n      \"contentType\": \"blog\",\n      \"id\": { \"$ref\": \"#/$params/slug\" }\n    }\n  }\n}\n```\n\nSee [Routing](/docs/framework/site/routing) and [Content collections](/docs/framework/site/content-collections).\n\n## Layouts\n\nOrdinary documents with `<slot>` elements marking where page content lands. Name a slot to define more than one region:\n\n```json\n{\n  \"tagName\": \"div\",\n  \"children\": [\n    { \"tagName\": \"header\" },\n    { \"tagName\": \"main\", \"children\": [{ \"tagName\": \"slot\" }] },\n    {\n      \"tagName\": \"aside\",\n      \"children\": [{ \"tagName\": \"slot\", \"attributes\": { \"name\": \"sidebar\" } }]\n    },\n    { \"tagName\": \"footer\" }\n  ]\n}\n```\n\nSee [Layouts](/docs/framework/site/layouts).\n\n## project.json\n\n```json\n{\n  \"$schema\": \"./project.schema.json\",\n  \"name\": \"My Site\",\n  \"url\": \"https://example.com\",\n  \"extensions\": [\"@jxsuite/parser\"],\n  \"defaults\": { \"layout\": \"./layouts/base.json\", \"lang\": \"en\" },\n  \"$head\": [{ \"tagName\": \"link\", \"attributes\": { \"rel\": \"icon\", \"href\": \"/favicon.svg\" } }],\n  \"imports\": { \"MarkdownCollection\": \"@jxsuite/parser/MarkdownCollection.class.json\" },\n  \"$media\": { \"--sm\": \"(min-width: 640px)\", \"--md\": \"(min-width: 768px)\" },\n  \"style\": { \"fontFamily\": \"system-ui, sans-serif\", \"margin\": \"0\" },\n  \"content\": {\n    \"blog\": {\n      \"source\": \"./content/blog/\",\n      \"schema\": {\n        \"type\": \"object\",\n        \"properties\": { \"title\": { \"type\": \"string\" } },\n        \"required\": [\"title\"]\n      }\n    }\n  },\n  \"build\": { \"outDir\": \"./dist\", \"trailingSlash\": \"always\" }\n}\n```\n\n`extensions` is the enablement list, and no Studio panel edits it: turning on the connector, auth, or search extension means adding its package name to this array by hand — a good job for the agent — then re-running `jx schema`. Studio picks up the change once it is on disk. See [project.json](/docs/framework/site/project-json) and [First-party extensions](/docs/extending/extensions/first-party).\n\n## `.class.json`\n\n```json\n{\n  \"$prototype\": \"Class\",\n  \"title\": \"MyParser\",\n  \"$implementation\": \"./my-parser.js\",\n  \"$defs\": {\n    \"parameters\": { \"src\": { \"identifier\": \"src\", \"type\": { \"type\": \"string\" } } },\n    \"constructor\": {\n      \"role\": \"constructor\",\n      \"$prototype\": \"Function\",\n      \"parameters\": [{ \"$ref\": \"#/$defs/parameters/src\" }]\n    },\n    \"methods\": {\n      \"resolve\": { \"role\": \"method\", \"identifier\": \"resolve\", \"returnType\": { \"type\": \"object\" } }\n    }\n  }\n}\n```\n\nSee [Custom classes](/docs/extending/extensions/classes).\n\n## The server tier\n\nA brochure site is not the ceiling. Three additions turn a Jx project into an application, and an agent should know they exist:\n\n- **Server functions.** A state entry with `timing: \"server\"` names an async export via `$src` and `$export` — no `$prototype`. It compiles to `POST /_jx/server/<exportName>`, and the function receives `(args, env)` where `env` carries the platform's bindings. Secrets stay in that process. See [Timing](/docs/framework/concepts/timing).\n- **Databases.** Add `@jxsuite/connector` to `extensions`, then declare `connections` (D1, Supabase, or SQLite) and `data` tables in `project.json`. `jx db push` applies the schema additively. Form actions and table queries read and write over `/_jx/data`. See [Databases](/docs/studio/data).\n- **Accounts.** Add `@jxsuite/auth` and an `auth` section for sign-in, sessions, roles, and per-table permissions. See [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\nTwo rules bind all three. **Secrets are never values in `project.json`** — the file records environment-variable _names_ only, and the values live in `.dev.vars` locally and in your host's environment when deployed. And any of this needs a server-capable `build.adapter` (`cloudflare-workers`, `cloudflare-pages`, `node`, or `bun`), which packages the server tier into a deployable worker; with connection-backed data tables it is not optional — the build **fails** on static. See [Build output and adapters](/docs/framework/site/deployment).\n\n## Hard rules\n\n1. `style` is always an **object** of camelCase properties, never a CSS string.\n2. Use `\"textContent\"` for leaf elements; use `\"children\": [\"text\", …]` for mixed content.\n3. IDL properties go on the element; everything else goes in `attributes`.\n4. Prefer CSS `clamp()` over media queries for simple responsive values.\n5. A component's `tagName` must contain a hyphen (Web Components requires it).\n6. `$defs` holds JSON Schema type definitions only — no functions, no defaults, no runtime artifacts.\n7. Template strings (`${}`) are pure expressions — no statements, no assignments.\n8. Function `body` strings are raw JavaScript where `state` is in scope; a sidecar export takes `state` as its first parameter.\n9. `$head` entries are `{ tagName, attributes }` objects, never HTML strings.\n10. Layouts inject content with `{ \"tagName\": \"slot\" }` — there is no `$slot` or `$content`.\n11. All state entries are reactive by default; no `signal: true` flag exists.\n12. `timing` (`\"compiler\"`, `\"server\"`, `\"client\"`) decides where a data source resolves. `\"client\"` is the default.\n\n## Check your work\n\nNever report a task complete on the strength of the output looking right. Run the loop:\n\n```bash\njx schema     # once per project, and after any change to project.json's \"extensions\"\njx validate   # after every edit — exits 1 with the failing instance paths\njx build      # only once validate is clean\n```\n\n`jx validate` prints one block per failing file:\n\n```\npages/about.json:\n  - /children/0: must NOT have additional properties\n```\n\nEach line names the file, the JSON pointer, and the violation — enough to fix without guessing. Repeat until it prints `Project is valid`. The same command gates CI, so nothing is gained by skipping it. See [CLI commands](/docs/framework/build/cli).\n\n## Related\n\n- [Working with agents](/docs/framework/agents) — where this briefing fits in the wider workflow\n- [Machine-readable docs](/docs/framework/agents/machine-readable) — the schema and documentation URLs an agent can fetch\n- [Documents](/docs/framework/concepts/documents) — the long-form version of the document model\n- [Site architecture](/docs/framework/site) — the project layout these rules describe\n",
    "section": "Framework",
    "slug": "framework/agents/authoring-rules",
    "title": "Authoring rules for agents",
    "url": "https://jxsuite.com/docs/framework/agents/authoring-rules/"
  },
  {
    "description": "Public endpoints an agent can fetch: llms.txt, full-docs.json, the site search index, and the canonical Jx JSON Schemas served from jxsuite.com.",
    "markdown": "\n# Machine-readable docs\n\nEverything on this site has a machine-readable counterpart at a stable URL. An agent working on a Jx project can fetch the documentation corpus, the search index, or the schemas themselves without scraping HTML — and none of it is a separate artifact that drifts, because all of it is regenerated from the same sources on every build.\n\n## The documentation corpus\n\n### `llms.txt`\n\n[`https://jxsuite.com/llms.txt`](https://jxsuite.com/llms.txt)\n\nA plain-text index in the [llms.txt convention](https://llmstxt.org): a one-paragraph summary of the project, then one `##` heading per documentation section, then one line per page in sidebar order:\n\n```\n# Jx Suite\n\n> Jx Suite is a visual site builder (Jx Studio), a JSON-native component format, and a compiler that prerenders every page — working on plain files you own and publish with git. …\n\n## Start here\n\n- [Start here](https://jxsuite.com/docs/start/): What Jx Suite is, how to install Jx Studio, and where to go first …\n- [Install Jx Studio](https://jxsuite.com/docs/start/install/): Download the Jx Studio desktop app …\n- [Your first project](https://jxsuite.com/docs/start/first-project/): …\n```\n\nEach section's landing page comes first, then its children, exactly as the sidebar orders them.\n\nIt carries no page bodies — roughly 27 KB against `full-docs.json`'s ~680 KB — which makes it the cheap first fetch: point an agent at it, let it read the titles and descriptions, and let it pull only the pages it needs.\n\n### `full-docs.json`\n\n[`https://jxsuite.com/docs/full-docs.json`](https://jxsuite.com/docs/full-docs.json)\n\nThe same corpus in the same order, with the prose included. A JSON array of objects, one per page:\n\n| Field         | What it holds                                           |\n| ------------- | ------------------------------------------------------- |\n| `slug`        | The docs path, e.g. `framework/agents/machine-readable` |\n| `url`         | The canonical page URL                                  |\n| `title`       | The page's frontmatter `title`                          |\n| `description` | The page's frontmatter `description`                    |\n| `section`     | The top-level section label, e.g. `Framework`           |\n| `markdown`    | The full page body as Markdown, frontmatter stripped    |\n\nThis is the fetch for an agent that wants the whole thing at once — one request, no crawling, and the `markdown` field is the same text a contributor edits in the repository.\n\n### `search-index.json`\n\n[`https://jxsuite.com/search-index.json`](https://jxsuite.com/search-index.json)\n\nThe index that powers the site's own search box, emitted by the [search extension](/docs/framework/site/search) during the build. An envelope — `version`, `engine` (`\"minisearch\"`), the indexed `fields`, and per-field `boost` weights — wrapped around a `documents` array. Each document is one page or one heading-level section within a page, carrying `id`, `collection`, `slug`, `url`, `title`, `description`, `heading`, and `text`.\n\nReach for it when you want lookup rather than reading: it is section-granular, so a query lands on a heading instead of a whole page.\n\n:::doc-note\n`llms.txt` and `full-docs.json` are written by `scripts/docs/build-llm-export.ts`, which runs immediately after `jx build` in the jxsuite.com build script. `search-index.json` is emitted by the search extension during that same build. All three are derived, never committed — so they cannot drift from `/docs`.\n:::\n\n## The schemas\n\nThe other machine-readable surface is the format itself. The Jx schemas are plain JSON Schema 2020-12 documents, served at the URLs that are also their canonical `$id`s:\n\n| URL                                                                                            | Validates                                                                        |\n| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| [`https://jxsuite.com/schema/v1`](https://jxsuite.com/schema/v1)                               | Components, pages, and layouts                                                   |\n| [`https://jxsuite.com/schema/project/v1`](https://jxsuite.com/schema/project/v1)               | `project.json`                                                                   |\n| [`https://jxsuite.com/schema/class/v1`](https://jxsuite.com/schema/class/v1)                   | `*.class.json` class definitions                                                 |\n| [`https://jxsuite.com/schema/document/paths/v2`](https://jxsuite.com/schema/document/paths/v2) | A page's `$paths` source — referenced by `schema/v1`, and overridden per project |\n\nAny compliant 2020-12 validator can consume them directly — there is no Jx-specific validation runtime. They are published from `packages/schema` through the `copy` map in the site's `project.json`, so the served documents are byte-for-byte the schemas the toolchain uses.\n\nFor work inside a project, prefer the local composed copies. `jx schema` writes `project.schema.json` and `document.schema.json` into the project root, merging these core schemas with the fragments each enabled extension ships and inlining every referenced resource. Those files resolve offline — no `node_modules`, no network, no editor configuration — and they are what `jx validate` checks against. The hosted URLs above are the right reference when you are outside a project or want the core format without any extension's additions — note that the hosted `$paths` union accepts any extension source shape it cannot see, where your project's generated copy checks the exact set your extensions provide. See [Schema composition](/docs/extending/extensions/schema-composition).\n\n## Related\n\n- [Working with agents](/docs/framework/agents) — the workflow these endpoints support\n- [Authoring rules for agents](/docs/framework/agents/authoring-rules) — the briefing to hand an agent alongside them\n- [CLI commands](/docs/framework/build/cli) — `jx schema` and `jx validate`\n- [Site search](/docs/framework/site/search) — how the search index is configured and emitted\n- [Contributing to these docs](/docs/extending/contributing/docs) — where the corpus comes from\n",
    "section": "Framework",
    "slug": "framework/agents/machine-readable",
    "title": "Machine-readable docs",
    "url": "https://jxsuite.com/docs/framework/agents/machine-readable/"
  },
  {
    "description": "Generated reference pages — the formula catalog and the blessed operator set, produced from the packages that ship them.",
    "markdown": "\n# Reference\n\nThe pages in this section are **generated from product data** at build time and committed — they cannot drift from what actually ships. To change one, change the package it documents and run `bun run docs:generate`.\n\n- **[Formula catalog](/docs/framework/reference/formulas/)** — every composite formula in `@jxsuite/formulas`, with parameters and expression bodies.\n- **[Operator reference](/docs/framework/reference/operators/)** — the closed operator set the Jx schema admits in `$expression` trees.\n\nExtension authors: the [protocol route reference](/docs/extending/reference/studio-routes/) is generated the same way from `@jxsuite/protocol`.\n",
    "section": "Framework",
    "slug": "framework/reference",
    "title": "Reference",
    "url": "https://jxsuite.com/docs/framework/reference/"
  },
  {
    "description": "Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Formula catalog\n\nThese composite formulas ship with Jx and appear in Studio's formula palette. Inserting one copies its pure `$expression` body into your document — there is no runtime dependency on this catalog. The blessed operator set the bodies draw from is in the [operator reference](/docs/framework/reference/operators/).\n\n## average\n\nArithmetic mean of an array of numbers; 0 for an empty array.\n\n| Parameter | Type     | Description |\n| --------- | -------- | ----------- |\n| `values`  | number[] | —           |\n\n```json\n{\n  \"operator\": \"?:\",\n  \"target\": {\n    \"$ref\": \"$args/values/length\"\n  },\n  \"value\": {\n    \"operator\": \"/\",\n    \"target\": {\n      \"operator\": \"reduce\",\n      \"target\": {\n        \"$ref\": \"$args/values\"\n      },\n      \"initial\": 0,\n      \"value\": {\n        \"operator\": \"+\",\n        \"target\": {\n          \"$ref\": \"$reduce/acc\"\n        },\n        \"value\": {\n          \"$ref\": \"$map/item\"\n        }\n      }\n    },\n    \"value\": {\n      \"$ref\": \"$args/values/length\"\n    }\n  },\n  \"initial\": 0\n}\n```\n\n## capitalize\n\nUppercase the first character of a string, leaving the rest unchanged (the CSS text-transform: capitalize shape for a single word).\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `text`    | string | —           |\n\n```json\n{\n  \"operator\": \"+\",\n  \"target\": {\n    \"operator\": \"toUpperCase\",\n    \"target\": {\n      \"operator\": \"charAt\",\n      \"target\": {\n        \"$ref\": \"$args/text\"\n      },\n      \"value\": 0\n    }\n  },\n  \"value\": {\n    \"operator\": \"slice\",\n    \"target\": {\n      \"$ref\": \"$args/text\"\n    },\n    \"value\": 1\n  }\n}\n```\n\n## clamp\n\nBound a number to the [min, max] range (the TC39 Math.clamp shape): returns min when below, max when above, the value itself otherwise.\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `value`   | number | —           |\n| `min`     | number | —           |\n| `max`     | number | —           |\n\n```json\n{\n  \"operator\": \"call\",\n  \"target\": {\n    \"$ref\": \"window#/Math/min\"\n  },\n  \"value\": [\n    {\n      \"operator\": \"call\",\n      \"target\": {\n        \"$ref\": \"window#/Math/max\"\n      },\n      \"value\": [\n        {\n          \"$ref\": \"$args/value\"\n        },\n        {\n          \"$ref\": \"$args/min\"\n        }\n      ]\n    },\n    {\n      \"$ref\": \"$args/max\"\n    }\n  ]\n}\n```\n\n## compact\n\nA copy of an array with the falsy elements removed (Array.prototype.filter on truthiness).\n\n| Parameter | Type      | Description |\n| --------- | --------- | ----------- |\n| `values`  | unknown[] | —           |\n\n```json\n{\n  \"operator\": \"filter\",\n  \"target\": {\n    \"$ref\": \"$args/values\"\n  },\n  \"value\": {\n    \"operator\": \"!\",\n    \"target\": {\n      \"operator\": \"!\",\n      \"target\": {\n        \"$ref\": \"$map/item\"\n      }\n    }\n  }\n}\n```\n\n## count\n\nThe length of an array or string, or 0 when the value is missing.\n\n| Parameter | Type      | Description |\n| --------- | --------- | ----------- |\n| `values`  | unknown[] | string      | —   |\n\n```json\n{\n  \"operator\": \"??\",\n  \"target\": {\n    \"$ref\": \"$args/values/length\"\n  },\n  \"value\": 0\n}\n```\n\n## first\n\nThe first element of an array (index 0), or null when empty.\n\n| Parameter | Type      | Description |\n| --------- | --------- | ----------- |\n| `values`  | unknown[] | —           |\n\n```json\n{\n  \"operator\": \"??\",\n  \"target\": {\n    \"$ref\": \"$args/values/0\"\n  },\n  \"value\": null\n}\n```\n\n## initials\n\nThe uppercase first letters of each whitespace-separated word, joined (e.g. \"ada lovelace\" → \"AL\").\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `name`    | string | —           |\n\n```json\n{\n  \"operator\": \"join\",\n  \"target\": {\n    \"operator\": \"map\",\n    \"target\": {\n      \"operator\": \"split\",\n      \"target\": {\n        \"$ref\": \"$args/name\"\n      },\n      \"value\": \" \"\n    },\n    \"value\": {\n      \"operator\": \"toUpperCase\",\n      \"target\": {\n        \"operator\": \"charAt\",\n        \"target\": {\n          \"$ref\": \"$map/item\"\n        },\n        \"value\": 0\n      }\n    }\n  },\n  \"value\": \"\"\n}\n```\n\n## isEmpty\n\nTrue when an array or string has no elements (length 0 — a missing value counts as empty).\n\n| Parameter | Type      | Description |\n| --------- | --------- | ----------- |\n| `value`   | unknown[] | string      | —   |\n\n```json\n{\n  \"operator\": \"===\",\n  \"target\": {\n    \"operator\": \"??\",\n    \"target\": {\n      \"$ref\": \"$args/value/length\"\n    },\n    \"value\": 0\n  },\n  \"value\": 0\n}\n```\n\n## last\n\nThe last element of an array (Array.prototype.at(-1)), or undefined when empty.\n\n| Parameter | Type      | Description |\n| --------- | --------- | ----------- |\n| `values`  | unknown[] | —           |\n\n```json\n{\n  \"operator\": \"at\",\n  \"target\": {\n    \"$ref\": \"$args/values\"\n  },\n  \"value\": -1\n}\n```\n\n## max\n\nThe largest number in an array (Math.max folded over the values); undefined for an empty array.\n\n| Parameter | Type     | Description |\n| --------- | -------- | ----------- |\n| `values`  | number[] | —           |\n\n```json\n{\n  \"operator\": \"reduce\",\n  \"target\": {\n    \"$ref\": \"$args/values\"\n  },\n  \"initial\": {\n    \"$ref\": \"$args/values/0\"\n  },\n  \"value\": {\n    \"operator\": \"call\",\n    \"target\": {\n      \"$ref\": \"window#/Math/max\"\n    },\n    \"value\": [\n      {\n        \"$ref\": \"$reduce/acc\"\n      },\n      {\n        \"$ref\": \"$map/item\"\n      }\n    ]\n  }\n}\n```\n\n## min\n\nThe smallest number in an array (Math.min folded over the values); undefined for an empty array.\n\n| Parameter | Type     | Description |\n| --------- | -------- | ----------- |\n| `values`  | number[] | —           |\n\n```json\n{\n  \"operator\": \"reduce\",\n  \"target\": {\n    \"$ref\": \"$args/values\"\n  },\n  \"initial\": {\n    \"$ref\": \"$args/values/0\"\n  },\n  \"value\": {\n    \"operator\": \"call\",\n    \"target\": {\n      \"$ref\": \"window#/Math/min\"\n    },\n    \"value\": [\n      {\n        \"$ref\": \"$reduce/acc\"\n      },\n      {\n        \"$ref\": \"$map/item\"\n      }\n    ]\n  }\n}\n```\n\n## percent\n\npart / whole as a percentage (0 when the whole is 0 or missing).\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `part`    | number | —           |\n| `whole`   | number | —           |\n\n```json\n{\n  \"operator\": \"?:\",\n  \"target\": {\n    \"$ref\": \"$args/whole\"\n  },\n  \"value\": {\n    \"operator\": \"*\",\n    \"target\": {\n      \"operator\": \"/\",\n      \"target\": {\n        \"$ref\": \"$args/part\"\n      },\n      \"value\": {\n        \"$ref\": \"$args/whole\"\n      }\n    },\n    \"value\": 100\n  },\n  \"initial\": 0\n}\n```\n\n## roundTo\n\nRound a number to the given decimal places (Math.round against a Math.pow(10, digits) scale).\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `value`   | number | —           |\n| `digits`  | number | —           |\n\n```json\n{\n  \"operator\": \"/\",\n  \"target\": {\n    \"operator\": \"call\",\n    \"target\": {\n      \"$ref\": \"window#/Math/round\"\n    },\n    \"value\": [\n      {\n        \"operator\": \"*\",\n        \"target\": {\n          \"$ref\": \"$args/value\"\n        },\n        \"value\": {\n          \"operator\": \"call\",\n          \"target\": {\n            \"$ref\": \"window#/Math/pow\"\n          },\n          \"value\": [\n            10,\n            {\n              \"$ref\": \"$args/digits\"\n            }\n          ]\n        }\n      }\n    ]\n  },\n  \"value\": {\n    \"operator\": \"call\",\n    \"target\": {\n      \"$ref\": \"window#/Math/pow\"\n    },\n    \"value\": [\n      10,\n      {\n        \"$ref\": \"$args/digits\"\n      }\n    ]\n  }\n}\n```\n\n## sum\n\nAdd every number in an array (Array.prototype.reduce with +, seeded at 0).\n\n| Parameter | Type     | Description |\n| --------- | -------- | ----------- |\n| `values`  | number[] | —           |\n\n```json\n{\n  \"operator\": \"reduce\",\n  \"target\": {\n    \"$ref\": \"$args/values\"\n  },\n  \"initial\": 0,\n  \"value\": {\n    \"operator\": \"+\",\n    \"target\": {\n      \"$ref\": \"$reduce/acc\"\n    },\n    \"value\": {\n      \"$ref\": \"$map/item\"\n    }\n  }\n}\n```\n\n## truncate\n\nShorten a string to at most the given length, appending a horizontal ellipsis when cut.\n\n| Parameter | Type   | Description |\n| --------- | ------ | ----------- |\n| `text`    | string | —           |\n| `length`  | number | —           |\n\n```json\n{\n  \"operator\": \"?:\",\n  \"target\": {\n    \"operator\": \">\",\n    \"target\": {\n      \"$ref\": \"$args/text/length\"\n    },\n    \"value\": {\n      \"$ref\": \"$args/length\"\n    }\n  },\n  \"value\": {\n    \"operator\": \"+\",\n    \"target\": {\n      \"operator\": \"slice\",\n      \"target\": {\n        \"$ref\": \"$args/text\"\n      },\n      \"value\": [\n        0,\n        {\n          \"$ref\": \"$args/length\"\n        }\n      ]\n    },\n    \"value\": \"…\"\n  },\n  \"initial\": {\n    \"$ref\": \"$args/text\"\n  }\n}\n```\n",
    "section": "Framework",
    "slug": "framework/reference/formulas",
    "title": "Formula catalog",
    "url": "https://jxsuite.com/docs/framework/reference/formulas/"
  },
  {
    "description": "The blessed operator set for declarative $expression trees — every operator and method token the schema admits.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Operator reference\n\nDeclarative expressions (`$expression`) admit a closed set of operators, enforced by the Jx schema. This page enumerates that set straight from `packages/schema/schema.json`. The expression model itself is documented in the [Framework concepts](/docs/framework/) section; named-formula composition is cataloged in the [formula catalog](/docs/framework/reference/formulas/).\n\n## Unary operators\n\nApplied to a single `target` operand.\n\n`!` · `-`\n\n## Binary operators\n\nApplied to a `target` and a `value` operand. Arithmetic, comparison, logical, and nullish-coalescing forms — the blessed subset of JavaScript's operator set (spec §19.4).\n\n`+` · `-` · `*` · `/` · `%` · `===` · `!==` · `<` · `<=` · `>` · `>=` · `&&` · `||` · `??`\n\n## Assignment operators\n\nStatement-position only: assign (or read-modify-write) a state path. Not valid inside pure expressions.\n\n`=` · `+=` · `-=` · `*=` · `/=`\n\n## Conditional, switch, and call\n\n`?:` selects between `value` and `else` on a `target` condition; `switch` matches a `target` against `cases`; `call` invokes a named formula (spec §19.4c) with `args`.\n\n`?:` · `switch` · `call`\n\n## Pure standard-library methods\n\nGenuine pure `String`/`Array`/`Number` prototype methods, usable inside pure expressions (spec §19.4d). The change-by-copy family (`toSorted`, `toReversed`, `toSpliced`, `with`) replaces their mutating counterparts.\n\n`includes` · `indexOf` · `lastIndexOf` · `join` · `slice` · `concat` · `at` · `flat` · `toSorted` · `toReversed` · `toSpliced` · `with` · `toUpperCase` · `toLowerCase` · `trim` · `trimStart` · `trimEnd` · `split` · `startsWith` · `endsWith` · `padStart` · `padEnd` · `replaceAll` · `repeat` · `charAt` · `normalize` · `toLocaleUpperCase` · `toLocaleLowerCase` · `toFixed` · `toPrecision` · `toLocaleString`\n\n## Mutation methods\n\nStatement-position array mutations. `pop`/`shift` take no argument; `push`/`unshift` take one; `splice` takes `start`, `deleteCount`, and `items`.\n\n`pop` · `shift` · `push` · `unshift` · `splice`\n\n## Iteration methods\n\n`map`/`filter` evaluate their `value` expression per item with `$map/item` and `$map/index` in scope; `reduce` additionally exposes `$reduce/acc` and takes an `initial` value.\n\n`reduce` · `map` · `filter`\n",
    "section": "Framework",
    "slug": "framework/reference/operators",
    "title": "Operator reference",
    "url": "https://jxsuite.com/docs/framework/reference/operators/"
  },
  {
    "description": "Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.",
    "markdown": "\n# Extending\n\nJx is built to be extended. First-party extensions (the Markdown parser, the data connector, auth) use only the public extension hooks — anything they can do, your extension can do. Studio itself is an embeddable app: any backend that speaks its protocol can host it. This section covers both directions, plus how to work on Jx itself.\n\n## Extensions\n\nAn extension is an npm package that contributes formats, classes, capability methods, project settings sections, server mounts, and data connectors to a Jx project. Start with [the anatomy of an extension](/docs/extending/extensions/anatomy), then go deep on the individual contribution points: [schema composition](/docs/extending/extensions/schema-composition), [classes](/docs/extending/extensions/classes), [formats](/docs/extending/extensions/formats), [capabilities](/docs/extending/extensions/capabilities), [project sections](/docs/extending/extensions/project-sections), [server mounts](/docs/extending/extensions/server), [connectors](/docs/extending/extensions/connectors), and the [security model](/docs/extending/extensions/security). Two tutorials build one end-to-end — [a TOML format](/docs/extending/extensions/tutorial-toml-format) and [a guestbook with server routes and a connector](/docs/extending/extensions/tutorial-guestbook) — and the [first-party extensions](/docs/extending/extensions/first-party) are the reference implementations to crib from.\n\n## Embedding Studio\n\nStudio is backend-agnostic: all of its file, git, and project operations go through a platform-adapter layer, and every adapter ultimately speaks one wire contract, the Studio Backend Protocol. The [embedding overview](/docs/extending/embedding) maps the landscape and helps you pick an integration path; [writing a platform adapter](/docs/extending/embedding/platform-adapter) covers the in-page `StudioPlatform` interface; [the backend protocol](/docs/extending/embedding/backend-protocol) defines the contract any backend can serve, with every endpoint listed in the [protocol route reference](/docs/extending/reference/studio-routes); and [dev server internals](/docs/extending/embedding/dev-server) walks through the reference implementation.\n\n## Contributing\n\nTo work on Jx itself, [working in the monorepo](/docs/extending/contributing/monorepo) covers the repository layout, test and coverage policy, and the tooling conventions; [contributing to these docs](/docs/extending/contributing/docs) is the style guide every page in `/docs` follows.\n",
    "section": "Extending",
    "slug": "extending",
    "title": "Extending",
    "url": "https://jxsuite.com/docs/extending/"
  },
  {
    "description": "What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.",
    "markdown": "\n# Extension anatomy\n\nJx documents are JSON, and everything beyond plain JSON — Markdown content, dynamic data tables, authentication, whatever you build next — enters the system through **extension packages**. The unit of admission is an npm package (or a local directory) shipping a `jx-extension.json` manifest; a project opts in with one line in `project.json`. The first-party extensions (`@jxsuite/parser`, `@jxsuite/connector`, `@jxsuite/auth`) use only these public hooks, so anything they do, your extension can do.\n\nAn extension contributes up to four kinds of things, all declared in JSON before any code runs:\n\n- **Classes** — `.class.json` descriptors that become `$prototype`-visible names ([Custom classes](/docs/extending/extensions/classes)).\n- **Formats** — file-extension handling for parsing, saving, and content loading ([Custom formats](/docs/extending/extensions/formats)).\n- **Project sections** — top-level `project.json` keys with data loading and Studio settings UI ([Project sections and settings](/docs/extending/extensions/project-sections)).\n- **Schema fragments** — JSON Schema documents composed into the project's effective schema ([Schema composition](/docs/extending/extensions/schema-composition)).\n\n## Package layout\n\nThe parser is the reference extension — its layout is the template:\n\n```\nextensions/\n  parser/                    # @jxsuite/parser — content collections, markdown, CSV\n    package.json             # \"jx\": \"./jx-extension.json\"; exports manifest, schemas, classes\n    jx-extension.json\n    schemas/\n      project.fragment.schema.json\n      document.fragment.schema.json\n    src/\n      Markdown.class.json    # class descriptors (admission blocks + capability methods)\n      markdown.ts            # implementations\n  connector/                 # @jxsuite/connector — connections + dynamic data tables\n  auth/                      # @jxsuite/auth — Better Auth sessions and permissions\n```\n\nThree files matter to hosts: `package.json` names the manifest in its `\"jx\"` field, the manifest enumerates classes and schema fragments, and each `.class.json` descriptor declares what its class actually does. Implementation modules (`markdown.ts`) are imported only when a host invokes a declared capability — discovery is JSON all the way down.\n\nPublish the manifest, descriptors, and fragments: they must appear in `package.json`'s `files` and `exports` so hosts can resolve `<package>/jx-extension.json` through the exports map.\n\n## Dependency rules\n\nIn the Jx monorepo, `packages/*` is **core** and `extensions/*` is **extensions** — and the arrow between them points one way:\n\n- Extensions may depend on core packages and on each other. `@jxsuite/auth` depends on `@jxsuite/connector` for its database connection.\n- Core packages may **never** list an extension in `dependencies`, `peerDependencies`, or `optionalDependencies`, and may never import `@jxsuite/<extension>` from `src/`. `devDependencies` are permitted for test fixtures only. CI enforces this in `scripts/check-dep-rules.ts`.\n- Bundling carve-outs are explicit and allowlisted with rationale: `packages/desktop` (an offline app shipped as installers, never published to npm) bundles the first-party extensions. Carve-outs live at the app layer, never in core libraries.\n\nThird-party extensions live outside the monorepo, but the same shape applies: depend on `@jxsuite/*` core packages and other extensions freely; nothing in core will ever know your package exists.\n\n## Declaring extensions in a project\n\nA project enables extensions in `project.json`:\n\n```json\n{\n  \"$schema\": \"./project.schema.json\",\n  \"extensions\": [\"@jxsuite/parser\", \"@jxsuite/connector\", \"./my-local-ext\"],\n  \"imports\": { \"PostCard\": \"./components/post-card.class.json\" },\n  \"content\": {\n    \"posts\": { \"source\": \"./content/posts/\", \"format\": \"Markdown\", \"schema\": {} }\n  }\n}\n```\n\n- Entries are bare package names or relative paths (for local, unpublished extensions). Package names resolve **project-first**: the project's own `node_modules`, then the host's. Projects own their extension dependencies — a scaffolded project lists `@jxsuite/parser` in its `package.json`.\n- `imports` keeps its original, reduced job: mapping `$prototype` names to **project-local** class files. It does not register formats or extensions.\n- **Name visibility**: the manifest's class keys become `$prototype`-visible names. On a collision, a project-local `imports` entry wins over a manifest class; two extensions exporting the same class name is a registry error — rename via a local wrapper class to disambiguate.\n- **No implicit defaults.** A project with no `extensions` supports only `.json` documents and core state prototypes.\n\n## Section keys\n\nExtensions contribute top-level `project.json` keys (\"sections\") — `content` (parser), `connections` and `data` (connector), `auth` (auth). By convention section keys are single words, and the key is used verbatim as:\n\n- the property name in `project.json`,\n- the key under `_project` in resolved scope (`config._project.content`),\n- the wire-path segment where applicable.\n\nTwo extensions claiming the same section key is a registry error. See [Project sections and settings](/docs/extending/extensions/project-sections) for what owning a section means.\n\n## The `jx-extension.json` manifest\n\nThe manifest lives at the package root, referenced by `\"jx\": \"./jx-extension.json\"` in `package.json`. The parser's real manifest, in full:\n\n```json\n{\n  \"name\": \"@jxsuite/parser\",\n  \"title\": \"Content & Markdown\",\n  \"description\": \"File-based content collections with Markdown and CSV formats\",\n  \"classes\": {\n    \"Markdown\": \"./src/Markdown.class.json\",\n    \"Csv\": \"./src/Csv.class.json\",\n    \"MarkdownCollection\": \"./src/MarkdownCollection.class.json\",\n    \"ContentCollection\": \"./src/ContentCollection.class.json\",\n    \"ContentEntry\": \"./src/ContentEntry.class.json\",\n    \"Content\": \"./src/Content.class.json\"\n  },\n  \"schemas\": {\n    \"project\": \"./schemas/project.fragment.schema.json\",\n    \"document\": \"./schemas/document.fragment.schema.json\"\n  }\n}\n```\n\n| Key           | Type                     | Meaning                                                                                                        |\n| ------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------- |\n| `name`        | `string` (required)      | Package name; must match `package.json`.                                                                       |\n| `title`       | `string`                 | Human-readable name for Studio surfaces.                                                                       |\n| `description` | `string`                 | One-line description.                                                                                          |\n| `classes`     | `Record<string, string>` | `$prototype`-visible name → class descriptor path, relative to the manifest.                                   |\n| `schemas`     | `object`                 | Schema fragments this package contributes: `project` (project.json sections), `document` (document positions). |\n\nThe manifest is pure data, validated by the generated `extension-manifest.schema.json` from `@jxsuite/schema`. It enumerates; it does not define behavior — behavior lives in the class descriptors it points to.\n\n:::doc-note\nThe registry implementing manifest discovery is `buildExtensionRegistry()` in `@jxsuite/schema/extension-registry`, with injected I/O so the identical logic serves node and browser hosts. It also enforces the conflict rules: duplicate class names, duplicate section keys, and overlapping server mounts all fail the registry build with a named error.\n:::\n\n## Related\n\n- [Schema composition](/docs/extending/extensions/schema-composition) — what the `schemas` fragments contribute\n- [Custom classes](/docs/extending/extensions/classes) — the descriptors the `classes` map points to\n- [Tutorial: a TOML format extension](/docs/extending/extensions/tutorial-toml-format) — a complete third-party package, end to end\n- [First-party extensions](/docs/extending/extensions/first-party) — what parser, connector, and auth each provide\n",
    "section": "Extending",
    "slug": "extending/extensions/anatomy",
    "title": "Extension anatomy",
    "url": "https://jxsuite.com/docs/extending/extensions/anatomy/"
  },
  {
    "description": "How a project's effective JSON Schema is composed from core and extension fragments: entry documents, the two union resources, validation, and jx schema.",
    "markdown": "\n# Schema composition\n\nA project's effective schema is a plain JSON Schema 2020-12 document composed from the core schema and each enabled extension's shipped schema fragment. There is no jx-specific composition runtime: any compliant validator can validate a project offline, and editors get autocomplete for extension-contributed sections through an ordinary `\"$schema\"` binding.\n\n## Fragments\n\nTwo kinds of fragment feed the composition:\n\n- **Core** ships `@jxsuite/schema/schemas/project.core.schema.json`: the core project properties (`name`, `url`, `build`, `imports`, `extensions`, …) plus two published `$defs` — `JxFieldSchema` (the JSON-Schema-subset field shape used by content and table schemas) and `RelationshipRef` (see [Relationships](/docs/framework/site/relationships)). The core fragment is **open** — closure happens in the generated entry document.\n- **Each extension** ships the fragments named in its [manifest](/docs/extending/extensions/anatomy)'s `schemas` map. A project fragment contributes plain `properties` for its section keys. Fragments must be standalone-valid 2020-12 documents with their own `$id`.\n\nThe parser's project fragment, abbreviated — it contributes the `content` section:\n\n```json\n{\n  \"$id\": \"https://jxsuite.com/schema/ext/parser/project/v1\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"content\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"source\": { \"type\": \"string\" },\n          \"format\": { \"type\": \"string\" },\n          \"schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"properties\": {\n                \"additionalProperties\": { \"$ref\": \"https://jxsuite.com/schema/project/fields/v2\" }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nNote the one non-local reference: field-schema positions point at the canonical URI `https://jxsuite.com/schema/project/fields/v2` rather than a local shape. That is the open recursion point described below.\n\n## Generated entry documents\n\n`jx schema` writes two committed files into the project root, composing core with every fragment listed by the project's `extensions`. Both files are **self-contained single-resource schemas**: every referenced resource is embedded under `$defs`, and every `$ref` is a root-relative JSON Pointer into the same file — no relative `./node_modules/…` paths and no canonical URIs, so every editor and validator resolves them offline and identically. The generated `project.schema.json` of a project using only the parser (embedded resource bodies elided):\n\n```json\n{\n  \"$comment\": \"Generated by `jx schema` from project.json#/extensions — do not edit.\",\n  \"$defs\": {\n    \"Fields\": {\n      \"anyOf\": [\n        { \"$ref\": \"#/$defs/project-core-v2/$defs/JxFieldSchema\" },\n        { \"$ref\": \"#/$defs/project-core-v2/$defs/RelationshipRef\" }\n      ]\n    },\n    \"project-core-v2\": { \"…\": \"embedded core fragment\" },\n    \"ext-parser-project-v1\": { \"…\": \"embedded parser fragment\" }\n  },\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"allOf\": [{ \"$ref\": \"#/$defs/project-core-v2\" }, { \"$ref\": \"#/$defs/ext-parser-project-v1\" }],\n  \"type\": \"object\",\n  \"unevaluatedProperties\": false\n}\n```\n\nTop-level section keys combine via `allOf` + `unevaluatedProperties: false` — 2020-12 `unevaluatedProperties` sees annotations from adjacent `allOf` branches, so the entry document closes the object without any fragment needing to know its siblings. A misspelled or un-contributed top-level key fails validation.\n\nEach `$defs` key is a slug of the embedded resource's `$id` — `https://jxsuite.com/schema/project/core/v2` becomes `project-core-v2`.\n\n`document.schema.json` is the same move for documents: the core document schema embedded as `#/$defs/v1` and referenced through `\"allOf\": [{ \"$ref\": \"#/$defs/v1\" }]`, with the paths union resource re-embedded as the union of extension-contributed `$paths` shapes (the parser contributes `ContentPathsSource` from its document fragment).\n\n`project.json` binds via `\"$schema\": \"./project.schema.json\"`.\n\n## The two union resources\n\nTwo positions are **open recursion points** where a fragment must reference the _effective_ union without knowing what other extensions contribute:\n\n| Resource `$id`                                 | Position                                                                                      | Shipped default → entry-document union                                                                                 |\n| ---------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `https://jxsuite.com/schema/project/fields/v2` | Field-schema values inside section entry schemas (content frontmatter fields, table columns). | Core `JxFieldSchema` + `RelationshipRef`; the entry adds extension field extras.                                       |\n| `https://jxsuite.com/schema/document/paths/v2` | The `$paths` value of a document.                                                             | Core source shapes plus an unknown-source branch; the entry drops that branch and unions each extension's paths shape. |\n\nThe mechanism is **`$id` shadowing**: core ships a default resource under each well-known `$id`, and the generated entry document re-embeds the same `$id` with the effective union — outermost wins, with plain `$ref`s and no jx-specific runtime. Fragments just write `{ \"$ref\": \"https://jxsuite.com/schema/project/fields/v2\" }` at field positions; no local fallbacks needed.\n\nAn entry embed **shadows** the shipped default rather than extending it, so it restates the core members — otherwise they would stop validating the moment an extension contributed one. The paths union is the one place the two differ: the shipped default also accepts \"a source shape from an extension I cannot see\", because a default has no way to know which extensions a project enables and would otherwise report `{ \"contentType\": \"blog\" }` as an error. Your generated `document.schema.json` knows, so it drops that branch and checks `$paths` exactly — a misspelled `contentType`, or a source belonging to an extension you have not enabled, is an error instead of a route that silently builds zero pages. The override is settled while the entry document is generated, not while it is validated: each canonical-`$id` reference is rewritten to the root pointer of the entry's own embed, so the committed file states the winner outright.\n\n:::doc-note\n`$dynamicRef` looks like the textbook fit, but ajv 8.x supports `$dynamicAnchor` only at schema-resource roots — and the recursion unit here (a field schema) is not the document root. `$id` shadowing achieves the identical outermost-wins override; the behavior is verified against ajv in `packages/schema/tests/project-schemas.test.ts`.\n:::\n\n## Validation\n\n- Every consumer resolves the committed entry documents offline **with no file access at all**: each `$ref` is a root-relative JSON Pointer into the same document. No `node_modules`, no network, no editor configuration.\n- `jx validate` validates the whole project tree: both committed entry documents for self-containment (any `$ref` that is a relative path, a URI, or a pointer to nothing fails), `project.json` against `./project.schema.json`, every component/page/layout against the bundled document schema, every project-local `*.class.json` against the class schema, and each fragment standalone. CI runs this over every project root in the repo.\n- Where a client fetches the canonical URLs instead (they are served from jxsuite.com), it gets the shipped defaults — the degradation is _under-suggestion_ of extension field extras, never false errors.\n\n:::doc-note\nRoot pointers rather than a `$id`-keyed compound document, because VS Code's JSON language service — which Monaco embeds, so this covers Studio too — implements neither half of compound-document resolution. It resolves `#/…` against the document root instead of the enclosing `$id`, and it fetches anything with a URI before the `#` over the network rather than matching the resource embedded in the same file. Root pointers sidestep both and cost nothing under ajv.\n:::\n\n## Regenerating\n\nRun [`jx schema`](/docs/framework/build/cli) whenever the `extensions` list changes (and re-run `jx validate` after). Regeneration also happens without the CLI:\n\n- Studio regenerates the entry documents when settings are saved.\n- The dev server's Studio API regenerates them on demand when they are missing or older than `project.json`, serving the same self-contained single-resource form to the browser (Monaco registers them as inline objects and never resolves a ref itself).\n\nThe outputs are committed artifacts — check `project.schema.json` and `document.schema.json` into the repo so editors and CI validate without a build step.\n\n## Related\n\n- [CLI commands](/docs/framework/build/cli) — `jx schema` and `jx validate`\n- [Extension anatomy](/docs/extending/extensions/anatomy) — where fragments are declared\n- [project.json](/docs/framework/site/project-json) — the document being validated\n- [Project sections and settings](/docs/extending/extensions/project-sections) — the behavior side of a contributed section\n",
    "section": "Extending",
    "slug": "extending/extensions/schema-composition",
    "title": "Schema composition",
    "url": "https://jxsuite.com/docs/extending/extensions/schema-composition/"
  },
  {
    "description": "Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.",
    "markdown": "\n# Custom classes\n\nAn extension's classes are ordinary Jx `.class.json` descriptors: JSON Schema 2020-12 documents that describe a class — its constructor parameters, fields, and methods — with an optional `$implementation` key pointing at the JavaScript module that implements it. Hosts read the descriptor to learn everything about the class; they import the implementation only to invoke it.\n\nEach class named in the [manifest](/docs/extending/extensions/anatomy)'s `classes` map becomes a `$prototype`-visible name in any project that enables the extension. A page can then use it as state with no `$src`:\n\n```json\n{\n  \"state\": {\n    \"about\": { \"$prototype\": \"Markdown\", \"src\": \"./content/about.md\" }\n  }\n}\n```\n\n## A descriptor at a glance\n\nThe parser's `Markdown.class.json`, heavily abbreviated:\n\n```json\n{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Markdown\",\n  \"$prototype\": \"Class\",\n  \"extends\": \"Object\",\n  \"$implementation\": \"./markdown.js\",\n  \"format\": { \"extensions\": [\".md\"], \"mediaType\": \"text/markdown\", \"…\": \"…\" },\n  \"$defs\": {\n    \"parameters\": {\n      \"src\": { \"identifier\": \"src\", \"type\": { \"type\": \"string\" } },\n      \"…\": {}\n    },\n    \"constructor\": {\n      \"role\": \"constructor\",\n      \"parameters\": [{ \"$ref\": \"#/$defs/parameters/src\" }, \"…\"],\n      \"body\": [\"this.config = config;\"]\n    },\n    \"methods\": {\n      \"resolve\": { \"role\": \"method\", \"scope\": \"instance\", \"identifier\": \"resolve\" },\n      \"parse\": { \"role\": \"parse\", \"scope\": \"static\", \"timing\": [\"compiler\", \"server\", \"client\"] },\n      \"serialize\": { \"role\": \"serialize\", \"scope\": \"static\", \"…\": \"…\" },\n      \"…\": {}\n    }\n  }\n}\n```\n\nThe `$defs` object is organized into well-known categories:\n\n| Category      | Purpose                                                                  |\n| ------------- | ------------------------------------------------------------------------ |\n| `parameters`  | Constructor parameter properties (config object fields)                  |\n| `fields`      | Instance fields (private if `#`-prefixed)                                |\n| `constructor` | Constructor body and super args                                          |\n| `methods`     | Instance methods and accessors, plus static capability methods by `role` |\n| `returnTypes` | Named return-type schemas for tooling                                    |\n\n## The external class contract\n\nEvery class — extension-shipped or project-local — satisfies the same runtime contract:\n\n- **Constructor** receives a single `config` object containing all `state` properties except the reserved keywords (`$prototype`, `$src`, `$export`, `timing`, `default`, `description`). For the state entry above, `config` is `{ src: \"./content/about.md\" }`.\n- **Value resolution** is checked in order: `instance.resolve()` (async, awaited) → `instance.value` (getter or property) → the instance itself.\n- **Reactivity is optional**: implement `subscribe(callback)` / `unsubscribe()` and the runtime re-renders when you notify.\n\nMethods declare their output shape with a JSON Schema in `returnType` (often a `$ref` into `$defs/returnTypes`). Tooling uses this to reason about a class without executing it — a `resolve` whose `returnType` is an array marks instances as valid sources for mapped iteration, so Studio offers the class in repeater pickers.\n\n## `$implementation`\n\n`$implementation` is resolved relative to the `.class.json` file and names the module exporting the class; the export is named by the descriptor's `title`. The parser ships TypeScript in `src/` and points `$implementation` at the built neighbor (`./markdown.js`).\n\nWhen `$implementation` is **absent**, the class is self-contained: the compiler generates an ES class from the schema itself — constructor (with `super()` support), private `#fields`, getters/setters, async methods, and the `extends` clause all come from the descriptor. This is how small classes ship with no JavaScript at all; structured `body` statements are covered in [Functions](/docs/framework/concepts/functions).\n\n## Admission blocks\n\nA plain class with nothing but the contract above is a state prototype — construct, `resolve()`, done. A class additionally participates in **host dispatch** through one or more top-level admission blocks:\n\n| Block       | Grants                                                                                      | Covered in                                                      |\n| ----------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |\n| `format`    | File-extension dispatch: parsing, serialization, content discovery/loading, Studio editing. | [Custom formats](/docs/extending/extensions/formats)            |\n| `project`   | Ownership of a project.json section: data loading, `$paths` resolution, Studio settings UI. | [Project sections](/docs/extending/extensions/project-sections) |\n| `server`    | A mounted route subtree in the deployed-site worker and the dev server.                     | [Server mounts](/docs/extending/extensions/server)              |\n| `connector` | A database connection provider: dialect, schema deploy, bindings, connection testing.       | [Connectors](/docs/extending/extensions/connectors)             |\n\n`Markdown` carries a `format` block; the connector's `Data` class carries `project` **and** `server` (it owns the `data` section and mounts `/_jx/data`). Blocks compose freely on one class.\n\n## How hosts introspect\n\nHosts follow a strict JSON-first contract — the registry (`buildExtensionRegistry()` in `@jxsuite/schema/extension-registry`) implements it once for node and browser hosts:\n\n1. Resolve each `extensions` entry to a package root; read the manifest named by its `\"jx\"` field; read each listed `.class.json`.\n2. Detect participation via the admission blocks.\n3. Find capabilities by scanning `$defs.methods` for well-known `role` values; the method's `identifier` (fallback: its key) names the static method.\n4. To invoke: import `$implementation`, take the export named by the class `title`, call `Export[identifier](...args)`.\n5. Respect `timing`: if the host's environment is not listed, delegate to the dev server.\n\nStep 4 is the only point where extension code runs in a host. Everything Studio shows — settings forms, format icons, capability option UIs — comes from steps 1–3.\n\n## Capability methods\n\nCapabilities are the static methods step 3 discovers: `parse`, `load`, `projectData`, `mount`, `lower`, and the rest of the well-known roles. Their contract — timing, options-as-parameters, and the compile-away `lower` hook — is the subject of [Capability methods](/docs/extending/extensions/capabilities).\n\n## Related\n\n- [Capability methods](/docs/extending/extensions/capabilities) — the static method contract in depth\n- [Custom formats](/docs/extending/extensions/formats) — the `format` admission block\n- [Data prototypes](/docs/framework/concepts/data-prototypes) — the built-in classes yours sit beside\n- [Tutorial: a TOML format extension](/docs/extending/extensions/tutorial-toml-format) — a full descriptor built from scratch\n",
    "section": "Extending",
    "slug": "extending/extensions/classes",
    "title": "Custom classes",
    "url": "https://jxsuite.com/docs/extending/extensions/classes/"
  },
  {
    "description": "Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.",
    "markdown": "\n# Custom formats\n\nA format teaches Jx a new file type. `.json` is the single native built-in — Jx _is_ JSON — so every other extension a project opens, saves, builds, or loads content from (`.md`, `.csv`, your `.toml`) is dispatched through a format class. A class participates in format dispatch iff its `.class.json` descriptor carries a top-level `format` object.\n\n## The `format` block\n\nThe parser's `Markdown.class.json` declares, verbatim:\n\n```json\n\"format\": {\n  \"extensions\": [\".md\"],\n  \"mediaType\": \"text/markdown\",\n  \"documentKinds\": [\"page\", \"component\", \"content\"],\n  \"exportTarget\": true,\n  \"remote\": false\n}\n```\n\n| Key             | Type                                 | Default | Meaning                                                                                                                                      |\n| --------------- | ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |\n| `extensions`    | `string[]` (required)                | —       | File extensions claimed, with leading dot.                                                                                                   |\n| `mediaType`     | `string`                             | —       | MIME type; used for icons, labels, HTTP responses.                                                                                           |\n| `documentKinds` | `(\"page\"\\|\"component\"\\|\"content\")[]` | `[]`    | `page`/`component` admit the extension into pages/components discovery globs; `content` admits it as a content source.                       |\n| `exportTarget`  | `boolean`                            | `false` | When true, site builds emit a serialized sidecar per page in this format (requires a `serialize` capability).                                |\n| `remote`        | `boolean`                            | `false` | When true, the `load` capability accepts `http(s)` URLs as sources. Remote content sources **must** name a remote-capable format explicitly. |\n\nTwo classes may claim the same extension **only with disjoint capabilities** — the registry build fails on an ambiguous `(extension, capability)` pair. A registry never claims `.json`.\n\n## Format capabilities\n\nThe block declares _what_ the class handles; the class's [capability methods](/docs/extending/extensions/capabilities) declare _how_. Four roles belong to the `format` block:\n\n| Role        | Signature                                                     | Consumers                                   |\n| ----------- | ------------------------------------------------------------- | ------------------------------------------- |\n| `parse`     | `(source, options?) → JxDocument`                             | compiler, server, Studio (open file)        |\n| `serialize` | `(doc, options?) → string`                                    | Studio (save), site build (export sidecars) |\n| `discover`  | `(source, { baseDir }) → string[]`                            | content loading (list entry files)          |\n| `load`      | `(path, { schema, directiveOptions }) → ContentLoaderEntry[]` | content loading (parse one source)          |\n\nA format implements the subset it needs: a read-only format can ship `parse` without `serialize` (Studio then opens files in this format read-only in structural modes); a data-only format like `Csv` needs `discover`/`load` but has no reason to be a page format.\n\nThe `Markdown` class implements all four, plus the standard instance `resolve()` — so `{ \"$prototype\": \"Markdown\", \"src\": \"./about.md\" }` works as runtime state, satisfying the same [external class contract](/docs/extending/extensions/classes) as every other class.\n\n## How the pipeline dispatches\n\nHosts never hard-code file types. Each one builds a format registry from the enabled extensions' manifests and routes by extension:\n\n- **Pages and components discovery** — the site build and dev server glob for `.json` plus every extension whose format declares the matching `documentKind`, then call `parse` on non-JSON matches. This is why adding a Markdown page is just dropping `pages/about.md` in a parser-enabled project.\n- **Content loading** — a `content` section entry names a format (or derives it from the source's file extension); the loader calls `discover` to list entry files, then `load` per file, validating each entry against the content type's schema. See [Content collections](/docs/framework/site/content-collections).\n- **Studio editing** — opening a claimed file calls `parse` to get the Jx tree the canvas edits; saving calls `serialize`. When a capability's `timing` excludes the browser, Studio round-trips through the dev server's `POST /__studio/format` endpoint instead of importing the implementation ([Studio routes](/docs/extending/reference/studio-routes)).\n- **Export sidecars** — with `exportTarget: true`, the build serializes each page into the format next to its HTML output.\n\n## Studio hints\n\nFormat classes describe their Studio control surface declaratively in a top-level `$studio` block — Studio interprets this data generically and never hard-codes per-format element sets. From `Markdown.class.json`, abbreviated:\n\n```json\n\"$studio\": {\n  \"icon\": \"markdown\",\n  \"modes\": [\"edit\", \"design\", \"preview\", \"source\"],\n  \"documentMode\": {\n    \"default\": \"content\",\n    \"componentWhen\": { \"frontmatterKey\": \"tagName\", \"matches\": \".+-.+\" }\n  },\n  \"newFileTemplate\": \"---\\ntitle: Untitled\\n---\\n\\n\",\n  \"elements\": {\n    \"block\": [\"h1\", \"h2\", \"h3\", \"p\", \"blockquote\", \"ul\", \"ol\", \"li\", \"pre\", \"…\"],\n    \"inline\": [\"em\", \"strong\", \"del\", \"code\", \"a\", \"img\", \"br\"],\n    \"void\": [\"hr\", \"br\", \"img\"],\n    \"textOnly\": [\"code\"],\n    \"nesting\": {\n      \"h1\": { \"block\": false, \"inline\": true, \"directive\": false },\n      \"ul\": { \"only\": [\"li\"] },\n      \"…\": {}\n    }\n  }\n}\n```\n\n- `icon` — file icon in the Files panel; `modes` — which canvas modes the format supports.\n- `documentMode` — whether files open as prose content or as components, with an optional frontmatter-based override (here: a hyphenated `tagName` means \"this .md file defines a custom element\").\n- `newFileTemplate` — the seed content for **New File** in this format.\n- `elements` — the allowlist and nesting constraints gating structural editing: which tags the element picker offers, what may nest where, which are void or text-only.\n\nOne more generic hint applies to any class, not just formats: `$studio.stateDefaults` — an object merged into state entries Studio creates for the prototype. The connector's `TableQuery` sets `{ \"timing\": \"client\" }` so Studio-created queries default to browser resolution.\n\n## Related\n\n- [Tutorial: a TOML format extension](/docs/extending/extensions/tutorial-toml-format) — build a working format end to end\n- [Capability methods](/docs/extending/extensions/capabilities) — timing and the options contract\n- [Content collections](/docs/framework/site/content-collections) — the consumer of `discover`/`load`\n- [Jx Markdown](/docs/framework/site/jx-markdown) — what the reference format's dialect looks like\n",
    "section": "Extending",
    "slug": "extending/extensions/formats",
    "title": "Custom formats",
    "url": "https://jxsuite.com/docs/extending/extensions/formats/"
  },
  {
    "description": "The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.",
    "markdown": "\n# Capability methods\n\nCapabilities are how a class's declarations turn into behavior. They are declared in the descriptor's `$defs.methods` using well-known `role` values, and they are all `scope: \"static\"` — hosts call them on the implementation class without constructing an instance. The instance `resolve()` method remains the runtime's on-demand access path; capabilities serve the build, the servers, and Studio.\n\n## The contract\n\nA capability declaration is an ordinary method entry with a reserved `role`. The parser's `Content` class declares its `$paths` expansion capability like this (abbreviated):\n\n```json\n\"resolvePaths\": {\n  \"role\": \"resolvePaths\",\n  \"scope\": \"static\",\n  \"identifier\": \"resolvePaths\",\n  \"discriminator\": \"contentType\",\n  \"timing\": [\"compiler\", \"server\"],\n  \"parameters\": [\n    {\n      \"identifier\": \"pathsDef\",\n      \"type\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"contentType\": { \"type\": \"string\" },\n          \"param\": { \"type\": \"string\", \"default\": \"slug\" },\n          \"field\": { \"type\": \"string\", \"default\": \"id\" }\n        },\n        \"required\": [\"contentType\"]\n      }\n    },\n    { \"identifier\": \"ctx\", \"type\": { \"type\": \"object\" } }\n  ],\n  \"returnType\": { \"$ref\": \"#/$defs/returnTypes/PathEntries\" }\n}\n```\n\nTo find and invoke a capability, a host scans `$defs.methods` for the `role`, takes the method's `identifier` (fallback: its key) as the static method name, imports `$implementation`, and calls `Export[identifier](...args)`. So the declaration above is fulfilled by `Content.resolvePaths(pathsDef, ctx)` in `content-loader.ts` — a plain exported static method, no framework API to implement.\n\n## The roles\n\n| Role             | Block       | Signature                                                                   | Consumers                                                   |\n| ---------------- | ----------- | --------------------------------------------------------------------------- | ----------------------------------------------------------- |\n| `parse`          | `format`    | `(source, options?) → JxDocument`                                           | compiler, server, Studio (open file)                        |\n| `serialize`      | `format`    | `(doc, options?) → string`                                                  | Studio (save), site build (export sidecars)                 |\n| `discover`       | `format`    | `(source, { baseDir }) → string[]`                                          | content loading (list entry files)                          |\n| `load`           | `format`    | `(path, { schema, directiveOptions }) → ContentLoaderEntry[]`               | content loading (parse one source)                          |\n| `projectData`    | `project`   | `(sectionValue, { projectConfig, root, registry, io }) → unknown`           | site build, dev server — result stored as `_project[<key>]` |\n| `resolvePaths`   | `project`   | `(pathsDef, { data, projectConfig, root }) → Record<string, unknown>[]`     | pages discovery (`$paths` expansion), Studio preview        |\n| `lower`          | any         | `(def, context) → JxStateDefinition`                                        | compiler — rewrites a state def into a core shape           |\n| `emit`           | `project`   | `(sectionValue, { projectConfig, root, sections, routes }) → EmitFile[]`    | site build — writes derived assets into the build output    |\n| `assets`         | `project`   | `(sectionValue, { projectConfig, root }) → AssetMount[]`                    | site build, dev server — publishes directories at site URLs |\n| `mount`          | `server`    | `(options, ctx) → (request, env) => Promise<Response>`                      | generated site worker, dev server                           |\n| `dialect`        | `connector` | `(connection, env) → Kysely Dialect`                                        | data mounts, auth, deploy                                   |\n| `deploySchema`   | `connector` | `(tables, connection, { env, dryRun }) → { statements, applied, warnings }` | `jx db push`, Studio push                                   |\n| `bindings`       | `connector` | `(connection) → wrangler config fragment`                                   | scaffolding, `jx db push`                                   |\n| `testConnection` | `connector` | `(connection, env) → { ok, error? }`                                        | Studio connections UI, CLI                                  |\n\n`resolvePaths` methods additionally declare a `discriminator` — the `$paths` key that routes to them. The parser's is `contentType`, so a page with `\"$paths\": { \"contentType\": \"blog\" }` dispatches to `Content.resolvePaths`. Hosts dispatch purely on which discriminator key is present in the `$paths` value; two extensions can coexist without either knowing the other's shape.\n\nThe format roles are covered in depth in [Custom formats](/docs/extending/extensions/formats); the project roles in [Project sections and settings](/docs/extending/extensions/project-sections); `mount`, `dialect`, `deploySchema`, `bindings`, and `testConnection` in [Server mounts](/docs/extending/extensions/server) and [Connectors](/docs/extending/extensions/connectors).\n\n## `timing`\n\nEach capability may declare a `timing` array — the environments allowed to invoke it directly:\n\n- Values: `\"compiler\"`, `\"server\"`, `\"client\"`. Default when omitted: `[\"compiler\", \"server\"]` — assume node-only.\n- A host whose environment is excluded round-trips through the dev server (`POST /__studio/format`, `POST /__jx_resolve__`) instead of importing the implementation.\n- Capabilities that are browser-safe — no `fs`, `glob`, or node imports anywhere on their code path — should declare `\"client\"` so Studio can call them in-process. `Markdown.parse` declares all three, which is why typing in Studio's Markdown editor never touches the network; `Markdown.discover` reads the filesystem, so it stays `[\"compiler\", \"server\"]`.\n\n:::doc-tip\n`timing` is per-method, not per-class. Mixed classes are normal: declare `\"client\"` on the pure-string transforms and let the filesystem-touching ones delegate.\n:::\n\n## Options as parameters\n\nCapability options are declared as ordinary `parameters` with JSON-Schema types — there is no separate options vocabulary. `Markdown.serialize` declares its second parameter as:\n\n```json\n{\n  \"identifier\": \"options\",\n  \"type\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"mode\": { \"type\": \"string\", \"enum\": [\"roundtrip\", \"export\"], \"default\": \"roundtrip\" },\n      \"frontmatter\": { \"type\": \"boolean\", \"default\": true }\n    }\n  }\n}\n```\n\nBecause the types, enums, and defaults are right there in the descriptor, Studio can render an options UI for any extension's capability without host-side knowledge of the extension — an enum becomes a picker, a boolean a toggle, a default the initial value.\n\n## `lower`\n\n`lower` lets a state class compile away. At build time, when a state entry's [timing](/docs/framework/concepts/timing) excludes the compiler, the compiler checks the class descriptor for a `lower` capability and, if present, replaces the def in place with the returned core-shape def (`Request`, `Function`, …). The connector's `TableQuery` declares:\n\n```json\n\"lower\": {\n  \"role\": \"lower\",\n  \"scope\": \"static\",\n  \"identifier\": \"lower\",\n  \"timing\": [\"compiler\"],\n  \"parameters\": [\n    { \"identifier\": \"def\", \"type\": { \"type\": \"object\" } },\n    { \"identifier\": \"context\", \"type\": { \"type\": \"object\" } }\n  ],\n  \"returnType\": { \"type\": \"object\" },\n  \"description\": \"Rewrite into a core Request def targeting /_jx/data\"\n}\n```\n\nA page authored with `{ \"$prototype\": \"TableQuery\", \"table\": \"posts\", \"timing\": \"client\" }` compiles into a plain reactive fetch of `/_jx/data/posts?…` — dynamic-data queries work in compiled sites with **no extension code shipped to the browser**. `lower` may appear on a class with any admission block (or none).\n\n## `assets`\n\n`assets` publishes a directory your section already reads from at a site URL, so the files beside its sources are reachable:\n\n```json\n\"assets\": {\n  \"role\": \"assets\",\n  \"scope\": \"static\",\n  \"identifier\": \"assets\",\n  \"timing\": [\"compiler\", \"server\"],\n  \"parameters\": [\n    { \"identifier\": \"sectionValue\", \"type\": { \"type\": \"object\" } },\n    { \"identifier\": \"ctx\", \"type\": { \"type\": \"object\" } }\n  ],\n  \"returnType\": { \"type\": \"array\" }\n}\n```\n\nEach returned pair is an **asset mount**: `{ \"urlPrefix\": \"/content/blog\", \"dir\": \"/abs/path/to/content/blog\" }`. The parser returns one per content type with a directory source, which is what lets a Markdown entry reference `./images/hero.png` and still resolve everywhere.\n\nHosts do three things with a mount, and your extension writes none of them:\n\n- the site build resolves mounted URLs for [image optimization](/docs/framework/site/images), then copies **only the files the compiled output references** to their URL path in `dist/`;\n- the dev and desktop servers serve the mount ahead of the project root, so previews match the built site;\n- `jx preview` needs nothing — it serves `dist/`, where the files already are.\n\n`dir` may sit outside the project root — that is the point. Return the mount from the section's config alone: the call is expected to be cheap and side-effect-free, and hosts may make it per request. A `urlPrefix` claimed for two different directories is a configuration error; sharing one `dir` across prefixes is fine.\n\n## Related\n\n- [Custom classes](/docs/extending/extensions/classes) — the descriptors capabilities live in\n- [Custom formats](/docs/extending/extensions/formats) — the four format roles in context\n- [Project sections and settings](/docs/extending/extensions/project-sections) — `projectData` and `resolvePaths` in context\n- [Timing](/docs/framework/concepts/timing) — the state-entry timing model `lower` keys off\n",
    "section": "Extending",
    "slug": "extending/extensions/capabilities",
    "title": "Capability methods",
    "url": "https://jxsuite.com/docs/extending/extensions/capabilities/"
  },
  {
    "description": "Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.",
    "markdown": "\n# Project sections and settings\n\nA project section is a top-level `project.json` key owned by an extension class — `content` (parser), `connections` and `data` (connector), `auth` (auth). Owning a section means three things: your fragment defines its schema, your capabilities load it and expand its `$paths`, and your `$studio` block gives it a settings page. A class claims a section iff its descriptor has a top-level `project` object.\n\n## The `project` block\n\nThe parser's `Content.class.json` declares, verbatim:\n\n```json\n\"project\": {\n  \"key\": \"content\",\n  \"title\": \"Content Types\",\n  \"description\": \"File-based content collections loaded at build and dev-serve time\",\n  \"referenceable\": true\n}\n```\n\n| Key             | Type      | Default | Meaning                                                                                                                   |\n| --------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |\n| `key`           | `string`  | —       | The project.json top-level property this class owns (single word by convention; exclusive across extensions).             |\n| `title`         | `string`  | —       | Studio label.                                                                                                             |\n| `description`   | `string`  | —       | Studio help text.                                                                                                         |\n| `referenceable` | `boolean` | `false` | Opts the section's named entries into the relationships vocabulary ([Relationships](/docs/framework/site/relationships)). |\n\nThe section's **value schema is not duplicated here** — it lives in the package's project fragment (the manifest's `schemas.project`, see [Schema composition](/docs/extending/extensions/schema-composition)). Hosts that need the entry shape — Studio settings forms, reference pickers — read `properties[<key>]` from the fragment via the registry.\n\n## Loading section data: `projectData`\n\nBehavior attaches through [capability methods](/docs/extending/extensions/capabilities) on the same class. The `projectData` capability turns the raw section value into loaded data:\n\n```\nprojectData(sectionValue, { projectConfig, root, registry, io }) → unknown\n```\n\nThe compiler's site build and the dev server's resolve path both call it, storing the result as `_project[<key>]` in resolved scope — so the parser's `projectData` loads every content type through the format registry and pages see the entries as `config._project.content`. The auth extension's `projectData` is nearly a passthrough: it exposes the section's identifiers under `_project.auth` (never secrets).\n\n## Expanding routes: `resolvePaths` and discriminators\n\nA dynamic route's `$paths` value is dispatched to the section class whose `resolvePaths` capability declares the matching **discriminator** — the key that routes to it. The parser's discriminator is `contentType`, so this page head:\n\n```json\n{ \"$paths\": { \"contentType\": \"blog\", \"param\": \"slug\" } }\n```\n\ndispatches to `Content.resolvePaths(pathsDef, { data, projectConfig, root })`, which returns one route-param object per entry (`[{ \"slug\": \"hello-world\" }, …]`). Hosts dispatch purely on which discriminator key is present — no central switch statement, no extension aware of any other. The contributed `$paths` shape is also what your document fragment unions into the paths schema resource, so editors validate it. See [Routing](/docs/framework/site/routing) for the authoring side.\n\n## Publishing files: `assets`\n\nIf your section reads from directories, the `assets` capability publishes them at a site URL so the files beside its sources are reachable:\n\n```\nassets(sectionValue, { projectConfig, root }) → [{ urlPrefix: \"/content/blog\", dir: \"/abs/content/blog\" }]\n```\n\nThe parser returns one mount per content type with a directory source. The site build resolves those URLs for image optimization and copies the referenced files into `dist/`; the dev server serves them from the source. That is what makes a collection's co-located images work — including when the source lives outside the project. Details in [Capability methods](/docs/extending/extensions/capabilities).\n\n## Studio settings: `$studio.settings`\n\nThe section class's `$studio` block may declare a settings section, rendered generically by Studio under _Settings_. The auth extension's, verbatim:\n\n```json\n\"$studio\": {\n  \"settings\": {\n    \"icon\": \"sp-icon-lock-closed\",\n    \"label\": \"Authentication\",\n    \"order\": 58,\n    \"layout\": \"form\",\n    \"entry\": {\n      \"ui\": {\n        \"connection\": { \"enum\": { \"$ref\": \"#/$context/connections\" } },\n        \"secretEnv\": { \"control\": \"secret\" }\n      }\n    }\n  }\n}\n```\n\n| Key              | Meaning                                                                                                                                                                    |\n| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `icon`           | Section icon in the settings nav.                                                                                                                                          |\n| `label`          | Section label (defaults to `project.title`).                                                                                                                               |\n| `order`          | Sort position among contributed sections.                                                                                                                                  |\n| `layout`         | `\"form\"` (default) — one form over the whole section value. `\"map\"` — master-detail for `type: object` + `additionalProperties` sections: key list left, entry form right. |\n| `entry.ui`       | Per-field control overrides for the entry form: `{ \"<field>\": { \"control\": \"<name>\" } }`.                                                                                  |\n| `entry.newEntry` | Template for freshly created entries, with `${key}` substitution.                                                                                                          |\n| `renderer`       | Escape hatch: names a Studio-registered custom section renderer. First-party extensions use the generic path.                                                              |\n\nEverything else — field labels, types, required marks — comes from the project fragment's schema for the section, so the form stays in sync with validation for free.\n\nThe parser's `Content` class shows the `map` layout: its section is a map of content types, so Studio renders a key list (add/rename/delete, slugified) beside an entry form, seeds new entries from `entry.newEntry` (`\"source\": \"./content/${key}/\"` — `${key}` becomes the new entry's name), and swaps the `schema` field for the visual `schema-builder` control. That is the entire implementation of the [Content types](/docs/studio/projects/content-types) surface — no parser-specific Studio code exists.\n\n### Controls and dynamic enums\n\nBuilt-in controls you can name in `entry.ui`:\n\n- `\"schema-builder\"` — the visual JSON-Schema field editor.\n- `\"secret\"` — the value is committed via the platform's secret store, **never** project.json.\n- `\"binding\"` — signal/route-param binding.\n- Implicit defaults per type, enum, and format cover everything else.\n\nEnum choices may be dynamic via `{ \"$ref\": \"#/$context/<pointer>\" }` — a JSON-pointer walk over the project config (with `{@param}` segment substitution and the `$formats` virtual root). Auth's `connection` field above lists the keys of the project's `connections` section; `#/$context/auth/roles` would list configured roles.\n\n:::doc-warning\nSecrets never enter `project.json`. Committed config carries identifiers and env-var _names_ only (`\"secretEnv\": \"BETTER_AUTH_SECRET\"`); the `secret` control writes actual values to the platform's secret store, and locally they live in the git-ignored `.dev.vars`. See [Security and secrets](/docs/extending/extensions/security).\n:::\n\n## Related\n\n- [Schema composition](/docs/extending/extensions/schema-composition) — where the section's value schema lives\n- [Capability methods](/docs/extending/extensions/capabilities) — `projectData` and `resolvePaths` contracts\n- [Server mounts](/docs/extending/extensions/server) — giving a section a route subtree\n- [Project settings in Studio](/docs/studio/projects/settings) — what users see\n",
    "section": "Extending",
    "slug": "extending/extensions/project-sections",
    "title": "Project sections and settings",
    "url": "https://jxsuite.com/docs/extending/extensions/project-sections/"
  },
  {
    "description": "How an extension class mounts routes under /_jx/ in the deployed-site worker and the dev server: the server block, mount order, and the shared context.",
    "markdown": "\n# Server mounts\n\nAn extension class contributes live routes to a deployed Jx site — and to the dev server — when its descriptor carries a top-level `server` object plus a `mount` capability. That is the whole admission test: no plugin API to register against, no HTTP framework to adopt. The generated site worker and the dev server both discover the block through the [extension registry](/docs/extending/extensions/anatomy) and dispatch requests to the handler your `mount` method returns.\n\nThis page covers the declaration, the handler contract, how mounts cooperate through the shared context, and the section-owner `deploySchema` hook that lets a mounted section contribute to schema pushes.\n\n## The server block\n\nThe auth extension's section class, `extensions/auth/src/Auth.class.json`, declares its mount in one line:\n\n```json\n\"server\": { \"basePath\": \"/_jx/auth\", \"order\": 10, \"module\": \"@jxsuite/auth/worker\" }\n```\n\n| Key        | Meaning                                                                                               |\n| ---------- | ----------------------------------------------------------------------------------------------------- |\n| `basePath` | The route subtree this mount owns. Must be under `/_jx/`. Conflicts are a registry error.             |\n| `order`    | Mount order (ascending). Earlier mounts run first and may populate the shared context.                |\n| `module`   | Bare specifier the generated worker imports (robust under bundlers); falls back to `$implementation`. |\n\nEvery mount lives under `/_jx/` — the reserved subtree for extension routes on a running site — and each `basePath` has exactly one owner. Two extensions claiming the same subtree fail the registry build instead of shadowing each other.\n\n## The mount capability\n\n`mount` is a static [capability method](/docs/extending/extensions/capabilities) with `timing: [\"server\"]`. It returns a **fetch-style handler**:\n\n```ts\n(request: Request, env: Record<string, unknown>) => Promise<Response>;\n```\n\nYou need no HTTP framework: the generated worker wraps each handler itself (`app.all('<basePath>/*', c => handler(c.req.raw, c.env))`), and the dev server dispatches to it directly.\n\n`mount(options, ctx)` receives:\n\n- `options` — JSON inlined at generation time, plus host-provided values (the section manifest, resolved class constructors). Because it is baked into the worker, it may carry identifiers only — never secret values. See [Security and secrets](/docs/extending/extensions/security).\n- `ctx` — the shared server context, described below.\n\nHere is the auth extension's real mount, abbreviated from `extensions/auth/src/worker.ts`:\n\n```ts\nexport const Auth = {\n  mount(options: AuthMountOptions, ctx: JxServerContext) {\n    const state = createAuthMountState();\n    ctx.auth = {\n      authorize: (input) => Promise.resolve(evaluatePermission(input)),\n      getSession: async (request, env) => {\n        try {\n          const auth = await getAuthForEnv(state, options, env);\n          return await getSessionContext(auth, request);\n        } catch {\n          return null; // fail-closed: an unreachable auth backend means signed out\n        }\n      },\n    };\n    return (request: Request, env: Env): Promise<Response> =>\n      handleAuthRequest(request, env, options, state);\n  },\n};\n```\n\nThe pattern to copy: set up per-isolate state in the closure, publish anything later mounts need onto `ctx`, and return the handler. Secret values (the signing secret, database URLs) never appear here — they arrive through `env` on each request.\n\n## Mount order and the shared context\n\nEach worker isolate creates **one** mutable `JxServerContext` object and passes it to every mount in ascending `order`. Mounts communicate through it:\n\n```ts\ninterface JxServerContext {\n  auth?: {\n    getSession(request: Request, env: Record<string, unknown>): Promise<SessionInfo | null>;\n    authorize(input: AuthorizeInput, env: Record<string, unknown>): Promise<AuthorizeDecision>;\n  };\n  [key: string]: unknown;\n}\n```\n\nThe first-party extensions demonstrate the choreography: the auth mount (order 10) sets `ctx.auth`; the connector's data mount (order 20) consumes it to evaluate table permission rules. Pick an order above the mounts you depend on — the guestbook example mounts at order 30 so both hooks are already in place.\n\n**Fail-closed rule**: a mount that gates writes on authorization must deny everything except explicitly-public rules when `ctx.auth` is absent. The data mount does exactly this — without the auth extension, only `public` permission rules pass.\n\nThe connector's data mount is the canonical consumer, serving the wire contract every table rides on:\n\n```\nGET    /_jx/data/:table        ?filter=<json>&sort=<json>&limit=&offset=&include=\nGET    /_jx/data/:table/:id\nPOST   /_jx/data/:table\nPATCH  /_jx/data/:table/:id\nDELETE /_jx/data/:table/:id\n```\n\n:::doc-note\nThe dev server constructs mounts the same way the generated worker does, so `jx dev` exercises the identical code path — including `ctx` ordering and the fail-closed behavior. See [The dev server](/docs/extending/embedding/dev-server).\n:::\n\n## Section-owner deploySchema\n\nConnector classes declare a `deploySchema` capability to sync tables ([Connectors](/docs/extending/extensions/connectors)). A **non-connector** [project-section class](/docs/extending/extensions/project-sections) may declare one too, letting its section contribute steps to the schema push (`jx db push`, the Studio push button). The signature differs — there is no single connection definition to hand over:\n\n```\ndeploySchema(sectionValue, projectConfig, { env, dryRun?, connection?, connectors? })\n  → { steps, applied, warnings, connection }\n```\n\n- `steps` are ready push-plan entries (`{ kind, table?, summary, sql?, connection? }`). Hosts append them **after** the connector plan and default each step's `kind` to the contributing section key — the auth extension's Better Auth system-table migration lands as `kind: \"auth\"` steps this way.\n- `connectors` carries the same provider stand-ins the mounts receive, so dev pushes hit the `local:` stand-in databases.\n- When a push is filtered to a `connection` the section does not live on, return empty steps.\n\nHosts stay extension-agnostic throughout: registry dispatch only, no extension imports, no hardcoded section names. `Auth.deploySchema` in `extensions/auth/src/worker.ts` is the working reference.\n\n## Related\n\n- [Capability methods](/docs/extending/extensions/capabilities) — roles, `timing`, and how hosts invoke statics.\n- [Security and secrets](/docs/extending/extensions/security) — why mount options carry identifiers only.\n- [Tutorial: a guestbook extension](/docs/extending/extensions/tutorial-guestbook) — a mount built end to end.\n- [Protocol route reference](/docs/extending/reference/studio-routes) — the `/__studio/*` routes, a separate surface from `/_jx/*` mounts.\n",
    "section": "Extending",
    "slug": "extending/extensions/server",
    "title": "Server mounts",
    "url": "https://jxsuite.com/docs/extending/extensions/server/"
  },
  {
    "description": "The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.",
    "markdown": "\n# Connectors\n\nA **connector** is a class that provides database connections: it knows how to open a dialect against a backing service, sync table schemas into it, emit deployment bindings, and probe reachability. A class becomes a connector when its descriptor carries a top-level `connector` object plus the connector [capability methods](/docs/extending/extensions/capabilities).\n\nThis page covers the declaration contract, then walks the `@jxsuite/connector` extension — the reference implementation — to show how a full data layer is assembled from it. If you want to _use_ databases rather than provide one, start with the user-level [Databases](/docs/studio/data) docs instead.\n\n## The connector block\n\nThe D1 provider class, `extensions/connector/src/D1.class.json`, declares:\n\n```json\n\"connector\": {\n  \"provider\": \"d1\",\n  \"kind\": \"sqlite\",\n  \"local\": \"sqlite\",\n  \"serve\": \"@jxsuite/connector/worker\",\n  \"module\": \"@jxsuite/connector/d1\"\n}\n```\n\n| Key        | Meaning                                                                                                       |\n| ---------- | ------------------------------------------------------------------------------------------------------------- |\n| `provider` | Identifier for the backing service (`d1`, `supabase`, `sqlite`).                                              |\n| `kind`     | SQL dialect family: `\"sqlite\"` \\| `\"postgres\"`. Consumed by dependents needing a dialect type (e.g. auth).    |\n| `local`    | When `\"sqlite\"`, the dev server stands this connector in with a local SQLite file (auto-synced on first use). |\n| `serve`    | The data-mount module serving this connector's tables.                                                        |\n\n(The class also carries a `module` bare specifier naming the provider implementation for bundler-robust imports, mirroring the `server` block's `module` key.)\n\n## Connector capabilities\n\nThe block grants nothing by itself — behavior attaches through static capability methods declared in `$defs.methods` by `role`:\n\n| Role             | Signature                                                                   | Consumers                  |\n| ---------------- | --------------------------------------------------------------------------- | -------------------------- |\n| `dialect`        | `(connection, env) → Kysely Dialect`                                        | data mounts, auth, deploy  |\n| `deploySchema`   | `(tables, connection, { env, dryRun }) → { statements, applied, warnings }` | `jx db push`, Studio push  |\n| `bindings`       | `(connection) → wrangler config fragment`                                   | scaffolding, `jx db push`  |\n| `testConnection` | `(connection, env) → { ok, error? }`                                        | Studio connections UI, CLI |\n\nAll four are `scope: \"static\"` with `timing: [\"compiler\", \"server\"]` — hosts import the implementation and call them without constructing an instance; browser hosts round-trip through the dev server. `env` is where secret values arrive at call time; the `connection` definition itself carries identifiers and env-var names only ([Security and secrets](/docs/extending/extensions/security)).\n\n## The reference implementation\n\n`@jxsuite/connector` builds a complete data layer from these hooks, wired exactly the way a third-party connector would be.\n\n### Providers\n\nThree provider classes each carry a `connector` block plus the four capabilities: **D1** (Cloudflare, binding-backed inside Workers, HTTP-API-backed for deploys and tests), **Supabase** (postgres over postgres-js/Hyperdrive), and **Sqlite** (file-backed via `bun:sqlite`). Kysely is the shared bridge — one JSON-drivable query and schema builder across all three dialects, on Workers, Bun, and node — so a new provider mostly means implementing `dialect`.\n\n### Project sections\n\nThe extension also owns two [project sections](/docs/extending/extensions/project-sections):\n\n- `connections` — named connections (`Connections.class.json`). Entries carry identifiers and env-var names only; the Studio settings form uses the `secret` control for values.\n- `data` — dynamic tables (`Data.class.json`). Each entry names its connection, a column `schema` (plain Jx field schemas plus relationship refs), an `id` strategy, `timestamps`, `indexes`, `permissions`, and an optional `ownerField`.\n\n### Schema push\n\n`jx db push` (and Studio's **Push Schema** button) plans DDL through each provider's `deploySchema` — **additive-only**: tables and columns are created, never dropped or retyped. `--dry-run` returns the statements without applying. In dev, `local: \"sqlite\"` connections are stood in with `.jx/data/<connection>.sqlite`, auto-synced on first touch, so pushes and queries work before any cloud account exists.\n\n### The data mount\n\n`Data.class.json` also declares a [server mount](/docs/extending/extensions/server) — `/_jx/data`, order 20 — whose handler (`extensions/connector/src/worker.ts`) serves the canonical wire contract (`GET/POST/PATCH/DELETE /_jx/data/:table[/:id]`). Permission rules are evaluated fail-closed against `ctx.auth` from the auth mount.\n\n### State classes and lowering\n\nPages consume tables through state classes: `TableQuery`/`TableEntry` for reads, `TableInsert`/`TableUpdate`/`TableDelete` for form-friendly writes. In compiled sites these **lower** to core defs — queries become plain `Request` fetches against `/_jx/data`, actions become inline `Function` handlers — so no extension code ships to the browser. Read-after-write rides the `_v` convention: lowered queries append `_v=${(state._v || 0)}` to their URL and successful writes bump `state._v`, re-running every query effect on the page.\n\n### Grid support\n\nStudio's [data grid](/docs/studio/data/grid) is the owner console over the same tables. It rides the dev server's `/__studio/data/*` routes rather than the public mount — those intentionally bypass table permission rules and are protected by the dev server's loopback/token boundary instead ([Security and secrets](/docs/extending/extensions/security)).\n\n:::doc-note\nEverything an editor touches — connections, table schemas, permissions, the push button — is documented for Studio users under [Databases](/docs/studio/data), [Connections](/docs/studio/data/connections), and [Data tables](/docs/studio/data/tables). This page describes the extension machinery those surfaces are built on.\n:::\n\n## Related\n\n- [Server mounts](/docs/extending/extensions/server) — the `/_jx/data` mount and the shared context.\n- [Capability methods](/docs/extending/extensions/capabilities) — roles, timing, and lowering.\n- [First-party extensions](/docs/extending/extensions/first-party) — how connector, auth, and parser fit together.\n- [Tutorial: a guestbook extension](/docs/extending/extensions/tutorial-guestbook) — a table-backed feature built on the connector.\n",
    "section": "Extending",
    "slug": "extending/extensions/connectors",
    "title": "Connectors",
    "url": "https://jxsuite.com/docs/extending/extensions/connectors/"
  },
  {
    "description": "How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.",
    "markdown": "\n# Search indexes\n\n`@jxsuite/search` is the reference implementation of the **`emit` capability** — the hook that lets a section-owner class write derived artifacts (here: a search index) into the build output. It is deliberately headless: the extension contributes the `search` project section, the build-time index, a `Search` state class, and a browser client, but no UI. Sites author their own search box against the client's contract.\n\nIf you want to _add search to a site_ rather than study the extension, start with the user-level [Site search](/docs/framework/site/search) page.\n\n## What the manifest declares\n\n```json\n{\n  \"name\": \"@jxsuite/search\",\n  \"classes\": {\n    \"SearchIndex\": \"./src/SearchIndex.class.json\",\n    \"Search\": \"./src/Search.class.json\"\n  },\n  \"schemas\": { \"project\": \"./schemas/project.fragment.schema.json\" }\n}\n```\n\nTwo classes, one schema fragment:\n\n- **`SearchIndex`** owns the `search` section (`project.key: \"search\"`) and carries two capabilities: `projectData` (normalizes the section into `_project.search`) and `emit` (builds the index).\n- **`Search`** is a state class pages query results through; it compiles away via [`lower`](/docs/extending/extensions/capabilities).\n- The fragment contributes the `search` property to the composed [project schema](/docs/extending/extensions/schema-composition): an `engine` (an enum of `minisearch` today, room for more), an `output` path, and per-collection index settings.\n\n## The `emit` capability\n\n`emit` runs at build time, after routes and components compile (extensions.md §8.4). The host passes the section value and a context carrying the **already-loaded** project sections — the emitter never re-reads source files:\n\n```ts\nemit(sectionValue, { projectConfig, root, sections, routes })\n  → [{ path: \"/search-index.json\", content: \"…json…\" }]\n```\n\n`SearchIndex.emit` walks each configured collection in `sections.content` and produces two document granularities per entry:\n\n- a **page document** — title and description from frontmatter, full text extracted from the entry's rendered `$children`;\n- **section documents** (when `sections: true`) — one per heading up to `sectionDepth`, each with the heading text, the section's own text, and a URL ending in `#<heading-id>`. Heading ids are assigned by the parser and always match the rendered anchors ([parser.md §3.2](/docs/framework/site/content-collections)).\n\nThe result is one JSON envelope at the configured `output` path:\n\n```json\n{\n  \"version\": 1,\n  \"engine\": \"minisearch\",\n  \"fields\": [\"title\", \"heading\", \"text\"],\n  \"boost\": { \"title\": 4, \"heading\": 2 },\n  \"documents\": [\n    { \"id\": \"docs:framework/site\", \"url\": \"/docs/framework/site/\", \"heading\": \"\", \"text\": \"…\" },\n    {\n      \"id\": \"docs:framework/site#assets\",\n      \"url\": \"/docs/framework/site/#assets\",\n      \"heading\": \"Assets\",\n      \"text\": \"…\"\n    }\n  ]\n}\n```\n\nThe emitter returns data; **the host writes the files**, guards against path traversal, and skips the emitter entirely when the project declares no `search` section — the same gating as section loading and server mounts.\n\n## Lowering `Search` to client code\n\nA page declares reactive results with the `Search` prototype:\n\n```json\n\"state\": {\n  \"q\": \"\",\n  \"results\": { \"$prototype\": \"Search\", \"query\": { \"$ref\": \"#/state/q\" }, \"timing\": \"client\" }\n}\n```\n\nIn compiled sites `Search.lower()` replaces that def with a core `Function` computed that lazy-imports the bundled client, preloads the index once, and re-queries whenever `state.q` (or the ready flag) changes. No extension code ships to the browser except the client bundle itself, which the lowered def names via **`$bundle`** (extensions.md §8.3):\n\n```js\n{\n  \"$bundle\": [\"npm:@jxsuite/search/client\"],\n  \"$prototype\": \"Function\",\n  \"timing\": \"client\",\n  \"body\": \"…import('/assets/jxsuite-search-client.js') … m.query(state.q, {…})…\"\n}\n```\n\nThe compiler's sidecar bundler resolves `npm:@jxsuite/search/client` from `node_modules`, bundles it (MiniSearch inlined) into `/assets/jxsuite-search-client.js`, and strips `$bundle` from the def. The deterministic URL comes from `sidecarAssetPath` in `@jxsuite/schema/asset-paths`, shared by extension and compiler so neither depends on the other.\n\nInside compiled **components**, use the client's `$src` surface instead — components are not lowered; see [Site search](/docs/framework/site/search).\n\n## Under node\n\n`Search.resolve()` degrades to `[]` with a warning outside the browser — search is a client interaction, and compiler-timing bakes or dev-server resolution should never fail a build over it.\n\n:::doc-tip\nThe `emit` contract generalizes beyond search: RSS/Atom feeds, export manifests, or any derived artifact a section owner can compute from loaded project data fits the same shape.\n:::\n",
    "section": "Extending",
    "slug": "extending/extensions/search",
    "title": "Search indexes",
    "url": "https://jxsuite.com/docs/extending/extensions/search/"
  },
  {
    "description": "The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.",
    "markdown": "\n# Security and secrets\n\nExtensions run real code in three privileged places: the build, the dev server, and the deployed-site worker. The rules on this page are what keep that safe — they are load-bearing for every extension, first-party or third-party, and hosts enforce the ones they can.\n\n## Names ride the wire, values never\n\n**Secrets never enter `project.json`.** Committed config carries identifiers and env-var _names_ only:\n\n```json\n{\n  \"connections\": {\n    \"main\": { \"provider\": \"supabase\", \"urlEnv\": \"SUPABASE_DB_URL\" }\n  },\n  \"auth\": { \"secretEnv\": \"BETTER_AUTH_SECRET\" }\n}\n```\n\nThe same rule extends to everything derived from config: schema fragments describe name fields, `projectData` results expose the section under `_project.<key>` without secrets, and mount `options` are inlined into the generated worker as identifiers only. Secret _values_ reach extension code exclusively through the `env` argument of capability calls (`mount` handlers, `dialect`, `deploySchema`, `testConnection`) at request or invocation time.\n\nWhere the values actually live:\n\n- **Local development** — `<project>/.dev.vars`, git-ignored (the wrangler convention). The dev server merges it over `process.env` when constructing mount environments (`packages/server/src/dev-vars.ts`).\n- **Production** — `wrangler secret put <NAME>`, or the hosting platform's secret store.\n- **Studio** — secret entry goes through the platform's secrets surface (`/__studio/secrets`); its list endpoint returns **names only**, never values. The `\"secret\"` [settings form control](/docs/extending/extensions/project-sections) writes there, never to `project.json`. The user-facing walkthrough is [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\n## Permission boundaries\n\nTwo layers of authorization exist on a running site, and they are deliberately different:\n\n**The public mount surface (`/_jx/*`)** is governed by declared rules. The connector's table permissions (`public | none | authenticated | owner | role:<r>`) are evaluated on every request, and the system **fails closed**: a mount that gates writes on authorization must deny everything except explicitly-public rules when `ctx.auth` is absent from the [shared server context](/docs/extending/extensions/server). Absent auth means 401, not a silent grant — the auth mount's own `getSession` returns `null` on any backend failure for the same reason. Declared `ownerField` columns are stamped server-side with the session's user id, so clients can never forge ownership.\n\n**The Studio data routes (`/__studio/data/*`)** are the owner console: they intentionally bypass table permission rules and are protected by the dev server's loopback/token boundary instead. Cloud backends hosting Studio must gate them on collaboration permission ([Backend protocol](/docs/extending/embedding/backend-protocol)).\n\n:::doc-warning\nA table with `insert: \"public\"` accepts writes from anyone on the internet. Prefer `authenticated` inserts, or accept the risk knowingly — rate limiting and CAPTCHA are not part of the current contract.\n:::\n\n## What extensions may do\n\n- Own a `basePath` under `/_jx/` and serve any routes inside it. Conflicts are a registry error, so no extension can shadow another's subtree.\n- Read secret values from `env` at request time, and publish cooperation hooks on the shared context for later mounts (`ctx.auth` is the model).\n- Contribute steps to the schema push via `deploySchema`, tagged with their own section key.\n- Declare a `\"secret\"` control in `$studio.settings` so Studio routes the value to the secret store.\n\n## What extensions may not do\n\n- Write secret values into `project.json`, return them from `projectData`, or accept them through mount `options` — the generated worker inlines options as committed JSON.\n- Claim routes outside `/_jx/`, or a `basePath` another extension owns.\n- Grant access when their auth dependency is missing — fail closed, always.\n- Be imported by core packages. The dependency rule (`core never depends on extensions`) is CI-enforced, and hosts import extension code only to invoke the static capabilities its descriptors declare — never for side effects.\n\n## Related\n\n- [Server mounts](/docs/extending/extensions/server) — the shared context and the fail-closed rule in situ.\n- [Connectors](/docs/extending/extensions/connectors) — how connections keep secrets out of committed config.\n- [Auth and secrets](/docs/studio/data/auth-and-secrets) — the same model from the Studio user's chair.\n- [Protocol route reference](/docs/extending/reference/studio-routes) — the `/__studio/*` surface, including secrets and data routes.\n",
    "section": "Extending",
    "slug": "extending/extensions/security",
    "title": "Security and secrets",
    "url": "https://jxsuite.com/docs/extending/extensions/security/"
  },
  {
    "description": "Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.",
    "markdown": "\n# Tutorial: a TOML format extension\n\nBy the end of this tutorial you have `@acme/jx-toml`, a standalone npm package that teaches every Jx host — build, dev server, Studio, runtime — to treat `.toml` files as content, with **no changes to any core package**. It is the worked example from the extensions spec, built step by step. Plan on about half an hour.\n\n**Prerequisites**: you have read [The anatomy of an extension](/docs/extending/extensions/anatomy) and [Formats](/docs/extending/extensions/formats), and you have a Jx site project to test against ([Your first project](/docs/start/first-project)).\n\n## 1. Scaffold the package\n\nCreate a package with a `jx` field pointing at the manifest, and export the manifest and class descriptor alongside the code:\n\n```json\n{\n  \"name\": \"@acme/jx-toml\",\n  \"type\": \"module\",\n  \"jx\": \"./jx-extension.json\",\n  \"files\": [\"src/\", \"jx-extension.json\"],\n  \"exports\": {\n    \".\": \"./src/toml.ts\",\n    \"./jx-extension.json\": \"./jx-extension.json\",\n    \"./Toml.class.json\": \"./src/Toml.class.json\"\n  }\n}\n```\n\nThe `\"jx\"` field is how hosts find the manifest; the `exports` entries make the JSON files importable by path, which matters once the package is consumed from `node_modules`.\n\n## 2. Write the manifest\n\n`jx-extension.json` at the package root enumerates what the package provides — here, a single class:\n\n```json\n{\n  \"name\": \"@acme/jx-toml\",\n  \"title\": \"TOML\",\n  \"classes\": { \"Toml\": \"./src/Toml.class.json\" }\n}\n```\n\nThe manifest is pure data: it names things, and behavior lives in the class descriptors it points to. The class key `Toml` becomes the `$prototype`-visible name in every project that enables the extension.\n\n## 3. Declare the format class\n\n`src/Toml.class.json` is an ordinary Jx class descriptor. Its top-level `format` block is the [admission block](/docs/extending/extensions/classes) that puts it into file-extension dispatch:\n\n```json\n{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Toml\",\n  \"description\": \"TOML content files as Jx content entries\",\n  \"$prototype\": \"Class\",\n  \"$implementation\": \"./toml.js\",\n  \"format\": {\n    \"extensions\": [\".toml\"],\n    \"mediaType\": \"application/toml\",\n    \"documentKinds\": [\"content\"]\n  }\n}\n```\n\n`documentKinds: [\"content\"]` admits `.toml` as a content-collection source only — it does not enter pages/components discovery the way the parser's Markdown format does.\n\n:::doc-note\n`$implementation` says `./toml.js` while the source file is `toml.ts` — the TypeScript convention of importing by emitted name. The first-party classes do the same (`Markdown.class.json` points at `./markdown.js`); Bun resolves it to the `.ts` source.\n:::\n\n## 4. Declare the constructor and runtime access\n\nNext, the class's `$defs` describe its instance surface — the standard external-class contract of a `config`-taking constructor plus an instance `resolve()`:\n\n```json\n\"$defs\": {\n  \"parameters\": {\n    \"src\": { \"identifier\": \"src\", \"type\": { \"type\": \"string\" } }\n  },\n  \"constructor\": {\n    \"role\": \"constructor\",\n    \"parameters\": [{ \"$ref\": \"#/$defs/parameters/src\" }],\n    \"body\": [\"this.config = config;\"]\n  },\n  \"methods\": {\n    \"resolve\": {\n      \"role\": \"method\",\n      \"scope\": \"instance\",\n      \"identifier\": \"resolve\",\n      \"returnType\": { \"type\": \"object\" }\n    }\n  }\n}\n```\n\nThis is what makes `{ \"$prototype\": \"Toml\", \"src\": \"./x.toml\" }` work as a state entry at runtime.\n\n## 5. Declare the parse capability\n\n[Capability methods](/docs/extending/extensions/capabilities) are static methods declared by well-known `role` values, alongside `resolve` in `$defs.methods`:\n\n```json\n\"parse\": {\n  \"role\": \"parse\",\n  \"scope\": \"static\",\n  \"identifier\": \"parse\",\n  \"timing\": [\"compiler\", \"server\", \"client\"],\n  \"parameters\": [{ \"identifier\": \"source\", \"type\": { \"type\": \"string\" } }],\n  \"returnType\": { \"type\": \"object\" }\n}\n```\n\n`timing` includes `\"client\"` because TOML parsing needs no filesystem — declaring it lets Studio parse in-process instead of round-tripping through the dev server.\n\n## 6. Declare the content capabilities\n\n`discover` lists entry files for a content source; `load` parses one source into entries. Both are node-only, so `timing` stops at `\"server\"`:\n\n```json\n\"discover\": {\n  \"role\": \"discover\",\n  \"scope\": \"static\",\n  \"identifier\": \"discover\",\n  \"timing\": [\"compiler\", \"server\"],\n  \"parameters\": [\n    { \"identifier\": \"source\", \"type\": { \"type\": \"string\" } },\n    { \"identifier\": \"options\", \"type\": { \"type\": \"object\" } }\n  ],\n  \"returnType\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n},\n\"load\": {\n  \"role\": \"load\",\n  \"scope\": \"static\",\n  \"identifier\": \"load\",\n  \"timing\": [\"compiler\", \"server\"],\n  \"parameters\": [\n    { \"identifier\": \"path\", \"type\": { \"type\": \"string\" } },\n    { \"identifier\": \"options\", \"type\": { \"type\": \"object\" } }\n  ],\n  \"returnType\": { \"type\": \"array\" }\n}\n```\n\n## 7. Implement the statics\n\nThe spec leaves `src/toml.ts` to you, and so does this tutorial — the contract is the interesting part. Per the [host introspection contract](/docs/extending/extensions/anatomy), hosts import `$implementation` and take the export named by the class `title`: so `toml.ts` must export a `Toml` object (or class) whose static methods carry the declared identifiers — `parse` turning TOML source into an object with any TOML library, `discover` globbing `.toml` files under the source directory, `load` reading one file into content entries, and instance `resolve()` for runtime state access.\n\nIf you later want Studio saves or export sidecars for your format, add a `serialize` capability the same way — the parser's `Markdown.class.json` declares one; this example, like the spec's, elides it.\n\n## 8. Use it in a project\n\nA project enables the extension with one line, then names the format on a content type:\n\n```json\n{\n  \"extensions\": [\"@jxsuite/parser\", \"@acme/jx-toml\"],\n  \"content\": {\n    \"settings\": { \"source\": \"./content/settings/\", \"format\": \"Toml\" }\n  }\n}\n```\n\n(`@jxsuite/parser` stays in the list because the `content` section itself belongs to it — your package contributes only the format.)\n\nRun `jx schema` to regenerate the project's entry schema documents. This extension ships no [schema fragment](/docs/extending/extensions/schema-composition) — it contributes no `project.json` section — but the generator still picks up the class, so `\"format\": \"Toml\"` validates in the regenerated format-name enum.\n\nYou should now see the whole surface light up: the content loader discovers and loads `.toml` entries through your class, `ContentCollection`/`ContentEntry` queries work unchanged, pages can declare `{ \"$prototype\": \"Toml\", \"src\": \"./x.toml\" }` state for runtime access, and Studio lists `.toml` files (add a `$studio` block to the descriptor to refine its icon and editing modes).\n\n## What you built\n\nOne package, three JSON files, and one implementation module: a manifest naming a class, a descriptor whose `format` block and capability roles tell every host what the class can do, and statics the hosts invoke — with no host knowing anything TOML-specific.\n\n## Next steps\n\n- [Tutorial: a guestbook extension](/docs/extending/extensions/tutorial-guestbook) — the same pattern extended to project sections, tables, and a server mount.\n- [Formats](/docs/extending/extensions/formats) — everything the `format` block can declare, including `exportTarget` and `remote`.\n- [Content collections](/docs/framework/site/content-collections) — the user-facing model your format now feeds.\n",
    "section": "Extending",
    "slug": "extending/extensions/tutorial-toml-format",
    "title": "Tutorial: a TOML format extension",
    "url": "https://jxsuite.com/docs/extending/extensions/tutorial-toml-format/"
  },
  {
    "description": "Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.",
    "markdown": "\n# Tutorial: a guestbook extension\n\nThis tutorial builds `@acme/jx-guestbook`, the spec's second worked example — a moderated, auth-gated guestbook a site enables with one `extensions` line. Where the [TOML tutorial](/docs/extending/extensions/tutorial-toml-format) exercised the format surface, this one demonstrates the full stack: a `project.json` section with a Studio settings form, a dynamic table, a server mount cooperating with auth, and page state that compiles down to core defs. It builds **on** the connector and auth extensions as dependencies rather than reimplementing them. Plan on about an hour, reading the first-party sources alongside.\n\n**Prerequisites**: [Project sections](/docs/extending/extensions/project-sections), [Server mounts](/docs/extending/extensions/server), and [Connectors](/docs/extending/extensions/connectors); a test project with `@jxsuite/connector` and `@jxsuite/auth` already enabled ([Databases](/docs/studio/data)).\n\nThe spec describes this example at the architecture level, and this tutorial keeps that altitude: each step pins down the declarations and points at the first-party file implementing the same pattern, rather than reprinting implementation code.\n\n## 1. Manifest and fragment\n\nScaffold the package the same way as the [TOML tutorial](/docs/extending/extensions/tutorial-toml-format): a `package.json` with a `\"jx\"` field, a `jx-extension.json` naming one section class (say `Guestbook`), and — because this extension owns a `project.json` section — a project [schema fragment](/docs/extending/extensions/schema-composition) listed under `schemas.project`.\n\nThe fragment's schema defines the section value, `{ table, moderation }` — which table entries land in, and whether they await approval. The section class declares ownership plus a generic settings form:\n\n```json\n\"project\": { \"key\": \"guestbook\" },\n\"$studio\": { \"settings\": { \"layout\": \"form\" } }\n```\n\nYou should now see, after enabling the extension in your test project and running `jx schema`, a `guestbook` key validating in `project.json` — and a **Guestbook** section rendered from your fragment in Studio's Settings, with no Studio code written. The connector's `extensions/connector/schemas/project.fragment.schema.json` and `Data.class.json` are the fuller reference for this step.\n\n## 2. The table\n\nThe guestbook stores entries in an ordinary connector table instead of hand-rolled SQL. Declare a `projectData` [capability](/docs/extending/extensions/capabilities) on the section class that materializes a `data`-style table definition from the section value — columns `name` and `message`, readable by anyone, writable by signed-in users:\n\n```json\n{\n  \"permissions\": { \"read\": \"public\", \"insert\": \"authenticated\" },\n  \"schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"name\": { \"type\": \"string\" },\n      \"message\": { \"type\": \"string\" }\n    }\n  }\n}\n```\n\nBecause this is a standard table definition, entries flow through the connector's standard mount and DDL sync — `jx db push` creates the table additively, the dev server's local SQLite stand-in serves it immediately, and the Studio [data grid](/docs/studio/data/grid) doubles as your moderation console. No custom SQL anywhere.\n\n## 3. The mount\n\nModeration needs server-side logic, so the class also declares a [server block](/docs/extending/extensions/server):\n\n```json\n\"server\": { \"basePath\": \"/_jx/guestbook\", \"order\": 30 }\n```\n\nOrder 30 places it after auth (10) and the data mount (20), so by the time your `mount(options, ctx)` runs, `ctx.auth` is published and `/_jx/data` is live. The mount returns a fetch-style handler that reads `ctx.auth` for session lookups and proxies moderated writes into `/_jx/data/guestbook` — accepting a public submission, stamping it unapproved when `moderation` is on, and letting the table's own permission rules govern everything else. `Auth.mount` in `extensions/auth/src/worker.ts` is the pattern to copy: per-isolate state in the closure, the handler as the return value, secrets only ever read from `env`.\n\nYou should now see `jx dev` answering under `/_jx/guestbook`, with the same behavior the generated worker will have in production.\n\n## 4. The page\n\nThe visitor-facing page needs no extension-specific runtime. It uses the connector's state classes: a `TableQuery` entry listing approved entries, and a form whose submit posts through a `TableInsert` action — both [lowered](/docs/extending/extensions/capabilities) at compile time, so queries become plain `Request` fetches and the action becomes an inline `Function` handler. No extension code ships to the browser, and the `_v` read-after-write convention re-runs the listing query as soon as a write lands.\n\n## 5. Ship it\n\nA project adds the package and syncs:\n\n```json\n{ \"extensions\": [\"@jxsuite/parser\", \"@jxsuite/connector\", \"@jxsuite/auth\", \"@acme/jx-guestbook\"] }\n```\n\nThen `jx schema && jx db push`. You should now have a moderated, auth-gated guestbook — with `project.json` validation, a Studio settings section, and dev-server parity — none of it requiring changes to any core package.\n\n## What you built\n\n| Piece               | Mechanism                                  | First-party reference                            |\n| ------------------- | ------------------------------------------ | ------------------------------------------------ |\n| `guestbook` section | `project` block + schema fragment          | `extensions/connector/src/Data.class.json`       |\n| Settings form       | `$studio.settings` (`layout: \"form\"`)      | `extensions/auth/src/Auth.class.json`            |\n| Entries table       | `projectData` → connector table definition | `extensions/connector/src/Data.class.json`       |\n| `/_jx/guestbook`    | `server` block + `mount`, order 30         | `extensions/auth/src/worker.ts`                  |\n| Page state          | `TableQuery` / `TableInsert`, lowered      | `extensions/connector/src/TableQuery.class.json` |\n\n## Next steps\n\n- [Security and secrets](/docs/extending/extensions/security) — the fail-closed rules your mount inherits.\n- [Server mounts](/docs/extending/extensions/server) — the shared-context contract in full.\n- [First-party extensions](/docs/extending/extensions/first-party) — the three packages this tutorial leaned on.\n",
    "section": "Extending",
    "slug": "extending/extensions/tutorial-guestbook",
    "title": "Tutorial: a guestbook extension",
    "url": "https://jxsuite.com/docs/extending/extensions/tutorial-guestbook/"
  },
  {
    "description": "What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.",
    "markdown": "\n# First-party extensions\n\nFour extensions ship with Jx, and they are deliberately unprivileged: each is wired through the same manifest, admission blocks, and capability roles available to any third-party package. That makes them two things at once — the batteries most projects start with, and the reference implementations to crib from when you build your own. This page maps what each one contributes; follow the links for the mechanics.\n\n| Package              | Sections              | Formats       | Server mounts          | Connector providers  |\n| -------------------- | --------------------- | ------------- | ---------------------- | -------------------- |\n| `@jxsuite/parser`    | `content`             | Markdown, CSV | —                      | —                    |\n| `@jxsuite/connector` | `connections`, `data` | —             | `/_jx/data` (order 20) | D1, Supabase, Sqlite |\n| `@jxsuite/auth`      | `auth`                | —             | `/_jx/auth` (order 10) | —                    |\n| `@jxsuite/search`    | `search`              | —             | —                      | —                    |\n\n## @jxsuite/parser — content and Markdown\n\nThe content layer, and the model [format extension](/docs/extending/extensions/formats).\n\n- **Section**: `content` — file-based content collections, owned by the `Content` class (`referenceable`, so collections join the relationships vocabulary). Its `projectData` and `resolvePaths` capabilities load collections into `_project.content` and expand `$paths` for pages discovery.\n- **Formats**: `Markdown` (`.md`, admitted to pages, components, and content; an `exportTarget` with parse, serialize, discover, and load capabilities) and `Csv` (`.csv`, content-only, `remote: true` so sources may be `http(s)` URLs).\n- **Classes**: `MarkdownCollection`, `ContentCollection`, `ContentEntry` — glob-and-query state prototypes over content files.\n- **Schemas**: the only first-party package shipping a `document` fragment as well as a `project` one — its content-source shapes join both [composed schemas](/docs/extending/extensions/schema-composition).\n- **Studio settings**: the **Content Types** section, a `layout: \"map\"` master-detail with the `schema-builder` control for frontmatter fields.\n\nUser-level docs: [Content collections](/docs/framework/site/content-collections) and [Jx Markdown](/docs/framework/site/jx-markdown).\n\n## @jxsuite/connector — connections and data tables\n\nThe data layer, and the reference for both the [connector block](/docs/extending/extensions/connectors) and a [server mount](/docs/extending/extensions/server).\n\n- **Sections**: `connections` (named database connections; identifiers and env-var names only) and `data` (dynamic tables: column schema, id strategy, indexes, permissions, `ownerField`; `referenceable`).\n- **Providers**: `D1`, `Supabase`, and `Sqlite` classes, each with the four connector capabilities (`dialect`, `deploySchema`, `bindings`, `testConnection`) over a shared Kysely bridge.\n- **Mount**: `/_jx/data` (order 20) — the canonical table wire contract, fail-closed against `ctx.auth`.\n- **Classes**: `TableQuery`, `TableEntry`, `TableInsert`, `TableUpdate`, `TableDelete` — state prototypes that [lower](/docs/extending/extensions/capabilities) to core `Request`/`Function` defs in compiled sites.\n- **Studio settings**: **Connections** and **Data Tables** sections (`layout: \"map\"`), with the `secret` control for connection URLs and the `schema-builder` for table fields; **Test Connection** and **Push Schema** ride the connector capabilities.\n\nUser-level docs: [Databases](/docs/studio/data), [Connections](/docs/studio/data/connections), [Data tables](/docs/studio/data/tables), [Data grid](/docs/studio/data/grid).\n\n## @jxsuite/auth — sessions and permissions\n\nBetter Auth behind the connector's tables, and the reference for mount cooperation through the shared context.\n\n- **Section**: `auth` — sign-in methods, redirects, roles, and the connection its system tables (`user`, `session`, `account`, `verification`) live on.\n- **Mount**: `/_jx/auth` (order 10) — Better Auth's routes, publishing `ctx.auth = { getSession, authorize }` for the data mount to authorize against. Without it, table rules beyond `public`/`none` deny.\n- **Push contribution**: a section-owner `deploySchema` capability contributes the system-table migration to `jx db push` as `kind: \"auth\"` steps ([Server mounts](/docs/extending/extensions/server)).\n- **Classes**: `Session` (the live session, `null` when signed out — and always `null` outside browsers, so static pages render signed-out) and `AuthActions` (`signInEmail`, `signUpEmail`, `signInSocial`, `signOut` handlers to wire onto forms). Both default to client timing via `$studio.stateDefaults`.\n- **Studio settings**: the **Authentication** section (`layout: \"form\"`), with the `secret` control for `secretEnv` — the signing secret itself never touches `project.json` ([Security and secrets](/docs/extending/extensions/security)).\n\nUser-level docs: [Auth and secrets](/docs/studio/data/auth-and-secrets).\n\n## @jxsuite/search — build-time site search\n\nHeadless full-text search, and the reference for the [`emit` capability](/docs/extending/extensions/search).\n\n- **Section**: `search` — which content collections to index, per-collection fields/boosts, and the output path. `projectData` normalizes it into `_project.search`.\n- **Emit**: builds `/search-index.json` at build time from the loaded content collections — page-level documents plus per-heading section documents with `#anchor` deep links (heading ids come from the parser).\n- **Classes**: `Search` — reactive query results in page state, [lowered](/docs/extending/extensions/capabilities) to a core `Function` computed that lazy-loads the bundled MiniSearch client. Defaults to client timing via `$studio.stateDefaults`.\n- **Client**: `@jxsuite/search/client`, a headless browser module (`preload`/`query` plus `$src` state conventions) bundled into `/assets/` by the site build; sites author their own search UI over it.\n- **Studio settings**: the **Site Search** section (`layout: \"form\"`).\n\nUser-level docs: [Site search](/docs/framework/site/search).\n\n## How they depend on each other\n\nAuth depends on the connector (its dialect seam and permission types); search reads what the parser loads but depends only on core; all of them may depend on core packages; core packages never depend on any of them. That direction is CI-enforced, and it is what guarantees the claim these pages keep making: anything the first-party extensions do, yours can do too.\n\n## Related\n\n- [The anatomy of an extension](/docs/extending/extensions/anatomy) — manifests, admission blocks, and the registry.\n- [Connectors](/docs/extending/extensions/connectors) and [Server mounts](/docs/extending/extensions/server) — the machinery connector and auth are built on.\n- [Tutorial: a TOML format extension](/docs/extending/extensions/tutorial-toml-format) and [Tutorial: a guestbook extension](/docs/extending/extensions/tutorial-guestbook) — build your own alongside these references.\n",
    "section": "Extending",
    "slug": "extending/extensions/first-party",
    "title": "First-party extensions",
    "url": "https://jxsuite.com/docs/extending/extensions/first-party/"
  },
  {
    "description": "How Jx Studio runs on any backend: the platform-adapter layer, the wire protocol behind it, and how to choose an integration path for your host.",
    "markdown": "\n# Embedding overview\n\nJx Studio is one web app that runs against whatever backend hosts it. The `@jxsuite/studio` package contains all UI logic and imports no platform-specific modules — the same codebase ships as the desktop app, as the dev-mode editor in Chrome, and as a cloud editor in a plain browser. Everything environment-specific — file I/O, project loading, git, component discovery — is injected at startup. If you're building a backend or host for Studio, this page maps the layers and helps you pick where to plug in.\n\n## The two layers\n\nStudio's backend abstraction has two layers, and you implement exactly one of them:\n\n- **The platform adapter (PAL)** — an in-page JavaScript object implementing the `StudioPlatform` interface, registered via `registerPlatform()` before Studio boots. Every Studio operation calls the current adapter; nothing in Studio fetches a backend URL directly.\n- **The backend protocol** — the wire contract behind the adapters, defined once in `@jxsuite/protocol`: a canonical route table plus the request/response types. The sub-path, method, and body shapes are the contract; the transport and path prefix are free.\n\nEvery adapter ultimately speaks the protocol. The stock adapters differ only in transport: one speaks literal HTTP, one speaks native RPC, one speaks HTTP behind a session gateway.\n\n## Who implements what today\n\n| Host                                     | Platform adapter                                                     | Transport                                                                                                 |\n| ---------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| Dev server (`@jxsuite/server`)           | `packages/studio/src/platforms/devserver.ts` — registered by default | The literal `/__studio/*` HTTP routes — the protocol's reference implementation                           |\n| Desktop (ElectroBun / Chromium app-mode) | `packages/desktop/src/platform.ts`                                   | Typed RPC / WebSocket JSON-RPC into the same handlers, behind a loopback-only, token-gated project server |\n| Cloud platforms                          | `packages/studio/src/platforms/cloud.ts`                             | HTTP behind a session gateway (`/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes      |\n\n## Choosing your integration path\n\n**Serve the HTTP protocol** when your backend can expose HTTP endpoints. Implement the protocol's core routes at `/__studio/*` (or an equivalent prefix) and reuse the stock dev-server adapter unchanged — Studio registers it automatically when no other platform is present. Your backend can be written in any language; a conformance check is just iterating the route table and asserting every core route answers. This is the least code and the path most hosted backends take. Start at [the backend protocol](/docs/extending/embedding/backend-protocol).\n\n**Implement `StudioPlatform` in-page** when the transport isn't plain HTTP — a native RPC bridge like the desktop app's, a session gateway with its own path scheme and auth like the cloud adapter's — or when you need to own the client-side pieces the protocol can't cover. Project opening is the clearest example: it's inherently a client concern (a native file dialog on desktop, `showDirectoryPicker()` in Chrome, a project list on cloud), so it lives in the adapter, not on the wire. Start at [writing a platform adapter](/docs/extending/embedding/platform-adapter).\n\nEither way, the semantics come from `@jxsuite/protocol` — an in-page adapter should behave exactly as the corresponding routes describe, because Studio can't tell the difference.\n\n## Partial backends are fine\n\nYou don't have to implement everything. Optional protocol routes back optional `StudioPlatform` members; Studio checks for their presence and degrades exactly as each route's `degradation` entry describes — the starter picker empties, the Projects catalogue hides, realtime co-editing falls back to solo file-level saves, and so on. The [protocol route reference](/docs/extending/reference/studio-routes) lists every route with its optionality and degradation.\n\n## Related\n\n- [Writing a platform adapter](/docs/extending/embedding/platform-adapter) — the `StudioPlatform` interface, registration, and the two real adapters\n- [The backend protocol](/docs/extending/embedding/backend-protocol) — contract semantics, versioning, errors, and required behaviors\n- [Dev server internals](/docs/extending/embedding/dev-server) — how the reference implementation is put together\n- [Protocol route reference](/docs/extending/reference/studio-routes) — the generated route table\n",
    "section": "Extending",
    "slug": "extending/embedding",
    "title": "Embedding overview",
    "url": "https://jxsuite.com/docs/extending/embedding/"
  },
  {
    "description": "Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.",
    "markdown": "\n# Writing a platform adapter\n\nA platform adapter is a plain JavaScript object implementing the `StudioPlatform` interface, registered once before Studio boots. Every backend-touching operation in Studio — file I/O, project loading, git, component discovery, the AI proxy — goes through the registered adapter; Studio itself never fetches a backend URL directly. Write one when your host can't simply serve the HTTP protocol (see the [embedding overview](/docs/extending/embedding) for that decision).\n\nThe authoritative interface is `StudioPlatform` in `packages/studio/src/types.ts`. It is wider than the sketch in the desktop spec §3.1 — the real interface adds git, packages, collaboration, the data surface, and publish members on top of the original file and project operations.\n\n## The interface surface\n\nCore members are required on every adapter. Grouped by family:\n\n| Family          | Members                                                                                                                                                                                                        |\n| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Identity        | `id`, `projectRoot` (get/set), `canvasUrl?`                                                                                                                                                                    |\n| Session/project | `activate`, `openProject`, `probeRootProject`, `createDestination`, `createProject`, `resolveSiteContext`                                                                                                      |\n| Filesystem      | `listDirectory`, `readFile`, `writeFile`, `uploadFile`, `deleteFile`, `renameFile`, `createDirectory`, `locateFile`, `searchFiles`, `discoverComponents`                                                       |\n| Git             | `gitStatus`, `gitBranches`, `gitLog`, `gitStage`, `gitUnstage`, `gitCommit`, `gitPush`, `gitPull`, `gitFetch`, `gitCheckout`, `gitCreateBranch`, `gitDiff`, `gitShow`, `gitDiscard`, `gitInit`, `gitAddRemote` |\n| Packages        | `listPackages`, `addPackage`, `removePackage`                                                                                                                                                                  |\n| Services        | `codeService`, `fetchPluginSchema`, `aiChatUrl`                                                                                                                                                                |\n\nAll paths passed into adapter methods are project-relative; translating them to whatever the backend expects (a server-root prefix, an absolute path, a repo path) is the adapter's job. A core member may still answer \"not available\" through its return type — `codeService` resolves `null` on platforms without code tooling — but the member itself must exist.\n\n`createDestination` is a declaration, not a method: set it to `\"path\"` if your backend writes projects to a filesystem, or `\"repo\"` if a project is a remote repository. Studio uses it to decide which destination fields the New Project modal collects, and hands the answer back to `createProject` as `opts.destination`. **Your adapter must honor that destination and must not substitute one of its own** — a create with no usable destination is an error, not a cue to fall back to a default directory or account.\n\n### Optional members and degradation\n\nEverything else on the interface is optional, and each optional member maps to an optional protocol route whose `degradation` field describes exactly what Studio does without it. Omit what your backend can't support:\n\n| Family             | Members                                                                                                      | When absent                                                                                                                                   |\n| ------------------ | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| Live sync          | `subscribeFileEvents`, `collab`                                                                              | Sidebar is manual-refresh; editing is solo with file-level saves                                                                              |\n| Install pipeline   | `installDependencies`, `dependenciesNeedInstall`, `outdatedPackages`, `setPackageVersions`                   | Install/update affordances hide; manifest-only edits still work                                                                               |\n| Catalogue/scaffold | `listProjects`, `listStarters`, `importSite`, `pickDirectory`, `gitClone`                                    | Welcome-screen catalogue, starters, import, and clone flows hide; without `pickDirectory` the New Project **Location** field is typed by hand |\n| Formats/schemas    | `listFormats`, `listExtensions`, `fetchProjectSchemas`, `formatAction`, `resolveClass`                       | Only `.json` documents open; editors fall back to bundled schemas                                                                             |\n| Data + secrets     | `dataConnections`, `dataConnectionTest`, `dataPush`, `dataRows`, row CRUD, `listSecrets`, `setSecrets`       | The Data grid and connection/push/secret controls hide                                                                                        |\n| Publish            | `cfConnection`, `cfConnect`, `cfApi`, `createPullRequest`                                                    | Publish panel explains git-push publishing; PRs go via user token                                                                             |\n| Desktop shell      | `getAppInfo`, `openProjectInNewWindow`, `newWindow`, `setWindowProject`, `getProjectRoot`, recents, settings | Single-window; recents and settings persist in `localStorage`                                                                                 |\n| Cloud identity     | `getUser`, `getAccountStatus`, `listRepos`, `importProject`                                                  | No signed-in identity or repository picker                                                                                                    |\n\nStudio always checks for presence before calling an optional member, so an omitted member is never an error. The [protocol route reference](/docs/extending/reference/studio-routes) is the complete degradation catalogue.\n\nA browser-hosted adapter can still offer `pickDirectory`: `@jxsuite/studio/directory-picker` exports `canPickDirectory()` and `pickDirectoryPath(locate)`, which drive `showDirectoryPicker()` and hand you the picked folder's `name` plus the random id it wrote into a hidden `.jx-loc-id` there — your `locate` callback resolves that pair to an absolute path (the dev server does it with `GET /__studio/locate-directory`). Omit the member when `canPickDirectory()` is false rather than defining one that always returns null, so Studio hides the button instead of showing a dead one.\n\n## Registration\n\nRegistration is a module-level setter backed by a global, so an adapter can be registered from a separate script bundle that loads before `studio.js`:\n\n```ts\n// packages/studio/src/platform.ts\nconst g = globalThis as unknown as { __jxPlatform?: StudioPlatform };\n\nexport function registerPlatform(platform: StudioPlatform) {\n  g.__jxPlatform = platform;\n}\n\nexport function getPlatform() {\n  if (!g.__jxPlatform) {\n    throw new Error(\"No platform registered. Call registerPlatform() before starting Studio.\");\n  }\n  return g.__jxPlatform;\n}\n```\n\nThe desktop app does exactly this — a four-line init bundle injected ahead of the Studio bundle:\n\n```ts\n// packages/desktop/src/init.ts — loaded before studio.js\nimport { registerPlatform } from \"@jxsuite/studio/platform\";\nimport { createDesktopPlatform } from \"./platform\";\n\nregisterPlatform(createDesktopPlatform());\n```\n\nIf nothing has registered by the time Studio boots, it self-registers the dev-server adapter: `if (!hasPlatform()) registerPlatform(createDevServerPlatform())`. So an embedder that serves the HTTP protocol needs no registration code at all, and one that doesn't must win the race by loading its init script first.\n\n## The project-open flow\n\nOpening a project is the one flow the adapter owns end to end, because the picking UI is inherently platform-specific:\n\n1. The user triggers **Open Project**; Studio calls `getPlatform().openProject()`.\n2. The adapter presents its own picker — a native file dialog on desktop, `showDirectoryPicker()` in Chrome, a project list on cloud — and resolves the choice to a project root containing `project.json`.\n3. It returns `{ config, handle: { root, name, projectConfig } }`, or `null` if the user cancelled (never throw for a cancel).\n4. Studio initializes project state from the handle: file tree, component registry, expanded directories.\n\nTwo supporting members round out the flow. `activate(root)` tells the backend which project root subsequent operations (and static file serving) should resolve against — the dev-server adapter calls it whenever `projectRoot` is set. It must **reject when the backend refuses the root**, rather than resolving quietly: operations that carry no explicit directory resolve against the backend's own root, so a swallowed refusal leaves the session reading and writing the wrong tree. `probeRootProject()` runs at startup to auto-detect whether the backend's root is itself a project, powering the zero-click open in dev mode.\n\n## Two real adapters\n\n### Dev server (`packages/studio/src/platforms/devserver.ts`)\n\nThe reference adapter is a stateless wrapper over `fetch`: every member maps 1:1 to a `/__studio/*` route, and its only state is the active project root, which it prefixes onto outgoing paths and strips from responses (`serverPath`/`stripRoot`). Its `openProject` shows how client-side picking meets a server-side backend — the browser picks a directory, then the adapter matches it to a server path:\n\n```ts\n// openProject, abbreviated: match the picked directory to a server-known project\nconst sitesRes = await fetch(\"/__studio/sites\");\nconst sites = await readJson<SiteEntry[]>(sitesRes);\nconst match = sites.find((s: SiteEntry) => JSON.stringify(s.config) === JSON.stringify(config));\n\nif (!match) {\n  // Project is outside dev server root — ask the server to find it by directory name\n  const findRes = await fetch(`/__studio/find-project?name=${encodeURIComponent(dirHandle.name)}`);\n  // …\n}\n```\n\n### Desktop (`packages/desktop/src/platform.ts`)\n\nThe desktop adapter translates the same interface into ElectroBun RPC: each member is a one-line `rpc.request.*` call into Bun-side handlers (`openProject()` is literally `return await rpc.request.openProject()`, backed by a native file dialog in the Bun process). Beyond the mapping, it patches `window.fetch` so the runtime's dev-proxy endpoints (`/__jx_resolve__`, `/__jx_server__`) also ride the RPC bridge, and it implements the desktop-only families — multi-window, backend-persisted recents and settings, `getAppInfo`.\n\n## Related\n\n- [Embedding overview](/docs/extending/embedding) — choosing between an adapter and the HTTP protocol\n- [The backend protocol](/docs/extending/embedding/backend-protocol) — the semantics your adapter must preserve\n- [Protocol route reference](/docs/extending/reference/studio-routes) — every route with optionality and degradation\n",
    "section": "Extending",
    "slug": "extending/embedding/platform-adapter",
    "title": "Writing a platform adapter",
    "url": "https://jxsuite.com/docs/extending/embedding/platform-adapter/"
  },
  {
    "description": "The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.",
    "markdown": "\n# The backend protocol\n\nThe Studio Backend Protocol is the wire contract every Jx Studio backend implements. It lives in one package, `@jxsuite/protocol`, with two entrypoints: `@jxsuite/protocol/types` holds the request/response shapes (`DirEntry`, `GitStatusResult`, `StarterInfo`, and friends — environment-agnostic, importable in browsers, Bun, and Workers alike), and `@jxsuite/protocol/routes` holds `STUDIO_ROUTES`, the canonical endpoint table. If you're serving the protocol over HTTP, this page plus the [route reference](/docs/extending/reference/studio-routes) is your specification; if you're [writing a platform adapter](/docs/extending/embedding/platform-adapter) instead, these are still the semantics your adapter must preserve.\n\n## What exactly is the contract\n\nThe **sub-path, method, and body shapes are the contract** — the transport and path prefix are not. The dev server serves the literal `/__studio/*` routes; the desktop app dispatches the same operations over typed RPC / WebSocket JSON-RPC; cloud platforms serve them over HTTP behind a session gateway prefix. All three answer the same shapes for the same sub-paths, so Studio can't tell them apart.\n\nEach entry in `STUDIO_ROUTES` carries its method, path, a one-line contract summary, and — for optional routes — what Studio does without it:\n\n```ts\n// packages/protocol/src/routes.ts\nexport interface StudioRoute {\n  path: string;\n  method: StudioRouteMethod;\n  /** True when a backend may omit the route (its PAL member is optional). */\n  optional: boolean;\n  /** One-line contract summary. */\n  summary: string;\n  /** What Studio does when an optional route is absent. */\n  degradation?: string;\n}\n```\n\nThe full table is generated into the [protocol route reference](/docs/extending/reference/studio-routes) — consult it there rather than re-reading the source.\n\n## Core vs optional\n\nOptional routes back optional `StudioPlatform` members. Omitting one is not an error: Studio degrades exactly as the entry's `degradation` describes — the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing instead, and so on. `coreRouteNames()` and `optionalRouteNames()` split the table programmatically, so a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert that every core route answers.\n\nThe core set is what a minimal backend must serve: project session and probing (`activate`, `project`, `project-info`, `resolve-site`, `create-project`), the filesystem CRUD family, component discovery, package listing, the git suite, and the AI proxy pair (`ai/chat`, `ai/models`). Everything else — collab, starters, the data surface, secrets, Cloudflare publishing — is optional.\n\n## Versioning\n\n`STUDIO_PROTOCOL_VERSION` (currently `1`) bumps whenever any route's request or response shape changes incompatibly. The types are published as TypeScript source on the monorepo's release train; the package's only runtime dependency is `@jxsuite/schema`.\n\n## The error shape\n\nEvery failure is an `ErrorBody` with a meaningful HTTP status:\n\n```ts\n// packages/protocol/src/types.ts\nexport interface ErrorBody {\n  error: string;\n  /** Machine-readable discriminator (e.g. \"remote_moved\", \"cf_not_connected\"). */\n  code?: string;\n  detail?: unknown;\n}\n```\n\n`error` is the human-readable message; `code` is the machine-readable discriminator Studio switches on — `pull_conflict`, `remote_moved`, `cf_not_connected`, `needs_installation_access`. Return the right `code` and Studio shows its purpose-built recovery UI instead of a generic error toast.\n\n## Behaviors implementers must match\n\nRoute shapes alone don't capture everything. These semantics are part of the contract:\n\n- **`git/commit`** commits the staged files if any are staged, otherwise all dirty files. Cloud backends may make commit+push atomic and treat `git/push` as a sync check (`ahead` stays 0).\n- **`git/pull`** returns `409 { code: \"pull_conflict\", conflicts: [paths] }` when local dirty files overlap remote changes; a clean pull fast-forwards.\n- **`format`** dispatches `{ format, action: \"parse\" | \"serialize\", source? | doc?, options? }` through the project's format registry. Without it, only `.json` documents open.\n- **`ai/chat`** accepts `{ messages, tools, systemPrompt, model }` and streams the normalized `StreamEvent` SSE defined by `@jxsuite/ai/streaming-client`. `ai/models` returns `AiModelsResponse`; report `configured: true` when the backend holds credentials (Studio then unlocks the assistant without a locally stored key) and `managed: true` when the platform brokers them.\n- **`collab`** is a WebSocket upgrade speaking the `@jxsuite/collab` wire envelope — one socket per project, documents multiplexed by path. A plain GET (no Upgrade) answers `{ collab: true, version }` as the capability probe.\n- **`cf/proxy`** is an allowlisted Cloudflare API passthrough (accounts and Pages projects/deployments only). The backend injects credentials — a header-borne user token on the dev server, a vaulted OAuth token on cloud — and stateless implementations must never persist them.\n- **The data routes** (`data/connections`, `data/rows`, `data/push`, …) intentionally bypass table permission rules — they are the owner console, and the backend boundary (loopback/token locally, collaboration permission on cloud) is the gate. The secrets routes carry env-var **names only**, never values. See [connectors](/docs/extending/extensions/connectors) and the [extension security model](/docs/extending/extensions/security).\n\n## Related\n\n- [Protocol route reference](/docs/extending/reference/studio-routes) — the generated table: every route, method, summary, and degradation\n- [Dev server internals](/docs/extending/embedding/dev-server) — the reference implementation of these routes\n- [Writing a platform adapter](/docs/extending/embedding/platform-adapter) — the in-page interface these routes back\n",
    "section": "Extending",
    "slug": "extending/embedding/backend-protocol",
    "title": "The backend protocol",
    "url": "https://jxsuite.com/docs/extending/embedding/backend-protocol/"
  },
  {
    "description": "Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.",
    "markdown": "\n# Dev server internals\n\n`@jxsuite/server` is the reference implementation of the [backend protocol](/docs/extending/embedding/backend-protocol): a Bun-native dev server whose `/__studio/*` mount is the literal route table, plus the proxies and services that make documents behave in development as they do in production. This page is the architecture tour for backend implementers — for using the server day to day, see [the dev server](/docs/framework/build/dev-server) in the Framework section.\n\n## One handler, an ordered route chain\n\n`createDevServer()` stands up a single `Bun.serve` fetch handler that checks routes in a fixed order; the first claimant wins, and anything unclaimed falls through to static file serving:\n\n1. `/__reload` — the SSE live-reload endpoint (skipped when `watch: false`).\n2. `POST /__jx_resolve__` — the `$prototype`/`$src` resolution proxy.\n3. `POST /__jx_server__` — the `timing: \"server\"` function proxy.\n4. `/_jx/*` — extension server mounts.\n5. `/__studio/*` — the Studio API (skipped when `studio: false`): the collab WebSocket upgrade, `activate`, then the AI, site-import, and code sub-APIs, then `handleStudioApi` for everything else.\n6. Custom `middleware`, if the embedder passed one.\n7. Static files: project files at their natural URLs, the active project's tree, `public/` at the site root, and bare npm specifiers resolved through `node_modules` and bundled on demand.\n\nOrdering is load-bearing: the privileged endpoints must be claimed before the static fallback can ever see their paths, and `middleware` deliberately sits after the protocol routes so an embedder cannot accidentally shadow them.\n\n## The Studio API mount and activation\n\n`handleStudioApi` (in `studio-api.ts`) implements the protocol's route table against the filesystem. Its central piece of state is the **active project root**: `POST /__studio/activate` stores an absolute path, and from then on file operations, static serving, and format dispatch resolve against it. This is what lets one server open projects that live outside its own `root`.\n\nFormat and extension behavior is served from a per-project **extension registry**, built by scanning the project's dependencies and cached against `project.json`'s mtime — edit the manifest and the next request rebuilds the registry. The `formats`, `format`, and `project-schemas` routes all answer from it.\n\n## The resolve proxies\n\n`resolve.ts` implements the two endpoints the runtime uses to run server-side code during development. `handleResolve` takes a `$prototype`/`$src` entry, imports the module server-side (`.js` directly; `.class.json` via its `$implementation`, or a class constructed from the schema), instantiates it with the config, and returns the resolved value. `handleServerFunction` imports a module and invokes a named export with an arguments object. Both do dynamic `import()` of project code — they are remote-code-execution surfaces by design, which is why the security model below exists.\n\n`code-api.ts` adds the code services behind the `codeService` platform member: `oxfmt` formatting, `oxlint` diagnostics, and `Bun.Transpiler` minification for the function-body editor.\n\n## Extension server mounts\n\n`jx-mounts.ts` dispatches `/_jx/*` to extension classes that declare a `server` block — the same fetch-style handlers the generated site worker mounts in production, so data-backed documents work identically in both environments. In development it adds conveniences: `env` is `process.env` merged under the project's `.dev.vars` plus `JX_PROJECT_ROOT`, connectors with a `local` provider get stood in locally (D1 becomes SQLite under `.jx/data/`), and table schemas sync additively on first touch. See [server mounts](/docs/extending/extensions/server) for the extension-author side.\n\n## The security model\n\nThe dev server and the desktop's `createProjectServer` share one set of primitives (`packages/server/src/net-guard.ts`); the dev server applies all but the token:\n\n- **Loopback bind** (`127.0.0.1`) by default is the primary control — other local processes and LAN pages can't read a loopback page's location. A `hostname` option (`jx dev --host`) can widen the bind for containers, but that removes the control and should only be used behind trusted isolation.\n- **Origin/Host gate** guards every privileged surface — the RCE-capable `/__jx_resolve__` / `/__jx_server__` routes, the `/_jx/` mounts, and the whole `/__studio/*` API. A loopback (or absent) Origin passes; a cross-origin Origin or a rebinding Host is rejected. The browser Studio and the served site are same-origin, so they pass — a malicious external page does not. The dev server needs no token because it is same-origin; the desktop server, whose canvas iframe is cross-origin, adds a per-server token on top.\n- **File containment** (`containedPath`) pairs a lexical `relative()` check with a `realpath` re-check, so a `../` or absolute path can't escape and a symlink can't point outside the project root. It guards static serving, `assertAccessible` for the filesystem API, and every `$src` / `$base` / `$implementation` resolved before a dynamic `import()`. Over-encoded paths are rejected after a single decode.\n- **Two-root activation**: `assertAccessible(filePath, root, activeProjectRoot)` allows a path under the server root or the project root Studio bound via `POST /__studio/activate`. Activation itself accepts four kinds of root: one contained under the server root, an explicit `allowedRoots` entry, a project this server just created, or a project the account already owns — an absolute directory holding a `project.json` under the user's home directory. That last kind is what opens an _existing_ project: projects normally live outside whatever tree the server happens to serve. Anything else is a `403`, and a refused activation is an error to show the user — the endpoints that take no `dir` (the git surface especially) fall back to the server's own root, so swallowing the refusal would quietly act on the wrong tree.\n\n:::doc-warning\nThe resolve proxies import and execute arbitrary project code by design. Keep the loopback bind, and if you must widen it, put the server behind trusted isolation — the Origin/Host gate assumes the network itself is not hostile.\n:::\n\n## Related\n\n- [The dev server](/docs/framework/build/dev-server) — the user-level page: options, live reload, the proxies from the document author's view\n- [The backend protocol](/docs/extending/embedding/backend-protocol) — the contract this server is the reference for\n- [Protocol route reference](/docs/extending/reference/studio-routes) — every `/__studio/*` route it serves\n- [Extension security model](/docs/extending/extensions/security) — the trust boundaries extensions themselves live under\n",
    "section": "Extending",
    "slug": "extending/embedding/dev-server",
    "title": "Dev server internals",
    "url": "https://jxsuite.com/docs/extending/embedding/dev-server/"
  },
  {
    "description": "Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Protocol route reference\n\nThe canonical Studio Backend Protocol route table (protocol version 1), from `@jxsuite/protocol`. The dev server is the reference implementation; any backend serving these shapes can host Studio. Optional routes back optional platform-adapter members — Studio degrades without them as described.\n\n## Session / project\n\n| Route             | Method | Path                         | Summary                                                                                 | Optional | Degradation                                                                   |\n| ----------------- | ------ | ---------------------------- | --------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------- |\n| `activate`        | POST   | `/__studio/activate`         | Bind the backend to a project root                                                      | no       | —                                                                             |\n| `project`         | GET    | `/__studio/project`          | Root project metadata {name, root}                                                      | no       | —                                                                             |\n| `projectInfo`     | GET    | `/__studio/project-info`     | Probe a directory: {isSiteProject, projectConfig?, directories?}                        | no       | —                                                                             |\n| `resolveSite`     | GET    | `/__studio/resolve-site`     | Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?}         | no       | —                                                                             |\n| `sites`           | GET    | `/__studio/sites`            | Enumerate site projects [{config, path}] (backs listProjects)                           | yes      | The welcome screen's Projects catalogue stays hidden.                         |\n| `findProject`     | GET    | `/__studio/find-project`     | Locate a project directory by name outside the root                                     | yes      | openProject falls back to config-matching only.                               |\n| `locateDirectory` | GET    | `/__studio/locate-directory` | Resolve the absolute path of a showDirectoryPicker() folder by the id in its .jx-loc-id | yes      | The New Project Location field loses its Browse… button and is typed by hand. |\n| `createProject`   | POST   | `/__studio/create-project`   | Scaffold a project at a caller-chosen destination → {root, config}                      | no       | —                                                                             |\n| `starters`        | GET    | `/__studio/starters`         | Starter templates (StarterInfo[])                                                       | yes      | The New Project picker offers only blank/templates.                           |\n| `importSite`      | POST   | `/__studio/import-site`      | Clone a live website into a project; streams NDJSON progress                            | yes      | The New Project Import tab is unavailable.                                    |\n\n## Filesystem\n\n| Route        | Method | Path                    | Summary                                                                                                                                                                                                                                                                                                                                                                                   | Optional | Degradation                                                                 |\n| ------------ | ------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------- |\n| `files`      | GET    | `/__studio/files`       | List a directory (DirEntry[]); with ?glob=<pattern>, search matching files project-wide                                                                                                                                                                                                                                                                                                   | no       | —                                                                           |\n| `fileRead`   | GET    | `/__studio/file`        | Read a file's text content                                                                                                                                                                                                                                                                                                                                                                | no       | —                                                                           |\n| `fileWrite`  | PUT    | `/__studio/file`        | Write a file's text content                                                                                                                                                                                                                                                                                                                                                               | no       | —                                                                           |\n| `fileDelete` | DELETE | `/__studio/file`        | Delete a file                                                                                                                                                                                                                                                                                                                                                                             | no       | —                                                                           |\n| `fileUpload` | POST   | `/__studio/file/upload` | Upload binary content to a path                                                                                                                                                                                                                                                                                                                                                           | no       | —                                                                           |\n| `fileRename` | POST   | `/__studio/file/rename` | Rename/move (+ refactor report)                                                                                                                                                                                                                                                                                                                                                           | no       | —                                                                           |\n| `locate`     | POST   | `/__studio/locate`      | Find a file by name → {path \\| null}                                                                                                                                                                                                                                                                                                                                                      | no       | —                                                                           |\n| `collab`     | GET    | `/__studio/collab`      | Realtime co-editing: a WebSocket upgrade speaking the @jxsuite/collab wire envelope (one socket per project, documents multiplexed by path; y-protocols sync + project-level awareness in lib0 binary frames — see @jxsuite/collab/envelope for the frame layout and the docEpoch/doc-reset lifecycle). A plain GET (no Upgrade) answers {collab: true, version} as the capability probe. | yes      | Realtime co-editing is unavailable; Studio edits solo with file-level saves |\n\n## Documents / components / formats\n\n| Route            | Method | Path                        | Summary                                                                                                                                                                                                                                         | Optional | Degradation                                                                                                      |\n| ---------------- | ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |\n| `components`     | GET    | `/__studio/components`      | Discover components (ComponentMeta[])                                                                                                                                                                                                           | no       | —                                                                                                                |\n| `cem`            | GET    | `/__studio/cem`             | Custom-elements manifest of an npm dependency                                                                                                                                                                                                   | yes      | Dependency components lose prop/slot metadata.                                                                   |\n| `formats`        | GET    | `/__studio/formats`         | The project's format registry entries plus a sibling `extensions` array — per enabled extension its manifest identity and project-section contributions (ExtensionsInfo[]; backs listFormats and listExtensions).                               | yes      | Only .json documents open (backs listFormats); descriptor-contributed settings sections do not appear.           |\n| `projectSchemas` | GET    | `/__studio/project-schemas` | The project's generated entry documents (project.schema.json / document.schema.json), PRE-BUNDLED into self-contained schemas — {project, document} (backs fetchProjectSchemas). Regenerated on demand when missing or older than project.json. | yes      | The JSON editor falls back to the bundled core schemas (extension sections get no editor validation/completion). |\n| `format`         | POST   | `/__studio/format`          | Dispatch a format capability {format, action: parse\\|serialize, source?\\|doc?}                                                                                                                                                                  | yes      | Non-JSON documents cannot be parsed/serialized (backs formatAction).                                             |\n| `pluginSchema`   | GET    | `/__studio/plugin-schema`   | Extract a $studio schema from a class source                                                                                                                                                                                                    | yes      | Plugin property panels fall back to generic JSON editing.                                                        |\n| `codeFormat`     | POST   | `/__studio/code/format`     | Format posted source {code, path?} → {code, errors}                                                                                                                                                                                             | yes      | Code editors skip format-on-open/save (codeService returns null).                                                |\n| `codeMinify`     | POST   | `/__studio/code/minify`     | Minify posted source {code} → {code}                                                                                                                                                                                                            | yes      | Compiled-output minification is skipped.                                                                         |\n| `codeLint`       | POST   | `/__studio/code/lint`       | Lint posted source {code, path?} → {diagnostics}                                                                                                                                                                                                | yes      | Code editors show no lint markers.                                                                               |\n\n## Packages\n\n| Route                  | Method | Path                               | Summary                                           | Optional | Degradation                                                               |\n| ---------------------- | ------ | ---------------------------------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------------- |\n| `packages`             | GET    | `/__studio/packages`               | List dependencies (PackageInfo[])                 | no       | —                                                                         |\n| `packagesAdd`          | POST   | `/__studio/packages/add`           | Add a dependency                                  | no       | —                                                                         |\n| `packagesRemove`       | POST   | `/__studio/packages/remove`        | Remove a dependency                               | no       | —                                                                         |\n| `packagesInstall`      | POST   | `/__studio/packages/install`       | Run the package manager install                   | yes      | Install/reinstall affordances are hidden; manifest-only edits still work. |\n| `packagesNeedsInstall` | GET    | `/__studio/packages/needs-install` | Whether node_modules is stale                     | yes      | The install-on-open prompt never shows.                                   |\n| `packagesOutdated`     | GET    | `/__studio/packages/outdated`      | Dependencies with newer versions (OutdatedInfo[]) | yes      | The update affordances are hidden.                                        |\n| `packagesSetVersions`  | POST   | `/__studio/packages/set-versions`  | Rewrite dependency ranges and install             | yes      | Bulk version updates are hidden.                                          |\n\n## Git\n\n| Route             | Method | Path                          | Summary                                         | Optional | Degradation                                                          |\n| ----------------- | ------ | ----------------------------- | ----------------------------------------------- | -------- | -------------------------------------------------------------------- |\n| `gitStatus`       | GET    | `/__studio/git/status`        | Working-tree status (GitStatusResult)           | no       | —                                                                    |\n| `gitBranches`     | GET    | `/__studio/git/branches`      | Branch list (GitBranchesResult)                 | no       | —                                                                    |\n| `gitLog`          | GET    | `/__studio/git/log`           | Recent commits (GitLogEntry[])                  | no       | —                                                                    |\n| `gitStage`        | POST   | `/__studio/git/stage`         | Stage files                                     | no       | —                                                                    |\n| `gitUnstage`      | POST   | `/__studio/git/unstage`       | Unstage files                                   | no       | —                                                                    |\n| `gitCommit`       | POST   | `/__studio/git/commit`        | Commit staged (else all dirty) files            | no       | —                                                                    |\n| `gitPush`         | POST   | `/__studio/git/push`          | Push (cloud: sync check — commits land on push) | no       | —                                                                    |\n| `gitPull`         | POST   | `/__studio/git/pull`          | Pull/fast-forward; 409 {conflicts} on overlap   | no       | —                                                                    |\n| `gitFetch`        | POST   | `/__studio/git/fetch`         | Refresh remote tracking state                   | no       | —                                                                    |\n| `gitCheckout`     | POST   | `/__studio/git/checkout`      | Switch branches                                 | no       | —                                                                    |\n| `gitCreateBranch` | POST   | `/__studio/git/create-branch` | Create a branch                                 | no       | —                                                                    |\n| `gitDiff`         | GET    | `/__studio/git/diff`          | Unified diff of dirty files (or one path)       | no       | —                                                                    |\n| `gitShow`         | GET    | `/__studio/git/show`          | File content at a ref                           | no       | —                                                                    |\n| `gitDiscard`      | POST   | `/__studio/git/discard`       | Discard working changes                         | no       | —                                                                    |\n| `gitInit`         | POST   | `/__studio/git/init`          | Initialize a repository                         | no       | —                                                                    |\n| `gitAddRemote`    | POST   | `/__studio/git/add-remote`    | Add a remote                                    | no       | —                                                                    |\n| `gitClone`        | POST   | `/__studio/git/clone`         | Clone a repository                              | yes      | The welcome screen hides Clone Git Repository.                       |\n| `gitPr`           | POST   | `/__studio/git/pr`            | Open a pull request → PullRequestInfo           | yes      | Studio falls back to a direct GitHub API call with the user's token. |\n\n## Data surface (connector domain owner console)\n\n| Route                | Method | Path                              | Summary                                                                                                                                                                                 | Optional | Degradation                                                                            |\n| -------------------- | ------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------- |\n| `dataConnections`    | GET    | `/__studio/data/connections`      | Connector connections with configured/missingSecrets/isDefault state, reachable table names, and registry-descriptor provider metadata (DataConnectionsResponse; backs dataConnections) | yes      | The connections settings section shows no status, and data-domain actions stay hidden. |\n| `dataConnectionTest` | POST   | `/__studio/data/connections/test` | Probe a connection {connection} → DataConnectionTestResult (backs dataConnectionTest)                                                                                                   | yes      | The Test Connection action is hidden.                                                  |\n| `dataPush`           | POST   | `/__studio/data/push`             | Additive schema push {connection?, dryRun?} → DataPushResult (plan of DataPushStep[], applied, warnings/errors; backs dataPush)                                                         | yes      | The Push Schema action is hidden; schemas deploy via `jx db push` instead.             |\n| `dataRows`           | GET    | `/__studio/data/rows`             | Page a table (DataRowsQuery params) → DataRowsResult {rows, total, columns} (backs dataRows)                                                                                            | yes      | The data grid is unavailable.                                                          |\n| `dataInsertRow`      | POST   | `/__studio/data/rows`             | Insert a row {table, connection?, values} → {row} (backs dataInsertRow)                                                                                                                 | yes      | The data grid hides its add-row footer.                                                |\n| `dataUpdateRow`      | PUT    | `/__studio/data/rows`             | Update a row keyed on its primary key {table, connection?, pk, set} → {row} (backs dataUpdateRow)                                                                                       | yes      | Data grid cells are read-only.                                                         |\n| `dataDeleteRow`      | DELETE | `/__studio/data/rows`             | Delete a row (?table=&pk=&connection=) → {ok} (backs dataDeleteRow)                                                                                                                     | yes      | The data grid hides row deletion.                                                      |\n\n## Secrets (names only)\n\n| Route         | Method | Path                | Summary                                                                                                  | Optional | Degradation                                                                        |\n| ------------- | ------ | ------------------- | -------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- |\n| `secretsList` | GET    | `/__studio/secrets` | Configured secret env-var NAMES, never values (SecretsListResponse; backs listSecrets)                   | yes      | Secret fields cannot show set/unset state.                                         |\n| `secretsSet`  | PUT    | `/__studio/secrets` | Write/remove secrets in the backend store (.dev.vars locally) {set?, remove?} → names (backs setSecrets) | yes      | The secret form control renders disabled; secrets are edited in .dev.vars by hand. |\n\n## AI proxy\n\n| Route      | Method | Path                  | Summary                                                           | Optional | Degradation |\n| ---------- | ------ | --------------------- | ----------------------------------------------------------------- | -------- | ----------- |\n| `aiChat`   | POST   | `/__studio/ai/chat`   | StreamEvent SSE chat proxy {messages, tools, systemPrompt, model} | no       | —           |\n| `aiModels` | GET    | `/__studio/ai/models` | Model catalogue (AiModelsResponse)                                | no       | —           |\n\n## Cloudflare publish surface\n\n| Route     | Method | Path                 | Summary                                                                     | Optional | Degradation                                                      |\n| --------- | ------ | -------------------- | --------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------- |\n| `cfProxy` | POST   | `/__studio/cf/proxy` | Allowlisted Cloudflare API passthrough {path, method?, body?} (backs cfApi) | yes      | The Publish panel explains the git-push publishing path instead. |\n",
    "section": "Extending",
    "slug": "extending/reference/studio-routes",
    "title": "Protocol route reference",
    "url": "https://jxsuite.com/docs/extending/reference/studio-routes/"
  },
  {
    "description": "Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Implementation status\n\nThis page is generated from the `> **Status: …**` markers in the specifications. It lists every spec and the sections that are not yet fully `Implemented`. Regenerate with `bun run docs:generate`.\n\n## Specs at a glance\n\n| Spec                      | Version      | Status      | Updated    |\n| ------------------------- | ------------ | ----------- | ---------- |\n| `ai.md`                   | 0.1.2-draft  | Partial     | 2026-07-25 |\n| `collab.md`               | 0.1.1-draft  | Partial     | 2026-07-22 |\n| `compiler.md`             | 0.1.23-draft | Partial     | 2026-07-24 |\n| `desktop.md`              | 0.3.1-draft  | Pending     | 2026-07-25 |\n| `extensions.md`           | 0.3.3-draft  | Partial     | 2026-07-25 |\n| `imports.md`              | 0.1.6-draft  | Partial     | 2026-07-22 |\n| `jx-markdown.md`          | 0.1.7-draft  | Partial     | 2026-07-22 |\n| `parser.md`               | 0.2.4-draft  | Partial     | 2026-07-23 |\n| `relationships.md`        | 0.1.3-draft  | Partial     | 2026-07-22 |\n| `schema.md`               | 0.2.8-draft  | Partial     | 2026-07-22 |\n| `server.md`               | 0.2.1        | Implemented | 2026-07-25 |\n| `site-architecture.md`    | 0.1.37-draft | Pending     | 2026-07-24 |\n| `spec.md`                 | 0.4.24-draft | Partial     | 2026-07-24 |\n| `studio-ui-guidelines.md` | 0.2.0        | Implemented | 2026-07-26 |\n| `studio.md`               | 0.3.0-draft  | Partial     | 2026-07-27 |\n\n## Sections not yet implemented\n\n### Partial\n\n- `spec.md` §5.6 — Private State (`#` prefix)\n- `spec.md` §11.4 — Server Timing — RPC Function Boundary\n- `spec.md` §16.8 — CEM-Compatible Annotations\n\n### Pending\n\n- `desktop.md` §4.3 — Single File Mode\n- `site-architecture.md` §7.4 — Content Entry Editor\n- `site-architecture.md` §8.6 — Studio SEO Panel\n- `site-architecture.md` §9.4 — Studio Media Browser\n- `site-architecture.md` §11.4 — Studio Redirect Editor\n\n### Future\n\n- `desktop.md` §9.3 — Nix Package\n\n### Removed\n\n- `spec.md` §13.1 — Component Instances\n",
    "section": "Extending",
    "slug": "extending/reference/implementation-status",
    "title": "Implementation status",
    "url": "https://jxsuite.com/docs/extending/reference/implementation-status/"
  },
  {
    "description": "Release history for every specification, generated from each spec's changelog section: version, date, and what changed.",
    "markdown": "\n<!-- GENERATED by scripts/docs/generate-reference.ts — do not edit; run `bun run docs:generate`. -->\n\n# Spec changelog\n\nThis page is generated from the `## Changelog` section of each file in `/specs`. Every substantive spec edit is a release: the version advances, the `**Updated:**` date is restamped, and an entry lands here. Record one with `bun run spec:bump <spec.md> <major|minor|patch> -m \"<what changed>\"`.\n\nVersions are `MAJOR.MINOR.PATCH` — **major** for a breaking change to a documented contract, **minor** for additive behavior, **patch** for editorial changes. A `-draft` suffix marks a spec whose status is not yet `Implemented`.\n\n## `ai.md`\n\n- **0.1.2-draft** (2026-07-25) — Schema gate (§3.1): tool-level validation against the active project's entry documents, before-write for disk writes and after-apply on canvas, project.json included.\n- **0.1.1-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.0-draft** (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface (`c8d1d580`).\n\n## `collab.md`\n\n- **0.1.1-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.0-draft** (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface (`c8d1d580`).\n\n## `compiler.md`\n\n- **0.1.23-draft** (2026-07-24) — §1 Overview: condition the generated Hono worker on build.adapter (per-page _server.js without one) and scope the static-build failure to active data/auth mounts; §6.3 document compileSiteServer's mounts/connectors parameters and extension mount emission.\n- **0.1.22-draft** (2026-07-23) — Image src resolution consults extension asset mounts before public/ (§7.3).\n- **0.1.21-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.20-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.19-draft** (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script (`e629684d`).\n- **0.1.18-draft** (2026-07-17) — Bundle the site worker self-contained per adapter (`4096ba12`).\n- **0.1.17-draft** (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors (`07e28bc3`).\n- **0.1.16-draft** (2026-07-17) — Image pruning for persistent site build cache + github ci cache (`b45096ed`).\n- **0.1.15-draft** (2026-06-10) — Update site architecture to reflect new changes (`c0bdba08`).\n- **0.1.14-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.13-draft** (2026-06-03) — Use `.cache` isntead of `.jx-cache` to support cloudflare build cache (`1103d2d6`).\n- **0.1.12-draft** (2026-06-01) — Stronger typing (`fcbb5b5d`).\n- **0.1.11-draft** (2026-06-01) — Convert to typescript (`e352e265`).\n- **0.1.10-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.1.9-draft** (2026-05-18) — Always emit worker.js for cloudflare (`3dd37c2d`).\n- **0.1.8-draft** (2026-05-15) — Image optimization specs (`7d2ee67f`).\n- **0.1.7-draft** (2026-05-15) — Provider-sepcific Site-Wide Bundling (`51cb5cf6`).\n- **0.1.6-draft** (2026-04-23) — Rebrand to jxsuite (`2897a4e8`).\n- **0.1.5-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.4-draft** (2026-04-20) — Text nodes support (`4d45eeb7`).\n- **0.1.3-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.2-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.1-draft** (2026-04-10) — Finalize vision for site architecture (`da594993`).\n- **0.1.0-draft** (2026-04-10) — Consolidate specs (`80ca313f`).\n\n## `desktop.md`\n\n- **0.3.1-draft** (2026-07-25) — Repo picker gains a repository-access footer: per-installation manage links, install-on-another-account, and Refresh.\n- **0.3.0-draft** (2026-07-25) — New Project requires a user-chosen destination: StudioPlatform gains the required createDestination declaration, createProject takes a required destination (path parent or repo owner/name/visibility), and §4.5 defines the create flow. No backend picks a location.\n- **0.2.7-draft** (2026-07-24) — Cloud platform registers inside studio.js via a window.__jxCloud signal (single yjs for collab).\n- **0.2.6-draft** (2026-07-24) — Document packaged static-data staging into app/bun (create templates, starters) and postBuild bundle verification.\n- **0.2.5-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.2.4-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.2.3-draft** (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality (`c61ba567`).\n- **0.2.2-draft** (2026-07-13) — Run formatter (`9e776783`).\n- **0.2.1-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.2.0-draft** (2026-05-17) — Remove auto-detection script and update documentation for NixOS desktop runtime (`6b746644`).\n- **0.1.6-draft** (2026-05-16) — Update desktop spec (`9453ea1f`).\n- **0.1.5-draft** (2026-04-23) — Oxfmt (`af32c08c`).\n- **0.1.4-draft** (2026-04-23) — Rebrand to jxsuite (`2897a4e8`).\n- **0.1.3-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.2-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.1-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.0-draft** (2026-04-15) — Implement platform abstraction (`962ba588`).\n\n## `extensions.md`\n\n- **0.3.3-draft** (2026-07-25) — Composition is host-agnostic: one pure function with an injected loader, so the cloud session composes the same entry documents in-Worker with no filesystem (§5.5).\n- **0.3.2-draft** (2026-07-25) — $schema bindings must be satisfied by by-id registration, never fetching — an in-document $schema overrides fileMatch and an unresolvable one voids validation entirely (§5.4).\n- **0.3.1-draft** (2026-07-25) — $paths validates against the source union instead of accepting any object (§5.3).\n- **0.3.0-draft** (2026-07-25) — Committed entry documents are single-resource: every $ref a root pointer (§5.2, §5.4).\n- **0.2.9-draft** (2026-07-24) — §2 package layout: correct the auth package description, note that /_jx/auth serves the Better Auth routes while table permission rules are enforced at /_jx/data, and add the missing search extension to the tree.\n- **0.2.8-draft** (2026-07-23) — Add the assets capability (§8.5): section owners publish source directories at site URLs.\n- **0.2.7-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.2.6-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.2.5-draft** (2026-07-22) — Align specs and docs with the bundled-schema validation contract (`ae861ff6`).\n- **0.2.4-draft** (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors (`07e28bc3`).\n- **0.2.3-draft** (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality (`c61ba567`).\n- **0.2.2-draft** (2026-07-11) — Better Auth extension — sessions, permissions, auth-gated data (`bf472285`).\n- **0.2.1-draft** (2026-07-08) — Shipped schema fragments + per-project schema emitters (`9e4a8936`).\n- **0.2.0-draft** (2026-07-08) — Extensions v2 framework + docs (`3fb8795f`).\n- **0.1.0-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n\n## `imports.md`\n\n- **0.1.6-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.5-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.4-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.3-draft** (2026-06-01) — Remove old glob-based content type references (`6bcbfdaf`).\n- **0.1.2-draft** (2026-05-19) — Reflect new content type transition (`6eb3d2b6`).\n- **0.1.1-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.0-draft** (2026-04-22) — External web component support (`a9d0fbe4`).\n\n## `jx-markdown.md`\n\n- **0.1.7-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.6-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.5-draft** (2026-07-17) — Build-time syntax highlighting for markdown code fences (`b2e7a561`).\n- **0.1.4-draft** (2026-06-15) — Arrays as pseudo-element (`0b8b3070`).\n- **0.1.3-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.2-draft** (2026-05-25) — Element annotations (title/description) (`c9137e50`).\n- **0.1.1-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.1.0-draft** (2026-05-11) — Jx markdown (`7b102340`).\n\n## `parser.md`\n\n- **0.2.4-draft** (2026-07-23) — Document the Content project-section class: asset mounts and content-relative reference rewriting (§9).\n- **0.2.3-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.2.2-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.2.1-draft** (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors (`07e28bc3`).\n- **0.2.0-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.6-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.1.5-draft** (2026-05-08) — Pass markdown attributes as properties (`407b70fc`).\n- **0.1.4-draft** (2026-04-23) — Rebrand to jxsuite (`2897a4e8`).\n- **0.1.3-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.2-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.1-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.0-draft** (2026-04-10) — Consolidate specs (`80ca313f`).\n\n## `relationships.md`\n\n- **0.1.3-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.2-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.1-draft** (2026-07-08) — Shipped schema fragments + per-project schema emitters (`9e4a8936`).\n- **0.1.0-draft** (2026-07-08) — Extensions v2 framework + docs (`3fb8795f`).\n\n## `schema.md`\n\n- **0.2.8-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.2.7-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.2.6-draft** (2026-07-22) — Align specs and docs with the bundled-schema validation contract (`ae861ff6`).\n- **0.2.5-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.2.4-draft** (2026-05-18) — Always emit worker.js for cloudflare (`3dd37c2d`).\n- **0.2.3-draft** (2026-05-15) — Provider-sepcific Site-Wide Bundling (`51cb5cf6`).\n- **0.2.2-draft** (2026-04-23) — Site build (`ffe60ddc`).\n- **0.2.1-draft** (2026-04-23) — Compiler cli + published site (`4607ebbc`).\n- **0.2.0-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.3-draft** (2026-04-20) — Text nodes support (`4d45eeb7`).\n- **0.1.2-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.1-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.0-draft** (2026-04-10) — Consolidate specs (`80ca313f`).\n\n## `server.md`\n\n- **0.2.1** (2026-07-25) — Activation admits an existing project of the account's own (project.json under the home directory); a refused activation must surface as an error rather than fall back to the server root.\n- **0.2.0** (2026-07-25) — POST /__studio/create-project requires an explicit absolute destination parent — the server no longer falls back to its own root. Adds assertCreatableParent (root, allowedRoots, or home; absolute only), remembers created roots for a following activate, and returns an absolute root for projects outside the server root.\n- **0.1.9** (2026-07-23) — Serve extension asset mounts ahead of the project root in the static-file chain (§3).\n- **0.1.8** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.7** (2026-07-22) — Fix failing tests (`56e073f8`).\n- **0.1.6** (2026-07-22) — Harden dev server and unify runtime/compiler evaluation (`47a1d4c9`).\n- **0.1.5** (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality (`c61ba567`).\n- **0.1.4** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.3** (2026-04-23) — Rebrand to jxsuite (`2897a4e8`).\n- **0.1.2** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.1** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.0** (2026-04-10) — Consolidate specs (`80ca313f`).\n\n## `site-architecture.md`\n\n- **0.1.37-draft** (2026-07-24) — Document the application tier and correct the static-only framing: §1 vision, §1.1 principles 1/3/5, §1.2 coverage, §14.1/§14.2 adapter output (worker generation is gated on build.adapter alone), and new §15 Application Tier covering server functions, auth, and data mounts.\n- **0.1.36-draft** (2026-07-23) — Note the mounted-asset copy step in the build pipeline (§12.1).\n- **0.1.35-draft** (2026-07-23) — Content entries address media relative to themselves; collections publish their directory at /content/<type> (§9.3).\n- **0.1.34-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.33-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.32-draft** (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script (`e629684d`).\n- **0.1.31-draft** (2026-07-17) — Bundle the site worker self-contained per adapter (`4096ba12`).\n- **0.1.30-draft** (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors (`07e28bc3`).\n- **0.1.29-draft** (2026-07-17) — Image pruning for persistent site build cache + github ci cache (`b45096ed`).\n- **0.1.28-draft** (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality (`c61ba567`).\n- **0.1.27-draft** (2026-06-25) — Sitemap generation (`948c7a67`).\n- **0.1.26-draft** (2026-06-10) — Update site architecture to reflect new changes (`c0bdba08`).\n- **0.1.25-draft** (2026-06-10) — Use cloudflare cgi for image optimization (`96228874`).\n- **0.1.24-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.23-draft** (2026-06-03) — Use `.cache` isntead of `.jx-cache` to support cloudflare build cache (`1103d2d6`).\n- **0.1.22-draft** (2026-06-01) — Remove old glob-based content type references (`6bcbfdaf`).\n- **0.1.21-draft** (2026-05-28) — Separate directory + format for content type defs (`c43186ac`).\n- **0.1.20-draft** (2026-05-25) — Element annotations (title/description) (`c9137e50`).\n- **0.1.19-draft** (2026-05-25) — Allow nested global styles (`1159d585`).\n- **0.1.18-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.1.17-draft** (2026-05-19) — Reflect new content type transition (`6eb3d2b6`).\n- **0.1.16-draft** (2026-05-18) — Always emit worker.js for cloudflare (`3dd37c2d`).\n- **0.1.15-draft** (2026-05-18) — Remove unused 'rendered' property from JSON and CSV entries (`7478a87c`).\n- **0.1.14-draft** (2026-05-15) — Image optimization specs (`7d2ee67f`).\n- **0.1.13-draft** (2026-05-15) — Provider-sepcific Site-Wide Bundling (`51cb5cf6`).\n- **0.1.12-draft** (2026-05-04) — Longhand/shorthand property input sync (`05c7da35`).\n- **0.1.11-draft** (2026-04-29) — Update site architecture progress (`3305a8f0`).\n- **0.1.10-draft** (2026-04-29) — Project browser (`11c1fe7c`).\n- **0.1.9-draft** (2026-04-23) — Site build (`ffe60ddc`).\n- **0.1.8-draft** (2026-04-23) — Compiler cli + published site (`4607ebbc`).\n- **0.1.7-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.6-draft** (2026-04-20) — Better project-level scoping (`0cba233c`).\n- **0.1.5-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.4-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.3-draft** (2026-04-15) — Importmap support (`c1b329d4`).\n- **0.1.2-draft** (2026-04-10) — WinterTC server-side conventions (`60eba6dd`).\n- **0.1.1-draft** (2026-04-10) — Site architecture update (`86d1c515`).\n- **0.1.0-draft** (2026-04-10) — Enhanced font picker (`9d388a32`).\n\n## `spec.md`\n\n- **0.4.24-draft** (2026-07-24) — §5.3 and §11.4: a timing: \"server\" route lands in the generated site worker only when build.adapter is set; without an adapter the compiler emits a per-page _server.js handler instead.\n- **0.4.23-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.4.22-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.4.21-draft** (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface (`c8d1d580`).\n- **0.4.20-draft** (2026-07-22) — Align specs and docs with the bundled-schema validation contract (`ae861ff6`).\n- **0.4.19-draft** (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script (`e629684d`).\n- **0.4.18-draft** (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors (`07e28bc3`).\n- **0.4.17-draft** (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality (`c61ba567`).\n- **0.4.16-draft** (2026-07-17) — Clean up spec (`897e8c1e`).\n- **0.4.15-draft** (2026-07-15) — Pure method operators and the composite formula catalog (spec §19.4d) (`58be3b1a`).\n- **0.4.14-draft** (2026-07-15) — Conditional operators and editor evaluation trace (spec §19.4b, §19.9) (`79926245`).\n- **0.4.13-draft** (2026-06-15) — Arrays as pseudo-element (`0b8b3070`).\n- **0.4.12-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.4.11-draft** (2026-06-02) — Returns type in the class definition (`32e4737a`).\n- **0.4.10-draft** (2026-06-01) — Declarative expressions (`472aeb15`).\n- **0.4.9-draft** (2026-05-25) — Element annotations (title/description) (`c9137e50`).\n- **0.4.8-draft** (2026-05-20) — \"format\" on fields for image fields (`02f87d29`).\n- **0.4.7-draft** (2026-05-20) — Release 0.11.0 (`4a0e17ed`).\n- **0.4.6-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.4.5-draft** (2026-05-18) — Always emit worker.js for cloudflare (`3dd37c2d`).\n- **0.4.4-draft** (2026-05-15) — Add markdown prototypes at top-level (`24020906`).\n- **0.4.3-draft** (2026-05-15) — Provider-sepcific Site-Wide Bundling (`51cb5cf6`).\n- **0.4.2-draft** (2026-04-23) — Oxfmt (`af32c08c`).\n- **0.4.1-draft** (2026-04-23) — Rebrand to jxsuite (`2897a4e8`).\n- **0.4.0-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.3.7-draft** (2026-04-22) — External web component support (`a9d0fbe4`).\n- **0.3.6-draft** (2026-04-22) — Init new site (`f33d319b`).\n- **0.3.5-draft** (2026-04-20) — Text nodes support (`4d45eeb7`).\n- **0.3.4-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.3.3-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.3.2-draft** (2026-04-15) — Importmap support (`c1b329d4`).\n- **0.3.1-draft** (2026-04-15) — Require json based entrypoints for external class integration (`86dc4383`).\n- **0.3.0-draft** (2026-04-10) — Consolidate specs (`80ca313f`).\n- **0.2.3-draft** (2026-04-07) — Custom element spec (`4f377be3`).\n- **0.2.2-draft** (2026-04-06) — Server-side timing (scaffolding) (`23932590`).\n- **0.2.1-draft** (2026-04-06) — Transition to vue reactivity (`70cb8445`).\n- **0.2.0-draft** (2026-04-06) — Pure js in defs and strings (`6494999d`).\n- **0.1.3-draft** (2026-04-06) — External/md parser (`5161ec0e`).\n- **0.1.2-draft** (2026-04-04) — Declarative media breakpoints (`3142b64e`).\n- **0.1.1-draft** (2026-04-04) — Rebrand as JSONsx (`0daa94f7`).\n- **0.1.0-draft** (2026-04-04) — Init (`a93852ac`).\n\n## `studio-ui-guidelines.md`\n\n- **0.2.0** (2026-07-26) — Canvas caret replaces the inline-edit session (§8.3); click selects and places the caret (§8.1); canvas drags start only from the bar handle (§8.2); single-shape action bar (§8.6).\n- **0.1.8** (2026-07-26) — Modal surfaces own the keyboard: showDialog focus handoff, Escape dismissal, and the isModalOpen() shortcut gate (§8.7).\n- **0.1.7** (2026-07-26) — Dialogs and overlay layers (§8.7): the ui/layers.ts contract, showPromptDialog as the replacement for window.prompt(), and a ban on native browser dialogs.\n- **0.1.6** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.5** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.4** (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control (`ccdc1d3e`).\n- **0.1.3** (2026-06-01) — Convert to typescript (`e352e265`).\n- **0.1.2** (2026-04-22) — External web component support (`a9d0fbe4`).\n- **0.1.1** (2026-04-22) — Init new site (`f33d319b`).\n- **0.1.0** (2026-04-18) — Ui guidelines (`91f2b29e`).\n\n## `studio.md`\n\n- **0.3.0-draft** (2026-07-27) — Derive the caret's editable tag set from the document's element vocabulary (§8.2.2): the format class decides per tag and can say no, so a Markdown blockquote holds paragraphs and a link is markup within a block; subsections after it renumber (nothing referenced them).\n- **0.2.0-draft** (2026-07-26) — Fluid document editing: the canvas carries a live caret (§8.2), one block action bar (§4.4), both editable modes behave identically for text (§4.2), and a rewritten keyboard contract (§10).\n- **0.1.29-draft** (2026-07-26) — File create/rename/delete naming dialogs (§9.1.1); branch, clone, and nested-selector flows now open Spectrum dialogs instead of native prompts.\n- **0.1.28-draft** (2026-07-25) — The Cloud platform target composes per-project schemas server-side (§3.4).\n- **0.1.27-draft** (2026-07-25) — Source-mode schema validation contract: per-project entry documents, offline $schema-id registration, worker self-location (§4.2.1); fetchProjectSchemas in the PAL table (§3.4).\n- **0.1.26-draft** (2026-07-25) — PAL table records the destination members: createDestination, createProject's user-chosen destination, and pickDirectory.\n- **0.1.25-draft** (2026-07-22) — Proper spec versioning (`fb0f3ec7`).\n- **0.1.24-draft** (2026-07-22) — Machine-readable spec status vocabulary + generated status page (`79daba23`).\n- **0.1.23-draft** (2026-07-17) — Scheme-variant editing — token overrides, scheme-layer routing, live feedback (`49f0c525`).\n- **0.1.22-draft** (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control (`ccdc1d3e`).\n- **0.1.21-draft** (2026-07-17) — Consolidated field mode switcher (`0a135ed1`).\n- **0.1.20-draft** (2026-06-10) — Consolidate markdown and csv handling to the parser package (`8b1ba6da`).\n- **0.1.19-draft** (2026-05-25) — Allow nested global styles (`1159d585`).\n- **0.1.18-draft** (2026-05-20) — \"format\" on fields for image fields (`02f87d29`).\n- **0.1.17-draft** (2026-05-20) — Run formatter (`8ba47930`).\n- **0.1.16-draft** (2026-05-15) — Git sidebar (`79663844`).\n- **0.1.15-draft** (2026-04-23) — Include global styling (`d8d25640`).\n- **0.1.14-draft** (2026-04-23) — Site build (`ffe60ddc`).\n- **0.1.13-draft** (2026-04-23) — Compiler cli + published site (`4607ebbc`).\n- **0.1.12-draft** (2026-04-22) — Consolidate project config schema and rename as such (`e3523dbf`).\n- **0.1.11-draft** (2026-04-22) — External web component support (`a9d0fbe4`).\n- **0.1.10-draft** (2026-04-22) — Init new site (`f33d319b`).\n- **0.1.9-draft** (2026-04-20) — Text nodes support (`4d45eeb7`).\n- **0.1.8-draft** (2026-04-20) — Better project-level scoping (`0cba233c`).\n- **0.1.7-draft** (2026-04-18) — Dedicated combo/picker component for style preview (`d8d07921`).\n- **0.1.6-draft** (2026-04-18) — Fix the test path handling on windows (`26ea0d70`).\n- **0.1.5-draft** (2026-04-17) — Update studio specs (`d0e5475a`).\n- **0.1.4-draft** (2026-04-17) — Reorganize code tree (`d5ee04c4`).\n- **0.1.3-draft** (2026-04-16) — Landing site + working exports + release-it + linting (`a8409b5f`).\n- **0.1.2-draft** (2026-04-15) — Rebrand to Jx / Jx Platform (`abc63f2d`).\n- **0.1.1-draft** (2026-04-10) — Finalize vision for site architecture (`da594993`).\n- **0.1.0-draft** (2026-04-10) — Consolidate specs (`80ca313f`).\n",
    "section": "Extending",
    "slug": "extending/reference/spec-changelog",
    "title": "Spec changelog",
    "url": "https://jxsuite.com/docs/extending/reference/spec-changelog/"
  },
  {
    "description": "Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.",
    "markdown": "\n# Working in the monorepo\n\nThe Jx monorepo ([github.com/jxsuite/jx](https://github.com/jxsuite/jx)) is a Bun workspace.\n\n## Layout\n\n- `packages/` — the `@jxsuite/*` core packages: runtime, compiler, schema, server, studio, desktop, protocol, formulas, collab, ai, markup, import, starters, create.\n- `extensions/` — extension packages built on the public hooks: parser (Markdown/CSV formats and content), connector (databases), auth.\n- `specs/` — the numbered specifications. These are the living source of truth: consult and update them **before** implementing a feature.\n- `sites/` — real sites built with Jx, including jxsuite.com.\n- `docs/` — this documentation (see [Contributing to these docs](/docs/extending/contributing/docs/)).\n\n## Everyday commands\n\n- `bun install` — set up the workspace.\n- `bun run dev` — start the dev server and open Studio in a browser.\n- `bun test --isolate` (per package) — the supported test mode; plain `bun test` has known order-dependent failures.\n- `bun run typecheck`, `bun run lint`, `bun run format` — tsgo, oxlint (all categories at error), oxfmt.\n\n## Testing policy\n\nEvery package keeps full unit-test coverage, enforced per file by each package's `bunfig.toml` thresholds plus a manifest check that fails CI when a source file is never imported by any test. New source files ship with tests in the same PR.\n",
    "section": "Extending",
    "slug": "extending/contributing/monorepo",
    "title": "Working in the monorepo",
    "url": "https://jxsuite.com/docs/extending/contributing/monorepo/"
  },
  {
    "description": "How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.",
    "markdown": "\n# Contributing to these docs\n\nThe documentation lives in `/docs` at the monorepo root, published through the jxsuite.com site. Every page is a Markdown file with YAML frontmatter; the folder path is the URL (`docs/studio/editing.md` → `/docs/studio/editing/`).\n\n## Adding a page\n\n1. Create the Markdown file under the right section (`start/`, `studio/`, `framework/`, `extending/`).\n2. Add frontmatter: `title` (sentence case, no site suffix) and `description` (≤155 characters).\n3. Add one line to `docs/nav.json` — the sidebar is generated from it, and CI fails if a page is missing from nav (or nav points at a missing page).\n4. Run `bun run docs:check` before pushing.\n\nOptional frontmatter associates a page with its sources, validated by CI:\n\n```yaml\nspec:\n  - spec.md#19.4 # a specs/ file and numbered section\ncode:\n  - packages/runtime/src/runtime.ts # repo paths that must exist\n```\n\nIn the other direction, code comments may carry `@docs <slug>` tags (e.g. `@docs framework/concepts/reactivity`) pointing at the page that documents them — also validated.\n\nThese associations also power `bun run docs:sync`: given your working diff, it lists the pages and spec sections tied to the source files you changed. It runs automatically as a pre-commit advisory (and as an agent stop-check), so behavior changes and their documentation land together. It never blocks — a pure refactor needs no doc update.\n\nYour page is also published for machines. After `jx build`, the site build runs `scripts/docs/build-llm-export.ts`, which regenerates `llms.txt` and `/docs/full-docs.json` from the same frontmatter and Markdown — the whole corpus, in nav order, served verbatim to whatever fetches it. In `llms.txt` each page is one line — `- [Title](url): description` — so your `description` is nearly everything an agent has before deciding whether to open the page. Make it say what the page covers rather than restate the title. See [Machine-readable docs](/docs/framework/agents/machine-readable).\n\n## Voice and style\n\n- Second person, present tense, imperative steps: \"Click **New Project**\", not \"The New Project button can be clicked\".\n- Sentence-case headings; one H1 matching the frontmatter title.\n- Studio and Start pages assume no code knowledge. Code belongs in Framework/Extending pages — or inside a `:::doc-note` aside.\n- Bold for clickable UI labels (**Commit & Sync**); `:kbd[Ctrl+K]` for keys (give macOS and Windows/Linux pairs on first mention); backticks only for literal code, filenames, and JSON keys.\n- Chevron click paths for navigation chains: _Settings > CSS Variables_.\n- American English, short paragraphs, no marketing superlatives.\n\n## Canonical UI names\n\nNever invent synonyms for Studio surfaces. The activities are **Files, Layers, Imports, Elements, State, Data, Document, Source Control**; the canvas modes are **Edit, Design, Grid, Code, Stylebook** plus the **Preview** toggle; the right-panel tabs are **Properties, Events, Style**.\n\n## Callouts\n\nThree container directives render as styled asides:\n\n```markdown\n:::doc-note\nNeutral context — including \"behind the scenes\" notes naming what Studio writes to disk.\n:::\n\n:::doc-tip\nShortcuts and good practices.\n:::\n\n:::doc-warning\nData loss or surprising behavior only.\n:::\n```\n\nUse at most a couple per screenful, and never open a page with one.\n\n## Page shapes\n\n- **Studio surface page**: definition sentence (what it is, where it lives) → hero screenshot → \"Open …\" click path first → verb-first task sections with numbered steps and a screenshot after each state-changing step → a `:::doc-note` naming what Studio writes, linking the Framework counterpart → related links.\n- **Framework concept page**: a \"Studio writes this format for you\" note linking the Studio surface → smallest complete JSON example first → one H2 per variant with a short example each → how it compiles → hard rules → related links.\n- **Tutorial**: outcome + finished screenshot + rough duration + prerequisites → numbered steps with expected-result sentences (\"You should now see…\") → \"What you built\" recap → next steps.\n- **Generated reference**: do not edit these — they carry a `GENERATED` banner and are produced by `bun run docs:generate` from package data, the specs' status markers, and the specs' changelogs; CI fails on drift. Releasing a spec (`bun run spec:bump`) changes [Implementation status](/docs/extending/reference/implementation-status) and [Spec changelog](/docs/extending/reference/spec-changelog), so regenerate in the same change set.\n\n## Internal links\n\nWrite them as root-absolute slugs — `/docs/framework/site/routing` — with no `.md` extension and no trailing slash. Never use a relative `../foo.md` link: the site serves the target verbatim rather than rewriting it to a URL, so the link is broken the moment it publishes.\n\n:::doc-warning\n**No gate checks internal links.** `docs:check` validates frontmatter, spec anchors, `code:` paths, images, and the nav bijection — nothing resolves a link target. A typo, or a renamed page, ships silently. Verify each slug against `docs/nav.json` as you write it, and grep for the old slug whenever you rename a page.\n:::\n\n## Screenshots\n\nAll screenshots come from the automated pipeline — none are hand-taken, so every image can be regenerated when the UI changes:\n\n1. Declare the shot in `scripts/screenshots/manifest.json` (project, file, actions, regions). Give it a `docs` field listing the page slugs it illustrates.\n2. Run `bun run screenshots` — output lands in `docs/images/` and is committed.\n3. Reference it **relative to your page**, e.g. `![descriptive alt text](../images/<name>.png)` from `docs/start/`, `../../images/<name>.png` one level deeper.\n\nRelative paths are what make `/docs` readable in any markdown editor — the images travel with the pages. The site build republishes them under `/content/docs/images/` (the `docs` collection's [asset mount](/docs/framework/site/content-collections)), which is also how a site page outside `/docs` references one.\n\nAlt text is mandatory and describes the state shown (\"Style inspector with the Typography section expanded\"), not the filename. Shots drive the starter sites (real-estate by default, dark theme) so docs show real projects, not Jx internals. CI verifies every referenced image resolves into `docs/images/`, is produced by the manifest, and exists on disk.\n",
    "section": "Extending",
    "slug": "extending/contributing/docs",
    "title": "Contributing to these docs",
    "url": "https://jxsuite.com/docs/extending/contributing/docs/"
  },
  {
    "description": "Run the AI assistant eval suite, read its transcripts, and use the render critic and regression gate when you change the assistant's scaffolding.",
    "markdown": "\n# AI assistant evals\n\nStudio's [AI assistant](/docs/studio/ai) is not one thing you can unit-test. It is a model you do not control wrapped in scaffolding you do: a system prompt, a set of tool schemas, the translations that turn a validation error back into an instruction the model can act on. Change any of that and the only honest question is whether the assistant got better — which a test suite cannot answer and a hunch should not.\n\n`packages/studio/evals/` is the harness that answers it. Reach for it whenever you touch `ai-system-prompt.ts`, `ai-tools.ts`, or the few-shot examples.\n\n## The invariant\n\n**The model is held fixed; only the scaffolding is iterated.** That is the whole design. Because the model, the tasks, and the graders stay constant between runs, a difference in pass-rate is attributable to the diff you just made. Change two things at once — the prompt and the model, say — and the run tells you nothing.\n\nThe second half of the design is that the harness drives the **real** production loop: the actual `runAgentLoop`, the actual `@jxsuite/ai` tool registry, the actual system prompt built by `buildSystemPrompt`. The only substitution is the LLM client, where the fake test client is swapped for a real OpenAI-compatible one. There is no parallel \"eval mode\" implementation to keep in sync, so a scaffolding change is exercised exactly as users will meet it.\n\n## Golden tasks\n\nEach task in `evals/tasks/*.json` is one isolated, unambiguous specification:\n\n```json\n{\n  \"id\": \"counter-button\",\n  \"prompt\": \"Add a button that increments a counter, and show the current count above it. Wire up the reactive state.\",\n  \"tags\": [\"intent\", \"state\", \"binding\"],\n  \"intent\": [\n    \"declares numeric state for the count\",\n    \"a button increments the count on click\",\n    \"the count is shown via a ${...} template binding\"\n  ],\n  \"initialDoc\": {\n    \"tagName\": \"counter-widget\",\n    \"state\": { \"count\": { \"type\": \"integer\", \"default\": 0 } },\n    \"children\": []\n  }\n}\n```\n\n`initialDoc` is the document the assistant starts from — every trial gets a fresh tab, so nothing leaks between runs. `intent` records the human success criteria: it is what you read transcripts against today, and the hook for a future LLM-as-judge grader. Keep the suite stable. Tasks are the benchmark, and a benchmark that moves with the code cannot measure it.\n\n## Running it\n\n```bash\n# Whole suite, k=3 trials per task (the default)\nOPENAI_API_KEY=sk-… bun run eval\n\n# One task, single trial — a smoke check\nOPENAI_API_KEY=sk-… bun run eval --tasks counter-button --k 1\n```\n\n`OPENAI_BASE_URL` and `OPENAI_MODEL` (default `gpt-4o`) are optional and mirror how the server proxy resolves its config. Without `OPENAI_API_KEY` the CLI exits `2` immediately — the harness calls a real model by design.\n\nEach run writes a timestamped directory under `evals/runs/`, which is gitignored:\n\n- `report.md` — the human summary: mean pass-rate, pass@k and pass^k counts, and a per-task table with `Δrate` against the previous run.\n- `results.json` — the same numbers, machine-readable, minus the transcripts.\n- `transcripts/<task>-<trial>.md` — one file per trial: the round and tool-call counts, both graders' verdicts, the final document, and the full message transcript.\n\n**Read the transcripts.** A grader you have not watched is a grader you cannot trust, and the failure that matters is usually visible in the third assistant turn rather than in the summary row.\n\n## The two graders\n\n**Render critic — the primary signal.** It mounts the produced document with the real `@jxsuite/runtime` under happy-dom, the same render path the Studio canvas uses, and fails on anything the runtime surfaces: a thrown error during scope-building or node rendering, or a `console.error`/`console.warn` — unresolved `$ref`s, missing `$prototype` classes, broken bindings. Errors are phrased as actionable corrections (\"→ Fix: a template binding references state that doesn't exist\"), so the same output could later be fed back into the live loop.\n\n**Schema grader — the baseline.** The same `validateDoc()` the agent loop already self-corrects against. It is free and deterministic, so it is reported alongside for context.\n\nA trial passes on the **render critic**, not the schema grader. A document can be perfectly schema-valid and still render nothing useful, which is precisely the gap the critic exists to close.\n\n## pass@k and pass^k\n\nModels are non-deterministic, so a single trial per task measures luck. Each task runs `k` times (3 by default) and the scoreboard reports both:\n\n- **pass@k** — at least one of the `k` trials passed. Capability: can the assistant do this at all?\n- **pass^k** — all `k` trials passed. Reliability: can a user count on it?\n\n`passRate` (passes ÷ k) sits between them and is what the mean and the `Δrate` column are computed from.\n\n## The merge rule\n\n1. Run `bun run eval` and note the baseline mean pass-rate; collect the failing transcripts.\n2. Read the failures, then change **one** scaffolding file — the system prompt, the tool schemas plus `translateValidationError`, or the few-shot examples. One file per experiment keeps runs comparable.\n3. Re-run. **Keep the change only if the mean pass-rate improves and `regressed` is empty.** Otherwise discard it.\n\nThe scoreboard computes `regressed` by diffing against the most recent prior run: any task that passed pass@k before and does not now. The CLI exits `1` when that list is non-empty, so the same command can gate CI.\n\n:::doc-warning\nNever tune the tasks to make a scaffolding change look good. If a task is genuinely ambiguous, fix the task in its own commit and re-baseline — but treat that as a change to the benchmark, not a result.\n:::\n\n## The headless rubric harness\n\n`bun run eval:headless` runs a second harness at `packages/studio/tests/harness/`, which landed alongside the one above. It drives a catalog of prompts through the same production loop and scores them on rubric axes with evidence attached, running each test three times (`JX_AI_RUNS`) and reporting the **worst** run per axis so a borderline result cannot cherry-pick a lucky pass. It reads its key from `JX_AI_KEY`, falling back to `OPENAI_API_KEY`, and loads the repo-root `.env` regardless of the working directory. Use it for a qualitative read on assistant behavior; use `bun run eval` for the pass/fail number you gate on.\n\n## Out of scope\n\nThe harness deliberately does not do runtime UX sensors in the live assistant, LLM-as-judge grading, token accounting (the streaming client does not surface usage yet), or autonomous self-editing. The render critic's error format is LLM-ready on purpose, so a later phase can wire it into the live loop.\n\n## Related\n\n- [AI assistant](/docs/studio/ai) — what the harness is measuring, from the user's side\n- [Working in the monorepo](/docs/extending/contributing/monorepo) — the testing policy the rest of the repo follows\n- [Working with agents](/docs/framework/agents) — external agents on a Jx project, and the checks that hold them honest\n",
    "section": "Extending",
    "slug": "extending/contributing/ai-evals",
    "title": "AI assistant evals",
    "url": "https://jxsuite.com/docs/extending/contributing/ai-evals/"
  }
]