{"version":1,"engine":"minisearch","fields":["title","heading","text"],"boost":{"title":4,"heading":2},"documents":[{"id":"docs:extending","collection":"docs","slug":"extending","url":"/docs/extending/","title":"Extending","description":"Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.","heading":"","text":"Extending Jx 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. Extensions An 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 , then go deep on the individual contribution points: schema composition , classes , formats , capabilities , project sections , server mounts , connectors , and the security model . Two tutorials build one end-to-end — a TOML format and a guestbook with server routes and a connector — and the first-party extensions are the reference implementations to crib from. Embedding Studio Studio 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 maps the landscape and helps you pick an integration path; writing a platform adapter covers the in-page StudioPlatform interface; the backend protocol defines the contract any backend can serve, with every endpoint listed in the protocol route reference ; and dev server internals walks through the reference implementation. Contributing To work on Jx itself, working in the monorepo covers the repository layout, test and coverage policy, and the tooling conventions; contributing to these docs is the style guide every page in /docs follows."},{"id":"docs:extending#extending","collection":"docs","slug":"extending","url":"/docs/extending/#extending","title":"Extending","description":"Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.","heading":"Extending","text":"Jx 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."},{"id":"docs:extending#extensions","collection":"docs","slug":"extending","url":"/docs/extending/#extensions","title":"Extending","description":"Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.","heading":"Extensions","text":"An 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 , then go deep on the individual contribution points: schema composition , classes , formats , capabilities , project sections , server mounts , connectors , and the security model . Two tutorials build one end-to-end — a TOML format and a guestbook with server routes and a connector — and the first-party extensions are the reference implementations to crib from."},{"id":"docs:extending#embedding-studio","collection":"docs","slug":"extending","url":"/docs/extending/#embedding-studio","title":"Extending","description":"Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.","heading":"Embedding Studio","text":"Studio 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 maps the landscape and helps you pick an integration path; writing a platform adapter covers the in-page StudioPlatform interface; the backend protocol defines the contract any backend can serve, with every endpoint listed in the protocol route reference ; and dev server internals walks through the reference implementation."},{"id":"docs:extending#contributing","collection":"docs","slug":"extending","url":"/docs/extending/#contributing","title":"Extending","description":"Extend Jx with custom formats, classes, project sections, and server mounts — or embed Studio on your own backend.","heading":"Contributing","text":"To work on Jx itself, working in the monorepo covers the repository layout, test and coverage policy, and the tooling conventions; contributing to these docs is the style guide every page in /docs follows."},{"id":"docs:framework","collection":"docs","slug":"framework","url":"/docs/framework/","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"","text":"Framework This is the conceptual foundation beneath Jx Studio . You never have to read it to build with Studio — but if you want to know exactly what the format is, start here. Jx 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. Core Premise Structure and state are data. The shape of each state entry determines its type and behavior — no additional flags required in the common case. A Jx component is a single .json file that can be fully self-describing: { \"$id\" : \"Counter\" , \"state\" : { \"count\" : 0 , \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" } }, \"tagName\" : \"my-counter\" , \"children\" : [ { \"tagName\" : \"span\" , \"textContent\" : \"${state.count}\" }, { \"tagName\" : \"button\" , \"textContent\" : \"+\" , \"onclick\" : { \"$ref\" : \"#/state/increment\" } } ] } Design Principles DOM-First Design — Property names mirror standard DOM element properties. tagName , className , textContent , hidden , tabIndex all map directly to their DOM equivalents. 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. JSON as the Authoritative Format — Documents are valid JSON. Fully serializable. No this ambiguity. Natively understood by visual builders, IDEs, validators, and bundlers. Explicit Over Implicit — Signal scope does not leak across component boundaries. Every dependency must be explicitly declared as a $prop . Standards Alignment — Where a web platform standard exists, Jx follows it: JSON Schema 2020-12, JSON Pointer (RFC 6901), Web Components v1, CSSOM camelCase. Document Format Every Jx document has these top-level fields: Field Required Description $schema Recommended URI identifying the Jx dialect version $id Recommended Component identifier $defs Optional Pure JSON Schema type definitions (tooling only) state Optional Reactive state: signals, computed values, functions tagName Required HTML tag name for the root element children Optional Array of child element definitions and/or text nodes Reserved Keywords Keyword Purpose $schema Dialect identifier $id Component identifier $defs JSON Schema type definitions $ref Reference pointer (JSON Pointer, RFC 6901) $props Explicit prop passing at component boundary $prototype Constructor name — Web API, \"Function\" , or class $src External module specifier $switch Dynamic component switching $map Iteration context namespace $media Named media breakpoint declarations $elements Custom element dependency declarations timing Execution timing: \"compiler\" , \"server\" , \"client\" For the complete specification, see the full spec document ."},{"id":"docs:framework#framework","collection":"docs","slug":"framework","url":"/docs/framework/#framework","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"Framework","text":"This is the conceptual foundation beneath Jx Studio . You never have to read it to build with Studio — but if you want to know exactly what the format is, start here. Jx 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."},{"id":"docs:framework#core-premise","collection":"docs","slug":"framework","url":"/docs/framework/#core-premise","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"Core Premise","text":"Structure and state are data. The shape of each state entry determines its type and behavior — no additional flags required in the common case. A Jx component is a single .json file that can be fully self-describing: { \"$id\" : \"Counter\" , \"state\" : { \"count\" : 0 , \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" } }, \"tagName\" : \"my-counter\" , \"children\" : [ { \"tagName\" : \"span\" , \"textContent\" : \"${state.count}\" }, { \"tagName\" : \"button\" , \"textContent\" : \"+\" , \"onclick\" : { \"$ref\" : \"#/state/increment\" } } ] }"},{"id":"docs:framework#design-principles","collection":"docs","slug":"framework","url":"/docs/framework/#design-principles","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"Design Principles","text":"DOM-First Design — Property names mirror standard DOM element properties. tagName , className , textContent , hidden , tabIndex all map directly to their DOM equivalents. 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. JSON as the Authoritative Format — Documents are valid JSON. Fully serializable. No this ambiguity. Natively understood by visual builders, IDEs, validators, and bundlers. Explicit Over Implicit — Signal scope does not leak across component boundaries. Every dependency must be explicitly declared as a $prop . Standards Alignment — Where a web platform standard exists, Jx follows it: JSON Schema 2020-12, JSON Pointer (RFC 6901), Web Components v1, CSSOM camelCase."},{"id":"docs:framework#document-format","collection":"docs","slug":"framework","url":"/docs/framework/#document-format","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"Document Format","text":"Every Jx document has these top-level fields: Field Required Description $schema Recommended URI identifying the Jx dialect version $id Recommended Component identifier $defs Optional Pure JSON Schema type definitions (tooling only) state Optional Reactive state: signals, computed values, functions tagName Required HTML tag name for the root element children Optional Array of child element definitions and/or text nodes"},{"id":"docs:framework#reserved-keywords","collection":"docs","slug":"framework","url":"/docs/framework/#reserved-keywords","title":"Framework","description":"The Jx specification: a declarative DOM format using plain JSON with reactive state, web components, and standards alignment.","heading":"Reserved Keywords","text":"Keyword Purpose $schema Dialect identifier $id Component identifier $defs JSON Schema type definitions $ref Reference pointer (JSON Pointer, RFC 6901) $props Explicit prop passing at component boundary $prototype Constructor name — Web API, \"Function\" , or class $src External module specifier $switch Dynamic component switching $map Iteration context namespace $media Named media breakpoint declarations $elements Custom element dependency declarations timing Execution timing: \"compiler\" , \"server\" , \"client\" For the complete specification, see the full spec document ."},{"id":"docs:start","collection":"docs","slug":"start","url":"/docs/start/","title":"Start here","description":"What Jx Suite is, how to install Jx Studio, and where to go first — whether you build visually or want the format underneath.","heading":"","text":"Start here Jx 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. Pick your path: Build visually. Install Jx Studio and follow Your first project to go from starter template to published site. Understand the format. The Framework section documents the JSON documents Studio writes for you — components, state, reactivity, styling, and how a site compiles. Extend it. The Extending section covers writing extensions, embedding Studio, and the backend protocol."},{"id":"docs:start#start-here","collection":"docs","slug":"start","url":"/docs/start/#start-here","title":"Start here","description":"What Jx Suite is, how to install Jx Studio, and where to go first — whether you build visually or want the format underneath.","heading":"Start here","text":"Jx 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. Pick your path: Build visually. Install Jx Studio and follow Your first project to go from starter template to published site. Understand the format. The Framework section documents the JSON documents Studio writes for you — components, state, reactivity, styling, and how a site compiles. Extend it. The Extending section covers writing extensions, embedding Studio, and the backend protocol."},{"id":"docs:studio","collection":"docs","slug":"studio","url":"/docs/studio/","title":"Studio","description":"Jx Studio is the visual builder for Jx projects — edit content inline, design on a live canvas, wire logic, and publish with git.","heading":"","text":"Studio Jx 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. Everything Studio does is organized around a few surfaces: Browse your project — pages, components, content, and media with live previews. Edit mode — inline writing on the real page, with slash commands and frontmatter forms. Design mode — a live canvas per breakpoint with a full style inspector. Logic — state, formulas, events, and data without leaving the visual editor. Databases — connect SQLite, D1, or Supabase, define tables, decide who can read and write them, and browse the rows in a grid. Publish — commit, branch, and push; your host builds and serves the site. Studio writes plain JSON and Markdown files. Everything you build here is readable, diffable, and documented in the Framework section."},{"id":"docs:studio#studio","collection":"docs","slug":"studio","url":"/docs/studio/#studio","title":"Studio","description":"Jx Studio is the visual builder for Jx projects — edit content inline, design on a live canvas, wire logic, and publish with git.","heading":"Studio","text":"Jx 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. Everything Studio does is organized around a few surfaces: Browse your project — pages, components, content, and media with live previews. Edit mode — inline writing on the real page, with slash commands and frontmatter forms. Design mode — a live canvas per breakpoint with a full style inspector. Logic — state, formulas, events, and data without leaving the visual editor. Databases — connect SQLite, D1, or Supabase, define tables, decide who can read and write them, and browse the rows in a grid. Publish — commit, branch, and push; your host builds and serves the site. Studio writes plain JSON and Markdown files. Everything you build here is readable, diffable, and documented in the Framework section."},{"id":"docs:extending/embedding","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/","title":"Embedding overview","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.","heading":"","text":"Embedding overview Jx 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. The two layers Studio's backend abstraction has two layers, and you implement exactly one of them: 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. 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. Every 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. Who implements what today Host Platform adapter Transport Dev server ( @jxsuite/server ) packages/studio/src/platforms/devserver.ts — registered by default The literal /__studio/* HTTP routes — the protocol's reference implementation 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 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 Choosing your integration path 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 . 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 . Either 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. Partial backends are fine You 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 lists every route with its optionality and degradation. Related Writing a platform adapter — the StudioPlatform interface, registration, and the two real adapters The backend protocol — contract semantics, versioning, errors, and required behaviors Dev server internals — how the reference implementation is put together Protocol route reference — the generated route table"},{"id":"docs:extending/embedding#embedding-overview","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#embedding-overview","title":"Embedding overview","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.","heading":"Embedding overview","text":"Jx 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."},{"id":"docs:extending/embedding#the-two-layers","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#the-two-layers","title":"Embedding overview","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.","heading":"The two layers","text":"Studio's backend abstraction has two layers, and you implement exactly one of them: 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. 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. Every 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."},{"id":"docs:extending/embedding#who-implements-what-today","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#who-implements-what-today","title":"Embedding overview","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.","heading":"Who implements what today","text":"Host Platform adapter Transport Dev server ( @jxsuite/server ) packages/studio/src/platforms/devserver.ts — registered by default The literal /__studio/* HTTP routes — the protocol's reference implementation 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 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"},{"id":"docs:extending/embedding#choosing-your-integration-path","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#choosing-your-integration-path","title":"Embedding overview","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.","heading":"Choosing your integration path","text":"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 . 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 . Either 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."},{"id":"docs:extending/embedding#partial-backends-are-fine","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#partial-backends-are-fine","title":"Embedding overview","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.","heading":"Partial backends are fine","text":"You 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 lists every route with its optionality and degradation."},{"id":"docs:extending/embedding#related","collection":"docs","slug":"extending/embedding","url":"/docs/extending/embedding/#related","title":"Embedding overview","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.","heading":"Related","text":"Writing a platform adapter — the StudioPlatform interface, registration, and the two real adapters The backend protocol — contract semantics, versioning, errors, and required behaviors Dev server internals — how the reference implementation is put together Protocol route reference — the generated route table"},{"id":"docs:framework/agents","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/","title":"Working with agents","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.","heading":"","text":"Working with agents A 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. This page explains why that shape suits a model, where agents plug in, and the loop that turns a plausible answer into a checked one. Why documents are tractable where code is not Prompt-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: 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. 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. 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. None of this makes a model correct. It makes a model checkable — and that is the property you can build a workflow on. Three ways an agent meets a Jx project Studio's built-in assistant A 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 . An external coding agent Claude 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 . Validation in CI 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. The generate-validate-fix loop 1. Generate the project's schemas — jx schema . Run 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 . 2. Let the agent write files. 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. 3. Check the work — jx validate . The deterministic gate, run after every edit. It walks the project in five passes: 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. project.json against the generated entry schema. Every document under components/ , pages/ , and layouts/ against the bundled document schema. Every project-local *.class.json against the class schema. Each enabled extension's schema fragments, compiled standalone. Clean runs print Project is valid (N files checked in <root>) . Failures print Project is INVALID , then one block per file: pages/about.json: - /children/0: must NOT have additional properties 4. Feed the failures back verbatim. Each 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 . Wire 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. What an agent can build The whole framework is reachable from JSON, so the ceiling is higher than \"a static page\": Pages, layouts, and components — routing from the pages/ tree, shared shells with slot , reusable custom elements. See Site architecture . Content collections — folders of Markdown, JSON, or CSV files with schema validation, queried into pages. See Content collections . Databases — connections and data sections in project.json become real tables (D1, Supabase, or SQLite) with CRUD and form actions. See Databases . Server functions — a state entry with timing: \"server\" compiles into a server handler your secrets can live behind. See Timing . Accounts and sessions — the auth extension adds sign-in, sessions, and per-table permissions. See Auth and secrets . One 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. The 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 . Related Authoring rules for agents — the briefing to hand an external coding agent Machine-readable docs — the schema and documentation endpoints an agent can fetch CLI commands — every flag of jx schema , jx validate , and the rest Schema composition — how the entry documents are assembled AI assistant — the agent that ships inside Studio"},{"id":"docs:framework/agents#working-with-agents","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#working-with-agents","title":"Working with agents","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.","heading":"Working with agents","text":"A 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. This page explains why that shape suits a model, where agents plug in, and the loop that turns a plausible answer into a checked one."},{"id":"docs:framework/agents#why-documents-are-tractable-where-code-is-not","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#why-documents-are-tractable-where-code-is-not","title":"Working with agents","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.","heading":"Why documents are tractable where code is not","text":"Prompt-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: 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. 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. 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. None of this makes a model correct. It makes a model checkable — and that is the property you can build a workflow on."},{"id":"docs:framework/agents#three-ways-an-agent-meets-a-jx-project","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#three-ways-an-agent-meets-a-jx-project","title":"Working with agents","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.","heading":"Three ways an agent meets a Jx project","text":""},{"id":"docs:framework/agents#studios-built-in-assistant","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#studios-built-in-assistant","title":"Working with agents","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.","heading":"Studio's built-in assistant","text":"A 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 ."},{"id":"docs:framework/agents#an-external-coding-agent","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#an-external-coding-agent","title":"Working with agents","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.","heading":"An external coding agent","text":"Claude 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 ."},{"id":"docs:framework/agents#validation-in-ci","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#validation-in-ci","title":"Working with agents","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.","heading":"Validation in CI","text":"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."},{"id":"docs:framework/agents#the-generate-validate-fix-loop","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#the-generate-validate-fix-loop","title":"Working with agents","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.","heading":"The generate-validate-fix loop","text":"1. Generate the project's schemas — jx schema . Run 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 . 2. Let the agent write files. 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. 3. Check the work — jx validate . The deterministic gate, run after every edit. It walks the project in five passes: 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. project.json against the generated entry schema. Every document under components/ , pages/ , and layouts/ against the bundled document schema. Every project-local *.class.json against the class schema. Each enabled extension's schema fragments, compiled standalone. Clean runs print Project is valid (N files checked in <root>) . Failures print Project is INVALID , then one block per file: pages/about.json: - /children/0: must NOT have additional properties 4. Feed the failures back verbatim. Each 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 . Wire 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."},{"id":"docs:framework/agents#what-an-agent-can-build","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#what-an-agent-can-build","title":"Working with agents","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.","heading":"What an agent can build","text":"The whole framework is reachable from JSON, so the ceiling is higher than \"a static page\": Pages, layouts, and components — routing from the pages/ tree, shared shells with slot , reusable custom elements. See Site architecture . Content collections — folders of Markdown, JSON, or CSV files with schema validation, queried into pages. See Content collections . Databases — connections and data sections in project.json become real tables (D1, Supabase, or SQLite) with CRUD and form actions. See Databases . Server functions — a state entry with timing: \"server\" compiles into a server handler your secrets can live behind. See Timing . Accounts and sessions — the auth extension adds sign-in, sessions, and per-table permissions. See Auth and secrets . One 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. The 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 ."},{"id":"docs:framework/agents#related","collection":"docs","slug":"framework/agents","url":"/docs/framework/agents/#related","title":"Working with agents","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.","heading":"Related","text":"Authoring rules for agents — the briefing to hand an external coding agent Machine-readable docs — the schema and documentation endpoints an agent can fetch CLI commands — every flag of jx schema , jx validate , and the rest Schema composition — how the entry documents are assembled AI assistant — the agent that ships inside Studio"},{"id":"docs:framework/build","collection":"docs","slug":"framework/build","url":"/docs/framework/build/","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"","text":"How compilation works 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. The 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. What jx build does Running jx build (see CLI commands ) orchestrates one pipeline: Load project.json and register the extensions it declares ( extensions ), which contribute file formats such as Markdown pages. 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. Expand dynamic routes — a [param] page's $paths definition produces one concrete route per entry. 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. 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). The build finishes with a summary ( Done: 12 routes → 34 files ) and exits non-zero if any route failed. Static detection The 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: a state entry that exists at runtime (a plain value, a $prototype entry, or anything with a default ) a ${…} template string in a property, style, or attribute value a $ref binding on an element property a $switch node a mapped array ( $prototype: \"Array\" ) any child that is itself dynamic Three kinds of state are exempt because they are resolved during the build and then removed: the injected $site and $page context (read-only, never reactive) entries with timing: \"compiler\" — resolved at build time and baked into the HTML schema-only definitions (pure type information produces no output) What compiles away Site and page context looks like state, but it is data the compiler already has. This heading: { \"tagName\" : \"h1\" , \"textContent\" : \"${state.$site.name} — ${state.$page.title}\" } compiles to literal HTML with the template evaluated and gone: < h1 >Acme Realty — About us</ h1 > The 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. The output tiers Fully static When 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. Islands in a static shell A 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: < jx-island-0 ></ jx-island-0 > < script type = \"module\" src = \"./_islands/jx-island-0.js\" ></ script > The 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. Dynamic pages When 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. Two 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. Across 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. CSS extraction Style 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 for the authoring model. Why is my page shipping JavaScript? Work the static-detection list backwards: 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. A ${…} template string whose inputs exist only at runtime keeps its binding alive. Templates over build-time data compile away. $switch , $ref bindings, and mapped arrays over runtime state need the client tier; mapped arrays over build-resolved content do not. Interactive components cost their own module on the pages that use them — the rest of the page stays static. Related CLI commands — jx build flags and the rest of the CLI The dev server — the development-time counterpart Site architecture — the directory layout the build consumes Reactivity — what the dynamic tier compiles from Components — the component model behind islands"},{"id":"docs:framework/build#how-compilation-works","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#how-compilation-works","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"How compilation works","text":"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. The 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."},{"id":"docs:framework/build#what-jx-build-does","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#what-jx-build-does","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"What jx build does","text":"Running jx build (see CLI commands ) orchestrates one pipeline: Load project.json and register the extensions it declares ( extensions ), which contribute file formats such as Markdown pages. 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. Expand dynamic routes — a [param] page's $paths definition produces one concrete route per entry. 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. 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). The build finishes with a summary ( Done: 12 routes → 34 files ) and exits non-zero if any route failed."},{"id":"docs:framework/build#static-detection","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#static-detection","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Static detection","text":"The 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: a state entry that exists at runtime (a plain value, a $prototype entry, or anything with a default ) a ${…} template string in a property, style, or attribute value a $ref binding on an element property a $switch node a mapped array ( $prototype: \"Array\" ) any child that is itself dynamic Three kinds of state are exempt because they are resolved during the build and then removed: the injected $site and $page context (read-only, never reactive) entries with timing: \"compiler\" — resolved at build time and baked into the HTML schema-only definitions (pure type information produces no output)"},{"id":"docs:framework/build#what-compiles-away","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#what-compiles-away","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"What compiles away","text":"Site and page context looks like state, but it is data the compiler already has. This heading: { \"tagName\" : \"h1\" , \"textContent\" : \"${state.$site.name} — ${state.$page.title}\" } compiles to literal HTML with the template evaluated and gone: < h1 >Acme Realty — About us</ h1 > The 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."},{"id":"docs:framework/build#the-output-tiers","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#the-output-tiers","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"The output tiers","text":""},{"id":"docs:framework/build#fully-static","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#fully-static","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Fully static","text":"When 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."},{"id":"docs:framework/build#islands-in-a-static-shell","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#islands-in-a-static-shell","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Islands in a static shell","text":"A 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: < jx-island-0 ></ jx-island-0 > < script type = \"module\" src = \"./_islands/jx-island-0.js\" ></ script > The 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."},{"id":"docs:framework/build#dynamic-pages","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#dynamic-pages","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Dynamic pages","text":"When 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. Two 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. Across 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."},{"id":"docs:framework/build#css-extraction","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#css-extraction","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"CSS extraction","text":"Style 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 for the authoring model."},{"id":"docs:framework/build#why-is-my-page-shipping-javascript","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#why-is-my-page-shipping-javascript","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Why is my page shipping JavaScript?","text":"Work the static-detection list backwards: 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. A ${…} template string whose inputs exist only at runtime keeps its binding alive. Templates over build-time data compile away. $switch , $ref bindings, and mapped arrays over runtime state need the client tier; mapped arrays over build-resolved content do not. Interactive components cost their own module on the pages that use them — the rest of the page stays static."},{"id":"docs:framework/build#related","collection":"docs","slug":"framework/build","url":"/docs/framework/build/#related","title":"How compilation works","description":"What jx build does — route discovery, static detection, and the output tiers — and why most Jx pages ship zero JavaScript.","heading":"Related","text":"CLI commands — jx build flags and the rest of the CLI The dev server — the development-time counterpart Site architecture — the directory layout the build consumes Reactivity — what the dynamic tier compiles from Components — the component model behind islands"},{"id":"docs:framework/reference","collection":"docs","slug":"framework/reference","url":"/docs/framework/reference/","title":"Reference","description":"Generated reference pages — the formula catalog and the blessed operator set, produced from the packages that ship them.","heading":"","text":"Reference The 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 . Formula catalog — every composite formula in @jxsuite/formulas , with parameters and expression bodies. Operator reference — the closed operator set the Jx schema admits in $expression trees. Extension authors: the protocol route reference is generated the same way from @jxsuite/protocol ."},{"id":"docs:framework/reference#reference","collection":"docs","slug":"framework/reference","url":"/docs/framework/reference/#reference","title":"Reference","description":"Generated reference pages — the formula catalog and the blessed operator set, produced from the packages that ship them.","heading":"Reference","text":"The 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 . Formula catalog — every composite formula in @jxsuite/formulas , with parameters and expression bodies. Operator reference — the closed operator set the Jx schema admits in $expression trees. Extension authors: the protocol route reference is generated the same way from @jxsuite/protocol ."},{"id":"docs:framework/site","collection":"docs","slug":"framework/site","url":"/docs/framework/site/","title":"Site architecture","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.","heading":"","text":"Site architecture Studio manages this structure for you — Browse your project maps to pages/ , components/ , and content/ ; the content-type builder writes the content section; Project settings edits project.json . This section documents the on-disk layout for reference. A 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. Project anatomy my-site/ ├── project.json # Site configuration (required) ├── pages/ # File-based routing (required) │ ├── index.json # → / │ ├── about.json # → /about │ └── blog/ │ ├── index.json # → /blog │ └── [slug].json # → /blog/:slug (dynamic) ├── layouts/ # Shared page shells │ └── base.json ├── components/ # Reusable Jx components ├── content/ # Content collections │ └── blog/ │ └── hello-world.md ├── data/ # Static data files ├── public/ # Static assets (copied verbatim) └── dist/ # Build output (generated) Directory Purpose Required pages/ File-based routing. Each page file becomes a route. Yes layouts/ Layout components, referenced by pages via $layout . No components/ Reusable components, referenced via $ref or $elements . No content/ Content collections with schema validation. No data/ Static data files loaded at build time. No schema enforcement. No public/ Static assets copied verbatim to dist/ . No processing. No dist/ Build output. Ignored by git. Generated Files 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 ). Configuration 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 . Routing Every 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 . Layouts Layouts 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 . Content collections The 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 and Relationships . Markdown pages and content Markdown 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 . SEO and metadata Pages 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 . Images Images 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 . Redirects The redirects map in project.json declares old-URL-to-new-URL rules, including :param patterns and per-rule HTTP status codes. See Redirects . Databases, accounts, and server functions Not 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 , Auth and secrets , and Timing . Building and deploying 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 and Deployment ; Studio itself never runs this step — it commits and pushes your source , and your host builds on push."},{"id":"docs:framework/site#site-architecture","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#site-architecture","title":"Site architecture","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.","heading":"Site architecture","text":"Studio manages this structure for you — Browse your project maps to pages/ , components/ , and content/ ; the content-type builder writes the content section; Project settings edits project.json . This section documents the on-disk layout for reference. A 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."},{"id":"docs:framework/site#project-anatomy","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#project-anatomy","title":"Site architecture","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.","heading":"Project anatomy","text":"my-site/ ├── project.json # Site configuration (required) ├── pages/ # File-based routing (required) │ ├── index.json # → / │ ├── about.json # → /about │ └── blog/ │ ├── index.json # → /blog │ └── [slug].json # → /blog/:slug (dynamic) ├── layouts/ # Shared page shells │ └── base.json ├── components/ # Reusable Jx components ├── content/ # Content collections │ └── blog/ │ └── hello-world.md ├── data/ # Static data files ├── public/ # Static assets (copied verbatim) └── dist/ # Build output (generated) Directory Purpose Required pages/ File-based routing. Each page file becomes a route. Yes layouts/ Layout components, referenced by pages via $layout . No components/ Reusable components, referenced via $ref or $elements . No content/ Content collections with schema validation. No data/ Static data files loaded at build time. No schema enforcement. No public/ Static assets copied verbatim to dist/ . No processing. No dist/ Build output. Ignored by git. Generated Files 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 )."},{"id":"docs:framework/site#configuration","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#configuration","title":"Site architecture","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.","heading":"Configuration","text":"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 ."},{"id":"docs:framework/site#routing","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#routing","title":"Site architecture","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.","heading":"Routing","text":"Every 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 ."},{"id":"docs:framework/site#layouts","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#layouts","title":"Site architecture","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.","heading":"Layouts","text":"Layouts 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 ."},{"id":"docs:framework/site#content-collections","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#content-collections","title":"Site architecture","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.","heading":"Content collections","text":"The 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 and Relationships ."},{"id":"docs:framework/site#markdown-pages-and-content","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#markdown-pages-and-content","title":"Site architecture","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.","heading":"Markdown pages and content","text":"Markdown 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 ."},{"id":"docs:framework/site#seo-and-metadata","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#seo-and-metadata","title":"Site architecture","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.","heading":"SEO and metadata","text":"Pages 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 ."},{"id":"docs:framework/site#images","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#images","title":"Site architecture","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.","heading":"Images","text":"Images 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 ."},{"id":"docs:framework/site#redirects","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#redirects","title":"Site architecture","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.","heading":"Redirects","text":"The redirects map in project.json declares old-URL-to-new-URL rules, including :param patterns and per-rule HTTP status codes. See Redirects ."},{"id":"docs:framework/site#databases-accounts-and-server-functions","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#databases-accounts-and-server-functions","title":"Site architecture","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.","heading":"Databases, accounts, and server functions","text":"Not 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 , Auth and secrets , and Timing ."},{"id":"docs:framework/site#building-and-deploying","collection":"docs","slug":"framework/site","url":"/docs/framework/site/#building-and-deploying","title":"Site architecture","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.","heading":"Building and deploying","text":"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 and Deployment ; Studio itself never runs this step — it commits and pushes your source , and your host builds on push."},{"id":"docs:start/coming-from-ai-builders","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/","title":"Coming from Lovable or Builder.io","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.","heading":"","text":"Coming from Lovable or Builder.io AI 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 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. What the assistant edits A Jx page or component is a structured document — readable JSON describing its elements, styles, and state, in a format that's fully documented . 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. In an AI builder In Jx Prompt-to-code generation An assistant editing your project's documents Generated source you may never read Documents every Studio panel can show you Regenerate and hope Edit by hand, visually, or by prompt — interchangeably Export or eject Nothing to export — the files are already yours Editable in both directions This 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. Every change is a diff Because the assistant changes files, its work shows up in 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. Ownership A 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 . (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. Where Jx is different Two differences are worth stating plainly, and neither is the one people expect. Jx builds full applications, not just brochure sites: database connections , user accounts and sessions , and server-side logic 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. The 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. Start here The AI assistant — what it can do and how to work with it Your first project — see the editor the assistant shares with you Framework — the document format underneath it all"},{"id":"docs:start/coming-from-ai-builders#coming-from-lovable-or-builderio","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#coming-from-lovable-or-builderio","title":"Coming from Lovable or Builder.io","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.","heading":"Coming from Lovable or Builder.io","text":"AI 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 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."},{"id":"docs:start/coming-from-ai-builders#what-the-assistant-edits","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#what-the-assistant-edits","title":"Coming from Lovable or Builder.io","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.","heading":"What the assistant edits","text":"A Jx page or component is a structured document — readable JSON describing its elements, styles, and state, in a format that's fully documented . 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. In an AI builder In Jx Prompt-to-code generation An assistant editing your project's documents Generated source you may never read Documents every Studio panel can show you Regenerate and hope Edit by hand, visually, or by prompt — interchangeably Export or eject Nothing to export — the files are already yours"},{"id":"docs:start/coming-from-ai-builders#editable-in-both-directions","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#editable-in-both-directions","title":"Coming from Lovable or Builder.io","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.","heading":"Editable in both directions","text":"This 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."},{"id":"docs:start/coming-from-ai-builders#every-change-is-a-diff","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#every-change-is-a-diff","title":"Coming from Lovable or Builder.io","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.","heading":"Every change is a diff","text":"Because the assistant changes files, its work shows up in 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."},{"id":"docs:start/coming-from-ai-builders#ownership","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#ownership","title":"Coming from Lovable or Builder.io","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.","heading":"Ownership","text":"A 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 . (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."},{"id":"docs:start/coming-from-ai-builders#where-jx-is-different","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#where-jx-is-different","title":"Coming from Lovable or Builder.io","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.","heading":"Where Jx is different","text":"Two differences are worth stating plainly, and neither is the one people expect. Jx builds full applications, not just brochure sites: database connections , user accounts and sessions , and server-side logic 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. The 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."},{"id":"docs:start/coming-from-ai-builders#start-here","collection":"docs","slug":"start/coming-from-ai-builders","url":"/docs/start/coming-from-ai-builders/#start-here","title":"Coming from Lovable or Builder.io","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.","heading":"Start here","text":"The AI assistant — what it can do and how to work with it Your first project — see the editor the assistant shares with you Framework — the document format underneath it all"},{"id":"docs:start/coming-from-webflow","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/","title":"Coming from Webflow or Framer","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.","heading":"","text":"Coming from Webflow or Framer You 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 (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. The concept map Webflow / Framer Jx Where to look Style panel + classes Style inspector Style inspector Variables Design tokens Design tokens Tag-level styles Stylebook Stylebook Symbols / components Components Working with components CMS collections Content types Content types Collection lists Repeaters Repeaters Interactions Events, state, and formulas Logic Breakpoint bar A live canvas per breakpoint Design mode Site plan + publish Commit and sync; your host builds Publish Styling, without class anxiety In 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: The Stylebook sets what every heading, button, link, and form control looks like — your baseline, in one catalog. Component styles cover \"this card looks like this wherever it's used.\" Per-element styles from the Style inspector are reserved for true one-offs. Design tokens sit underneath all three — named colors, fonts, and sizes that every picker offers, like Webflow's Variables. The 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 . A canvas per breakpoint Instead 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 and Breakpoints . Symbols and components Webflow symbols and Framer components map to Jx 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 . CMS collections Collections translate directly. A content type names a collection, defines its fields — text, numbers, images, dates, and references between types — and gives every entry a schema-backed form. 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 builds the whole loop. Two 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 , 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. Collections 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 , with accounts and sessions 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. Interactions, honestly Behavior in Jx comes from three surfaces working together: State declares what a page knows, the Events tab binds behavior to clicks, typing, and submits, and 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. What Jx doesn't have is Webflow's scroll-driven animation timeline. Hover and focus effects with CSS transitions come through States and selectors , but choreographed scroll animation isn't a built-in surface today. Publishing and the missing lock-in When 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 has a built-in flow, and Netlify, GitHub Pages, and others 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. In 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 . Start here Your first project — starter template to published site Your first component — the symbol workflow, in Jx Your first collection — the CMS workflow, in Jx"},{"id":"docs:start/coming-from-webflow#coming-from-webflow-or-framer","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#coming-from-webflow-or-framer","title":"Coming from Webflow or Framer","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.","heading":"Coming from Webflow or Framer","text":"You 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 (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."},{"id":"docs:start/coming-from-webflow#the-concept-map","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#the-concept-map","title":"Coming from Webflow or Framer","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.","heading":"The concept map","text":"Webflow / Framer Jx Where to look Style panel + classes Style inspector Style inspector Variables Design tokens Design tokens Tag-level styles Stylebook Stylebook Symbols / components Components Working with components CMS collections Content types Content types Collection lists Repeaters Repeaters Interactions Events, state, and formulas Logic Breakpoint bar A live canvas per breakpoint Design mode Site plan + publish Commit and sync; your host builds Publish"},{"id":"docs:start/coming-from-webflow#styling-without-class-anxiety","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#styling-without-class-anxiety","title":"Coming from Webflow or Framer","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.","heading":"Styling, without class anxiety","text":"In 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: The Stylebook sets what every heading, button, link, and form control looks like — your baseline, in one catalog. Component styles cover \"this card looks like this wherever it's used.\" Per-element styles from the Style inspector are reserved for true one-offs. Design tokens sit underneath all three — named colors, fonts, and sizes that every picker offers, like Webflow's Variables. The 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 ."},{"id":"docs:start/coming-from-webflow#a-canvas-per-breakpoint","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#a-canvas-per-breakpoint","title":"Coming from Webflow or Framer","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.","heading":"A canvas per breakpoint","text":"Instead 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 and Breakpoints ."},{"id":"docs:start/coming-from-webflow#symbols-and-components","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#symbols-and-components","title":"Coming from Webflow or Framer","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.","heading":"Symbols and components","text":"Webflow symbols and Framer components map to Jx 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 ."},{"id":"docs:start/coming-from-webflow#cms-collections","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#cms-collections","title":"Coming from Webflow or Framer","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.","heading":"CMS collections","text":"Collections translate directly. A content type names a collection, defines its fields — text, numbers, images, dates, and references between types — and gives every entry a schema-backed form. 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 builds the whole loop. Two 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 , 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. Collections 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 , with accounts and sessions 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."},{"id":"docs:start/coming-from-webflow#interactions-honestly","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#interactions-honestly","title":"Coming from Webflow or Framer","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.","heading":"Interactions, honestly","text":"Behavior in Jx comes from three surfaces working together: State declares what a page knows, the Events tab binds behavior to clicks, typing, and submits, and 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. What Jx doesn't have is Webflow's scroll-driven animation timeline. Hover and focus effects with CSS transitions come through States and selectors , but choreographed scroll animation isn't a built-in surface today."},{"id":"docs:start/coming-from-webflow#publishing-and-the-missing-lock-in","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#publishing-and-the-missing-lock-in","title":"Coming from Webflow or Framer","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.","heading":"Publishing and the missing lock-in","text":"When 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 has a built-in flow, and Netlify, GitHub Pages, and others 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. In 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 ."},{"id":"docs:start/coming-from-webflow#start-here","collection":"docs","slug":"start/coming-from-webflow","url":"/docs/start/coming-from-webflow/#start-here","title":"Coming from Webflow or Framer","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.","heading":"Start here","text":"Your first project — starter template to published site Your first component — the symbol workflow, in Jx Your first collection — the CMS workflow, in Jx"},{"id":"docs:start/coming-from-wix","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/","title":"Coming from Wix or Squarespace","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.","heading":"","text":"Coming from Wix or Squarespace On 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. You still get a visual editor Jx 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 assumes you can code. One difference you'll notice immediately: Studio is an app you download , not a website you sign into. There's no account, because there's nothing to have an account on — it opens your files directly. Where your site lives Instead of one company's servers, your site touches three places, each with one job: Your computer — the project folder. This is the site: pages, images, settings, all as ordinary files Studio edits. A repository — an online copy of that folder, with its full history. It's your backup and your publishing channel. 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. The 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. A short glossary Term What it means Repository The online copy of your project folder, with its full history. GitHub is the best-known place to keep one. Commit A saved snapshot of your changes, with a short note saying what you did. Sync Sending your commits to the repository — this is the moment the host notices and rebuilds. Host The service that serves your site to visitors. Cloudflare Pages, Netlify, and GitHub Pages all work, and their basic plans are free. Build The automatic step where your project files are turned into the finished pages the host serves. What you take on Honestly: some one-time setup that Wix or Squarespace did for you. Connecting the pieces. You'll create a GitHub account and connect a host. Studio walks you through the repository part with Publish to GitHub , has a built-in flow for Cloudflare Pages , and the other hosts guide covers the rest — the host setup is two fields, once. Your domain. Domains come from a registrar or your host, not from the builder, and you connect one in the host's settings. 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. Editing 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. What you get back The trade is real, and so is the payoff: nothing is locked in. 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. You can switch hosts without rebuilding anything; the same files publish anywhere. Your pages and writing are readable files today and in twenty years, openable without Jx at all. 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. You 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. Start here Install Jx Studio — download the app for macOS, Windows, or Linux Your first project — from starter template to published site A tour of Jx Studio — find your way around the editor"},{"id":"docs:start/coming-from-wix#coming-from-wix-or-squarespace","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#coming-from-wix-or-squarespace","title":"Coming from Wix or Squarespace","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.","heading":"Coming from Wix or Squarespace","text":"On 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."},{"id":"docs:start/coming-from-wix#you-still-get-a-visual-editor","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#you-still-get-a-visual-editor","title":"Coming from Wix or Squarespace","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.","heading":"You still get a visual editor","text":"Jx 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 assumes you can code. One difference you'll notice immediately: Studio is an app you download , not a website you sign into. There's no account, because there's nothing to have an account on — it opens your files directly."},{"id":"docs:start/coming-from-wix#where-your-site-lives","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#where-your-site-lives","title":"Coming from Wix or Squarespace","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.","heading":"Where your site lives","text":"Instead of one company's servers, your site touches three places, each with one job: Your computer — the project folder. This is the site: pages, images, settings, all as ordinary files Studio edits. A repository — an online copy of that folder, with its full history. It's your backup and your publishing channel. 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. The 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."},{"id":"docs:start/coming-from-wix#a-short-glossary","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#a-short-glossary","title":"Coming from Wix or Squarespace","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.","heading":"A short glossary","text":"Term What it means Repository The online copy of your project folder, with its full history. GitHub is the best-known place to keep one. Commit A saved snapshot of your changes, with a short note saying what you did. Sync Sending your commits to the repository — this is the moment the host notices and rebuilds. Host The service that serves your site to visitors. Cloudflare Pages, Netlify, and GitHub Pages all work, and their basic plans are free. Build The automatic step where your project files are turned into the finished pages the host serves."},{"id":"docs:start/coming-from-wix#what-you-take-on","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#what-you-take-on","title":"Coming from Wix or Squarespace","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.","heading":"What you take on","text":"Honestly: some one-time setup that Wix or Squarespace did for you. Connecting the pieces. You'll create a GitHub account and connect a host. Studio walks you through the repository part with Publish to GitHub , has a built-in flow for Cloudflare Pages , and the other hosts guide covers the rest — the host setup is two fields, once. Your domain. Domains come from a registrar or your host, not from the builder, and you connect one in the host's settings. 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. Editing 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."},{"id":"docs:start/coming-from-wix#what-you-get-back","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#what-you-get-back","title":"Coming from Wix or Squarespace","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.","heading":"What you get back","text":"The trade is real, and so is the payoff: nothing is locked in. 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. You can switch hosts without rebuilding anything; the same files publish anywhere. Your pages and writing are readable files today and in twenty years, openable without Jx at all. 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. You 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."},{"id":"docs:start/coming-from-wix#start-here","collection":"docs","slug":"start/coming-from-wix","url":"/docs/start/coming-from-wix/#start-here","title":"Coming from Wix or Squarespace","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.","heading":"Start here","text":"Install Jx Studio — download the app for macOS, Windows, or Linux Your first project — from starter template to published site A tour of Jx Studio — find your way around the editor"},{"id":"docs:start/coming-from-wordpress","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"","text":"Coming from WordPress Most 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 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 — but that's a layer you opt into, not the substrate your pages sit on. If 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. The concept map WordPress Jx Where to look Theme Starter + Stylebook + tokens Starter templates Blocks Elements and components Elements The block editor Edit mode Edit mode Custom post types + custom fields Content types Content types Media library Media Media Plugins Extensions and npm packages Extending Publish button Commit and sync Publish The rest of this page walks through each row, then gets honest about what's different. Themes A 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: A starter template 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. The Stylebook sets the default look of every heading, button, link, and form control in one catalog. Design tokens name your colors, fonts, and sizes once, so the whole site draws from one palette. Blocks and the editor Gutenberg's blocks split into two ideas. Elements are the building parts — headings, images, sections, buttons — placed from the Elements panel by click or drag. 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. Writing feels like the part of Gutenberg you liked. Open a content page in Edit mode 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. Custom post types and fields If you've modeled content with custom post types and Advanced Custom Fields, 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. Entries 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 , 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 . Media Drag 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 . Plugins This is the honest gap. Jx has an extension system — 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 . But there is no plugin marketplace yet, and the catalog is a fraction of the WordPress directory's. The 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 and accounts and sessions , but you assemble the feature instead of activating someone else's. Publishing There'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 . What's different, in one list 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 ), kept apart from your writing. 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 , and it's separate from how you edit. No plugin marketplace yet. Extensions and npm components exist; a browsable ecosystem doesn't, yet. Publishing is git. Studio handles the git parts with buttons, but a repository and a host are part of the setup. What you'll miss The plugin directory. Decades of plugins for every niche — commerce, memberships, event calendars. The server-backed pieces are buildable today: data tables give you real reads and writes, auth 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. 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. The sheer size of the ecosystem — hosts, agencies, themes, and twenty years of tutorials. What you gain 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. Fast by default. Prebuilt pages from a CDN and automatically optimized images, with no caching plugins to tune. Version control of everything. Design, settings, and content all live in git — every change is a reviewable commit you can roll back. Portability. Your content is Markdown any tool can open; moving hosts is a settings change, not a database migration. A real design system. Tokens and the Stylebook instead of overriding theme CSS from a child theme. Start here Your first project — starter to published site in about ten minutes Your first collection — build the Jx version of a custom post type A tour of Jx Studio — learn the workspace before diving in"},{"id":"docs:start/coming-from-wordpress#coming-from-wordpress","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#coming-from-wordpress","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Coming from WordPress","text":"Most 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 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 — but that's a layer you opt into, not the substrate your pages sit on. If 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."},{"id":"docs:start/coming-from-wordpress#the-concept-map","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#the-concept-map","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"The concept map","text":"WordPress Jx Where to look Theme Starter + Stylebook + tokens Starter templates Blocks Elements and components Elements The block editor Edit mode Edit mode Custom post types + custom fields Content types Content types Media library Media Media Plugins Extensions and npm packages Extending Publish button Commit and sync Publish The rest of this page walks through each row, then gets honest about what's different."},{"id":"docs:start/coming-from-wordpress#themes","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#themes","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Themes","text":"A 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: A starter template 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. The Stylebook sets the default look of every heading, button, link, and form control in one catalog. Design tokens name your colors, fonts, and sizes once, so the whole site draws from one palette."},{"id":"docs:start/coming-from-wordpress#blocks-and-the-editor","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#blocks-and-the-editor","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Blocks and the editor","text":"Gutenberg's blocks split into two ideas. Elements are the building parts — headings, images, sections, buttons — placed from the Elements panel by click or drag. 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. Writing feels like the part of Gutenberg you liked. Open a content page in Edit mode 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."},{"id":"docs:start/coming-from-wordpress#custom-post-types-and-fields","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#custom-post-types-and-fields","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Custom post types and fields","text":"If you've modeled content with custom post types and Advanced Custom Fields, 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. Entries 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 , 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 ."},{"id":"docs:start/coming-from-wordpress#media","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#media","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Media","text":"Drag 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 ."},{"id":"docs:start/coming-from-wordpress#plugins","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#plugins","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Plugins","text":"This is the honest gap. Jx has an extension system — 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 . But there is no plugin marketplace yet, and the catalog is a fraction of the WordPress directory's. The 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 and accounts and sessions , but you assemble the feature instead of activating someone else's."},{"id":"docs:start/coming-from-wordpress#publishing","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#publishing","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Publishing","text":"There'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 ."},{"id":"docs:start/coming-from-wordpress#whats-different-in-one-list","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#whats-different-in-one-list","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"What's different, in one list","text":"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 ), kept apart from your writing. 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 , and it's separate from how you edit. No plugin marketplace yet. Extensions and npm components exist; a browsable ecosystem doesn't, yet. Publishing is git. Studio handles the git parts with buttons, but a repository and a host are part of the setup."},{"id":"docs:start/coming-from-wordpress#what-youll-miss","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#what-youll-miss","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"What you'll miss","text":"The plugin directory. Decades of plugins for every niche — commerce, memberships, event calendars. The server-backed pieces are buildable today: data tables give you real reads and writes, auth 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. 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. The sheer size of the ecosystem — hosts, agencies, themes, and twenty years of tutorials."},{"id":"docs:start/coming-from-wordpress#what-you-gain","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#what-you-gain","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"What you gain","text":"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. Fast by default. Prebuilt pages from a CDN and automatically optimized images, with no caching plugins to tune. Version control of everything. Design, settings, and content all live in git — every change is a reviewable commit you can roll back. Portability. Your content is Markdown any tool can open; moving hosts is a settings change, not a database migration. A real design system. Tokens and the Stylebook instead of overriding theme CSS from a child theme."},{"id":"docs:start/coming-from-wordpress#start-here","collection":"docs","slug":"start/coming-from-wordpress","url":"/docs/start/coming-from-wordpress/#start-here","title":"Coming from WordPress","description":"How WordPress concepts map to Jx — themes, blocks, custom post types, media, plugins, publishing — and what's honestly different when content is files.","heading":"Start here","text":"Your first project — starter to published site in about ten minutes Your first collection — build the Jx version of a custom post type A tour of Jx Studio — learn the workspace before diving in"},{"id":"docs:start/first-collection","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/","title":"Tutorial: a blog with content collections","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.","heading":"","text":"Tutorial: a blog with content collections In 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. About 20 minutes. Before you start: Have a project open in Jx Studio — Your first project gets you there. Knowing your way around the canvas and panels helps; Tutorial: your first interactive component is the gentlest way in, but isn't required. 1. Create the posts content type A content type is your CMS schema: where a collection's entries live, and what fields each one carries. Click the Settings gear at the bottom of the activity bar, then go to Settings > Content Types . Click New Entry at the bottom of the type list. Type Posts and click Create . You should see the new type selected, named posts , with a matching source folder ( content/posts/ ) and an empty field schema ready to fill. 2. Give it fields Add three fields — for each one, click Add Field , type the name, pick its type, and click Add : title — type string , with the Required switch on. date — type string , format date , so entries get a real date field. description — type string . You 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 . 3. Create your first post Click Manage in the toolbar. Click New — the menu now lists an item for the type you just defined. Pick Posts . Name the entry hello-world . Studio 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. 4. Fill it in The post opens in Edit mode with a Properties bar directly above the page — the same fields your schema defined: Type Hello World into title . Pick today in the date field. Give description a sentence — The first post on my brand-new blog. Then click into the page below and write a paragraph or two of body text. You 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 . 5. Add two more posts Repeat 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. You should now have three entries under Manage's Content filter, two of them with empty frontmatter. 6. Open the collection as a grid Editing entries one file at a time doesn't scale, so Studio can open the whole collection as a spreadsheet: Click Files in the activity bar. Right-click the content/posts folder and choose Edit Collection in Grid . You 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. 7. Fill in the blanks and save Double-click each empty title cell and type one. Cells are typed, so date cells give you a date field. Fill in the date and description cells the same way. Edited cells are highlighted, and nothing touches your files yet. Click Save — the button counts your pending changes — or press ⌘S (macOS) / Ctrl+S (Windows/Linux). Studio writes every change in one batch and reports what saved. Ranges, fill-down, and find & replace are covered in Grid mode . Saving a collection row rewrites that file's frontmatter block — which is exactly what fills in your blank entries here. 8. Create the Blog page Click Manage , then New > Page , and name it Blog . Studio writes the page and opens it — this is your site's /blog address. In the mode switcher on the right side of the toolbar, click Design . You should see the empty page on the design canvas, once per breakpoint. 9. Design one post card Design a single card — the repeater will copy it per post: Open Elements in the activity bar. With nothing selected, click the article card. With the article selected, click the h3 card, then the p card twice — each new element lands inside the selection. 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 . You should see one plausible-looking post card on the canvas. Style it as much or as little as you like — see Design mode . 10. Query the collection from state The page needs the posts as data it can render: Click State in the activity bar, then the + Add… picker. Pick ContentCollection — it's listed with the sources your project's imports and extensions provide. Rename the new entry to posts (type the name and press Enter ). Set contentType to posts , and add a sort rule on the date field with order desc , so the newest post lists first. Open 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 . 11. Repeat the card for every post Right-click the article — on the canvas or in Layers — and choose Repeat… . In the dialog, set Items source to posts . Click Create Repeater . Your 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 . 12. Bind the card to each post's fields Inside the template, each post's data is in scope: Double-click the heading, select the placeholder text, and delete it. Click Insert data on the floating toolbar and pick item.data.title . A live placeholder lands in the text. Do the same for the two paragraphs: item.data.description and item.data.date . Each 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.) 13. Preview and save Switch on the Preview toggle in the tab bar. You 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 ⌘S / Ctrl+S . When you're ready, commit the lot from Source Control — see Source control . Give each post its own page The 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 shows the exact file, and 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. What you built A complete content pipeline, end to end: A content type ( posts ) — schema-backed entries with one-click creation from Manage. Three entries , edited both one at a time (the Properties bar) and in bulk ( Grid mode ). A ContentCollection state entry — the collection as live, sorted data on a page. A repeater whose template binds item.data fields — one designed card, rendered per post. On 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 and Lists . Next steps Turn the post card into a reusable component — Working with components . Add an author type and point posts at it with a reference field — Content types and Relationships . Give posts their own pages with a dynamic route — Routing . Publish it — Git & publish ."},{"id":"docs:start/first-collection#tutorial-a-blog-with-content-collections","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#tutorial-a-blog-with-content-collections","title":"Tutorial: a blog with content collections","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.","heading":"Tutorial: a blog with content collections","text":"In 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. About 20 minutes. Before you start: Have a project open in Jx Studio — Your first project gets you there. Knowing your way around the canvas and panels helps; Tutorial: your first interactive component is the gentlest way in, but isn't required."},{"id":"docs:start/first-collection#1-create-the-posts-content-type","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#1-create-the-posts-content-type","title":"Tutorial: a blog with content collections","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.","heading":"1. Create the posts content type","text":"A content type is your CMS schema: where a collection's entries live, and what fields each one carries. Click the Settings gear at the bottom of the activity bar, then go to Settings > Content Types . Click New Entry at the bottom of the type list. Type Posts and click Create . You should see the new type selected, named posts , with a matching source folder ( content/posts/ ) and an empty field schema ready to fill."},{"id":"docs:start/first-collection#2-give-it-fields","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#2-give-it-fields","title":"Tutorial: a blog with content collections","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.","heading":"2. Give it fields","text":"Add three fields — for each one, click Add Field , type the name, pick its type, and click Add : title — type string , with the Required switch on. date — type string , format date , so entries get a real date field. description — type string . You 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 ."},{"id":"docs:start/first-collection#3-create-your-first-post","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#3-create-your-first-post","title":"Tutorial: a blog with content collections","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.","heading":"3. Create your first post","text":"Click Manage in the toolbar. Click New — the menu now lists an item for the type you just defined. Pick Posts . Name the entry hello-world . Studio 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."},{"id":"docs:start/first-collection#4-fill-it-in","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#4-fill-it-in","title":"Tutorial: a blog with content collections","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.","heading":"4. Fill it in","text":"The post opens in Edit mode with a Properties bar directly above the page — the same fields your schema defined: Type Hello World into title . Pick today in the date field. Give description a sentence — The first post on my brand-new blog. Then click into the page below and write a paragraph or two of body text. You 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 ."},{"id":"docs:start/first-collection#5-add-two-more-posts","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#5-add-two-more-posts","title":"Tutorial: a blog with content collections","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.","heading":"5. Add two more posts","text":"Repeat 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. You should now have three entries under Manage's Content filter, two of them with empty frontmatter."},{"id":"docs:start/first-collection#6-open-the-collection-as-a-grid","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#6-open-the-collection-as-a-grid","title":"Tutorial: a blog with content collections","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.","heading":"6. Open the collection as a grid","text":"Editing entries one file at a time doesn't scale, so Studio can open the whole collection as a spreadsheet: Click Files in the activity bar. Right-click the content/posts folder and choose Edit Collection in Grid . You 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."},{"id":"docs:start/first-collection#7-fill-in-the-blanks-and-save","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#7-fill-in-the-blanks-and-save","title":"Tutorial: a blog with content collections","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.","heading":"7. Fill in the blanks and save","text":"Double-click each empty title cell and type one. Cells are typed, so date cells give you a date field. Fill in the date and description cells the same way. Edited cells are highlighted, and nothing touches your files yet. Click Save — the button counts your pending changes — or press ⌘S (macOS) / Ctrl+S (Windows/Linux). Studio writes every change in one batch and reports what saved. Ranges, fill-down, and find & replace are covered in Grid mode . Saving a collection row rewrites that file's frontmatter block — which is exactly what fills in your blank entries here."},{"id":"docs:start/first-collection#8-create-the-blog-page","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#8-create-the-blog-page","title":"Tutorial: a blog with content collections","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.","heading":"8. Create the Blog page","text":"Click Manage , then New > Page , and name it Blog . Studio writes the page and opens it — this is your site's /blog address. In the mode switcher on the right side of the toolbar, click Design . You should see the empty page on the design canvas, once per breakpoint."},{"id":"docs:start/first-collection#9-design-one-post-card","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#9-design-one-post-card","title":"Tutorial: a blog with content collections","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.","heading":"9. Design one post card","text":"Design a single card — the repeater will copy it per post: Open Elements in the activity bar. With nothing selected, click the article card. With the article selected, click the h3 card, then the p card twice — each new element lands inside the selection. 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 . You should see one plausible-looking post card on the canvas. Style it as much or as little as you like — see Design mode ."},{"id":"docs:start/first-collection#10-query-the-collection-from-state","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#10-query-the-collection-from-state","title":"Tutorial: a blog with content collections","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.","heading":"10. Query the collection from state","text":"The page needs the posts as data it can render: Click State in the activity bar, then the + Add… picker. Pick ContentCollection — it's listed with the sources your project's imports and extensions provide. Rename the new entry to posts (type the name and press Enter ). Set contentType to posts , and add a sort rule on the date field with order desc , so the newest post lists first. Open 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 ."},{"id":"docs:start/first-collection#11-repeat-the-card-for-every-post","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#11-repeat-the-card-for-every-post","title":"Tutorial: a blog with content collections","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.","heading":"11. Repeat the card for every post","text":"Right-click the article — on the canvas or in Layers — and choose Repeat… . In the dialog, set Items source to posts . Click Create Repeater . Your 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 ."},{"id":"docs:start/first-collection#12-bind-the-card-to-each-posts-fields","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#12-bind-the-card-to-each-posts-fields","title":"Tutorial: a blog with content collections","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.","heading":"12. Bind the card to each post's fields","text":"Inside the template, each post's data is in scope: Double-click the heading, select the placeholder text, and delete it. Click Insert data on the floating toolbar and pick item.data.title . A live placeholder lands in the text. Do the same for the two paragraphs: item.data.description and item.data.date . Each 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.)"},{"id":"docs:start/first-collection#13-preview-and-save","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#13-preview-and-save","title":"Tutorial: a blog with content collections","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.","heading":"13. Preview and save","text":"Switch on the Preview toggle in the tab bar. You 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 ⌘S / Ctrl+S . When you're ready, commit the lot from Source Control — see Source control ."},{"id":"docs:start/first-collection#give-each-post-its-own-page","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#give-each-post-its-own-page","title":"Tutorial: a blog with content collections","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.","heading":"Give each post its own page","text":"The 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 shows the exact file, and 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."},{"id":"docs:start/first-collection#what-you-built","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#what-you-built","title":"Tutorial: a blog with content collections","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.","heading":"What you built","text":"A complete content pipeline, end to end: A content type ( posts ) — schema-backed entries with one-click creation from Manage. Three entries , edited both one at a time (the Properties bar) and in bulk ( Grid mode ). A ContentCollection state entry — the collection as live, sorted data on a page. A repeater whose template binds item.data fields — one designed card, rendered per post. On 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 and Lists ."},{"id":"docs:start/first-collection#next-steps","collection":"docs","slug":"start/first-collection","url":"/docs/start/first-collection/#next-steps","title":"Tutorial: a blog with content collections","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.","heading":"Next steps","text":"Turn the post card into a reusable component — Working with components . Add an author type and point posts at it with a reference field — Content types and Relationships . Give posts their own pages with a dynamic route — Routing . Publish it — Git & publish ."},{"id":"docs:start/first-component","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/","title":"Tutorial: your first interactive component","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.","heading":"","text":"Tutorial: your first interactive component In 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. About 15 minutes. Before you start: Have Jx Studio running — see Install Jx Studio . If Studio is completely new to you, skim Your first project first — this tutorial starts where it ends, with a project open. 1. Open a project Any project works. If you already have one from Your 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 . You should now see your project open on the canvas. 2. Create the component Click Manage in the toolbar. Click New and choose Component . Type counter-card and confirm. Studio 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 .) 3. Switch to Design mode Component files open in Edit mode. In the mode switcher on the right side of the toolbar, click Design . The 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 . 4. Add a button and a text line Click Elements in the activity bar to open the palette. With nothing selected, click the button card. The button is added to the empty component. Press Esc to clear the selection — otherwise the next element would land inside the button — then click the p card to add a paragraph. Select the button, open the Properties tab, and type Add one into Text Content . You 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 and the Elements panel . 5. Declare the count The component needs somewhere to keep its number. That's a state entry: Click State (the brackets icon) in the activity bar. 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. Rename it first: type count into the Name field and press Enter . Set Type to integer and leave Default at 0 . The 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 . 6. Bind the text to the count Now point the paragraph at the value instead of typing fixed text: Select the paragraph on the canvas. 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. 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. Studio pre-fills the field with your first state entry: ${state.count} . Keep it, or mix in words: Clicked ${state.count} times . The 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 . 7. Make the button count Select the button and click Events in the right panel. Click Add Event . A binding appears on onclick — and since the file has no functions yet, it starts as an inline handler. Switch the binding's mode to $expression , the mode for one-step reactions. In the formula editor, set the Operator to += , point the Target at count (a $ref ), and type 1 as the Value . The 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 . 8. Try it in Preview Switch on the Preview toggle in the tab bar. The paragraph now shows 0 — the real, resolved value. Click Add one a few times. You 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. 9. Watch the value in the Data explorer Click 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. When a page ever looks wrong, this panel is where you find out what it actually sees — see Data explorer . 10. Try a test value Because 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: Type 100 into the count field in the tab bar. The canvas re-renders with the count starting at 100, at every breakpoint. Clear the field to return to the default of 0 . Test values are a preview lens only — they're never saved into the component. Props and test values are covered in Working with components . 11. Save your work The tab shows a ● dot for unsaved changes. Press ⌘S (macOS) / Ctrl+S (Windows/Linux) — or click the Save button in the toolbar — and the status bar confirms with \"Saved\". You should see the dot disappear. When you're ready to publish, Source Control takes it from here — see Source control . What you built A working, reusable component — and every piece of the interactive toolkit in one pass: A state entry ( count ) — the component's memory, and automatically its prop. A template binding ( ${state.count} ) — text that follows the value wherever it goes. An event expression ( $count += 1 ) — behavior without a line of code. Preview , the Data explorer , and test values — three ways to watch it run. Everything 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 and Reactivity . Next steps Drop the card onto any page from the Elements panel — your components appear at the top of the palette. Style it — spacing, color, hover states — with the Style inspector . When one step isn't enough, build multi-step handlers as statements . Keep going: Tutorial: a blog with content collections ."},{"id":"docs:start/first-component#tutorial-your-first-interactive-component","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#tutorial-your-first-interactive-component","title":"Tutorial: your first interactive component","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.","heading":"Tutorial: your first interactive component","text":"In 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. About 15 minutes. Before you start: Have Jx Studio running — see Install Jx Studio . If Studio is completely new to you, skim Your first project first — this tutorial starts where it ends, with a project open."},{"id":"docs:start/first-component#1-open-a-project","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#1-open-a-project","title":"Tutorial: your first interactive component","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.","heading":"1. Open a project","text":"Any project works. If you already have one from Your 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 . You should now see your project open on the canvas."},{"id":"docs:start/first-component#2-create-the-component","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#2-create-the-component","title":"Tutorial: your first interactive component","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.","heading":"2. Create the component","text":"Click Manage in the toolbar. Click New and choose Component . Type counter-card and confirm. Studio 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 .)"},{"id":"docs:start/first-component#3-switch-to-design-mode","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#3-switch-to-design-mode","title":"Tutorial: your first interactive component","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.","heading":"3. Switch to Design mode","text":"Component files open in Edit mode. In the mode switcher on the right side of the toolbar, click Design . The 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 ."},{"id":"docs:start/first-component#4-add-a-button-and-a-text-line","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#4-add-a-button-and-a-text-line","title":"Tutorial: your first interactive component","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.","heading":"4. Add a button and a text line","text":"Click Elements in the activity bar to open the palette. With nothing selected, click the button card. The button is added to the empty component. Press Esc to clear the selection — otherwise the next element would land inside the button — then click the p card to add a paragraph. Select the button, open the Properties tab, and type Add one into Text Content . You 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 and the Elements panel ."},{"id":"docs:start/first-component#5-declare-the-count","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#5-declare-the-count","title":"Tutorial: your first interactive component","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.","heading":"5. Declare the count","text":"The component needs somewhere to keep its number. That's a state entry: Click State (the brackets icon) in the activity bar. 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. Rename it first: type count into the Name field and press Enter . Set Type to integer and leave Default at 0 . The 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 ."},{"id":"docs:start/first-component#6-bind-the-text-to-the-count","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#6-bind-the-text-to-the-count","title":"Tutorial: your first interactive component","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.","heading":"6. Bind the text to the count","text":"Now point the paragraph at the value instead of typing fixed text: Select the paragraph on the canvas. 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. 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. Studio pre-fills the field with your first state entry: ${state.count} . Keep it, or mix in words: Clicked ${state.count} times . The 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 ."},{"id":"docs:start/first-component#7-make-the-button-count","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#7-make-the-button-count","title":"Tutorial: your first interactive component","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.","heading":"7. Make the button count","text":"Select the button and click Events in the right panel. Click Add Event . A binding appears on onclick — and since the file has no functions yet, it starts as an inline handler. Switch the binding's mode to $expression , the mode for one-step reactions. In the formula editor, set the Operator to += , point the Target at count (a $ref ), and type 1 as the Value . The 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 ."},{"id":"docs:start/first-component#8-try-it-in-preview","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#8-try-it-in-preview","title":"Tutorial: your first interactive component","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.","heading":"8. Try it in Preview","text":"Switch on the Preview toggle in the tab bar. The paragraph now shows 0 — the real, resolved value. Click Add one a few times. You 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."},{"id":"docs:start/first-component#9-watch-the-value-in-the-data-explorer","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#9-watch-the-value-in-the-data-explorer","title":"Tutorial: your first interactive component","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.","heading":"9. Watch the value in the Data explorer","text":"Click 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. When a page ever looks wrong, this panel is where you find out what it actually sees — see Data explorer ."},{"id":"docs:start/first-component#10-try-a-test-value","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#10-try-a-test-value","title":"Tutorial: your first interactive component","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.","heading":"10. Try a test value","text":"Because 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: Type 100 into the count field in the tab bar. The canvas re-renders with the count starting at 100, at every breakpoint. Clear the field to return to the default of 0 . Test values are a preview lens only — they're never saved into the component. Props and test values are covered in Working with components ."},{"id":"docs:start/first-component#11-save-your-work","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#11-save-your-work","title":"Tutorial: your first interactive component","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.","heading":"11. Save your work","text":"The tab shows a ● dot for unsaved changes. Press ⌘S (macOS) / Ctrl+S (Windows/Linux) — or click the Save button in the toolbar — and the status bar confirms with \"Saved\". You should see the dot disappear. When you're ready to publish, Source Control takes it from here — see Source control ."},{"id":"docs:start/first-component#what-you-built","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#what-you-built","title":"Tutorial: your first interactive component","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.","heading":"What you built","text":"A working, reusable component — and every piece of the interactive toolkit in one pass: A state entry ( count ) — the component's memory, and automatically its prop. A template binding ( ${state.count} ) — text that follows the value wherever it goes. An event expression ( $count += 1 ) — behavior without a line of code. Preview , the Data explorer , and test values — three ways to watch it run. Everything 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 and Reactivity ."},{"id":"docs:start/first-component#next-steps","collection":"docs","slug":"start/first-component","url":"/docs/start/first-component/#next-steps","title":"Tutorial: your first interactive component","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.","heading":"Next steps","text":"Drop the card onto any page from the Elements panel — your components appear at the top of the palette. Style it — spacing, color, hover states — with the Style inspector . When one step isn't enough, build multi-step handlers as statements . Keep going: Tutorial: a blog with content collections ."},{"id":"docs:start/first-project","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"","text":"Your first project In 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 .) 1. Get Studio Download the desktop app for macOS, Windows, or Linux — there's no hosted Studio to sign into, it runs on your machine, against your files. Full details are in Install Jx Studio . 2. Create a project In 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 . Then pick a template . Start from Blank for an empty project, or clone one of the starter sites — 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. Already 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 — then open the result in Studio.) 3. Manage, edit, design Studio gives you a surface for each part of the job. Start in Manage to see your project — pages, components, content, and media — with live previews. Open a content page and switch to Edit 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 for the visual canvas: a live preview at every breakpoint, and a full CSS inspector for spacing, type, color, and hover states. 4. Add an interaction Websites do things. In Studio, interactivity comes from three panels working together — no separate \"code mode\" required: State — declare a value or a computed one (a counter, a total, a fetched list). Events — bind a handler to a click, input, or submit with the structured expression editor. Code — drop into the Monaco editor when a handler needs real JavaScript. Add a count to state, a button, and an onclick that increments it — you've built a reactive component. See Script & logic for the full toolkit. 5. Commit and publish When 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. Here'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 for the full flow. What's next Using Studio: Manage · Edit · Design · Script & logic · Git & publish For developers: Site architecture · Component model · Spec overview"},{"id":"docs:start/first-project#your-first-project","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#your-first-project","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"Your first project","text":"In 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 .)"},{"id":"docs:start/first-project#1-get-studio","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#1-get-studio","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"1. Get Studio","text":"Download the desktop app for macOS, Windows, or Linux — there's no hosted Studio to sign into, it runs on your machine, against your files. Full details are in Install Jx Studio ."},{"id":"docs:start/first-project#2-create-a-project","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#2-create-a-project","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"2. Create a project","text":"In 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 . Then pick a template . Start from Blank for an empty project, or clone one of the starter sites — 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. Already 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 — then open the result in Studio.)"},{"id":"docs:start/first-project#3-manage-edit-design","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#3-manage-edit-design","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"3. Manage, edit, design","text":"Studio gives you a surface for each part of the job. Start in Manage to see your project — pages, components, content, and media — with live previews. Open a content page and switch to Edit 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 for the visual canvas: a live preview at every breakpoint, and a full CSS inspector for spacing, type, color, and hover states."},{"id":"docs:start/first-project#4-add-an-interaction","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#4-add-an-interaction","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"4. Add an interaction","text":"Websites do things. In Studio, interactivity comes from three panels working together — no separate \"code mode\" required: State — declare a value or a computed one (a counter, a total, a fetched list). Events — bind a handler to a click, input, or submit with the structured expression editor. Code — drop into the Monaco editor when a handler needs real JavaScript. Add a count to state, a button, and an onclick that increments it — you've built a reactive component. See Script & logic for the full toolkit."},{"id":"docs:start/first-project#5-commit-and-publish","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#5-commit-and-publish","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"5. Commit and publish","text":"When 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. Here'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 for the full flow."},{"id":"docs:start/first-project#whats-next","collection":"docs","slug":"start/first-project","url":"/docs/start/first-project/#whats-next","title":"Your first project","description":"Build and publish a website with Jx Studio — design visually, wire up interactivity, and commit to git, without hand-writing JSON.","heading":"What's next","text":"Using Studio: Manage · Edit · Design · Script & logic · Git & publish For developers: Site architecture · Component model · Spec overview"},{"id":"docs:start/install","collection":"docs","slug":"start/install","url":"/docs/start/install/","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"","text":"Get Studio Jx Studio is a desktop application. There is no hosted, sign-in version — you run it on your own machine, against your own files. Download the app Grab an installer for your platform from the latest release : Platform Download macOS (Apple Silicon) JxStudio.dmg macOS (Intel) JxStudio.dmg Windows (x64) JxStudio.msi Linux (x64) JxStudio.tar.gz The 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 has the same links plus checksums and release notes. Once installed, open Studio and either create a new project , open an existing folder , or clone a repository — see Your first project . Updating Studio 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. For developers: scaffolding from a terminal The 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 for bun create @jxsuite and the jx CLI. Next Ready to build something? Continue to Your first project ."},{"id":"docs:start/install#get-studio","collection":"docs","slug":"start/install","url":"/docs/start/install/#get-studio","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"Get Studio","text":"Jx Studio is a desktop application. There is no hosted, sign-in version — you run it on your own machine, against your own files."},{"id":"docs:start/install#download-the-app","collection":"docs","slug":"start/install","url":"/docs/start/install/#download-the-app","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"Download the app","text":"Grab an installer for your platform from the latest release : Platform Download macOS (Apple Silicon) JxStudio.dmg macOS (Intel) JxStudio.dmg Windows (x64) JxStudio.msi Linux (x64) JxStudio.tar.gz The 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 has the same links plus checksums and release notes. Once installed, open Studio and either create a new project , open an existing folder , or clone a repository — see Your first project ."},{"id":"docs:start/install#updating","collection":"docs","slug":"start/install","url":"/docs/start/install/#updating","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"Updating","text":"Studio 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."},{"id":"docs:start/install#for-developers-scaffolding-from-a-terminal","collection":"docs","slug":"start/install","url":"/docs/start/install/#for-developers-scaffolding-from-a-terminal","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"For developers: scaffolding from a terminal","text":"The 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 for bun create @jxsuite and the jx CLI."},{"id":"docs:start/install#next","collection":"docs","slug":"start/install","url":"/docs/start/install/#next","title":"Install Jx Studio","description":"Download the Jx Studio desktop app for macOS, Windows, or Linux — the only way to run the visual editor.","heading":"Next","text":"Ready to build something? Continue to Your first project ."},{"id":"docs:start/studio-tour","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/","title":"A tour of Jx Studio","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.","heading":"","text":"A tour of Jx Studio Everything 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. The toolbar The 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 . The mode switcher On 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 . The activity bar The 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. The left panel The 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 . The canvas The 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 , Edit mode , and Design mode . The tab strip Every 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 . The right panel Three 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 for Style and Script & logic for Events. The status bar The thin strip along the bottom shows what's selected and its position in the page structure, plus short confirmation messages like \"Saved\". Quick Access Press ⌘P (macOS) or Ctrl+P (Windows/Linux) anywhere to open a file by typing part of its name — see Quick Access . Next Learn each region in depth in The workspace Start writing in Edit mode or styling in Design mode Keep your hands on the keyboard with the shortcut reference"},{"id":"docs:start/studio-tour#a-tour-of-jx-studio","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#a-tour-of-jx-studio","title":"A tour of Jx Studio","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.","heading":"A tour of Jx Studio","text":"Everything 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."},{"id":"docs:start/studio-tour#the-toolbar","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-toolbar","title":"A tour of Jx Studio","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.","heading":"The toolbar","text":"The 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 ."},{"id":"docs:start/studio-tour#the-mode-switcher","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-mode-switcher","title":"A tour of Jx Studio","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.","heading":"The mode switcher","text":"On 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 ."},{"id":"docs:start/studio-tour#the-activity-bar","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-activity-bar","title":"A tour of Jx Studio","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.","heading":"The activity bar","text":"The 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."},{"id":"docs:start/studio-tour#the-left-panel","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-left-panel","title":"A tour of Jx Studio","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.","heading":"The left panel","text":"The 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 ."},{"id":"docs:start/studio-tour#the-canvas","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-canvas","title":"A tour of Jx Studio","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.","heading":"The canvas","text":"The 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 , Edit mode , and Design mode ."},{"id":"docs:start/studio-tour#the-tab-strip","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-tab-strip","title":"A tour of Jx Studio","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.","heading":"The tab strip","text":"Every 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 ."},{"id":"docs:start/studio-tour#the-right-panel","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-right-panel","title":"A tour of Jx Studio","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.","heading":"The right panel","text":"Three 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 for Style and Script & logic for Events."},{"id":"docs:start/studio-tour#the-status-bar","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#the-status-bar","title":"A tour of Jx Studio","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.","heading":"The status bar","text":"The thin strip along the bottom shows what's selected and its position in the page structure, plus short confirmation messages like \"Saved\"."},{"id":"docs:start/studio-tour#quick-access","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#quick-access","title":"A tour of Jx Studio","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.","heading":"Quick Access","text":"Press ⌘P (macOS) or Ctrl+P (Windows/Linux) anywhere to open a file by typing part of its name — see Quick Access ."},{"id":"docs:start/studio-tour#next","collection":"docs","slug":"start/studio-tour","url":"/docs/start/studio-tour/#next","title":"A tour of Jx Studio","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.","heading":"Next","text":"Learn each region in depth in The workspace Start writing in Edit mode or styling in Design mode Keep your hands on the keyboard with the shortcut reference"},{"id":"docs:start/videos","collection":"docs","slug":"start/videos","url":"/docs/start/videos/","title":"Video tutorials","description":"Video walkthroughs of Jx Studio are planned. Until they arrive, every topic — first project, design, collections, publishing — has a written tutorial.","heading":"","text":"Video tutorials Video 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. Planned walkthroughs Video What it will cover Status Your first project New project, starter templates, editing, and going live Coming soon Design basics The canvas, style inspector, tokens, and Stylebook Coming soon Content collections Content types, entries, and listing pages Coming soon Publishing GitHub, choosing a host, and the publish flow Coming soon In the meantime Every planned topic already has a written counterpart you can follow today: Your first project — the end-to-end tutorial, from starter template to published site Your first component and Your first collection — the two workflows you'll use most A tour of Jx Studio — the workspace, mode by mode Design mode and Publish — the full guides behind the design and publishing videos The written docs stay the reference either way — videos will complement them, not replace them."},{"id":"docs:start/videos#video-tutorials","collection":"docs","slug":"start/videos","url":"/docs/start/videos/#video-tutorials","title":"Video tutorials","description":"Video walkthroughs of Jx Studio are planned. Until they arrive, every topic — first project, design, collections, publishing — has a written tutorial.","heading":"Video tutorials","text":"Video 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."},{"id":"docs:start/videos#planned-walkthroughs","collection":"docs","slug":"start/videos","url":"/docs/start/videos/#planned-walkthroughs","title":"Video tutorials","description":"Video walkthroughs of Jx Studio are planned. Until they arrive, every topic — first project, design, collections, publishing — has a written tutorial.","heading":"Planned walkthroughs","text":"Video What it will cover Status Your first project New project, starter templates, editing, and going live Coming soon Design basics The canvas, style inspector, tokens, and Stylebook Coming soon Content collections Content types, entries, and listing pages Coming soon Publishing GitHub, choosing a host, and the publish flow Coming soon"},{"id":"docs:start/videos#in-the-meantime","collection":"docs","slug":"start/videos","url":"/docs/start/videos/#in-the-meantime","title":"Video tutorials","description":"Video walkthroughs of Jx Studio are planned. Until they arrive, every topic — first project, design, collections, publishing — has a written tutorial.","heading":"In the meantime","text":"Every planned topic already has a written counterpart you can follow today: Your first project — the end-to-end tutorial, from starter template to published site Your first component and Your first collection — the two workflows you'll use most A tour of Jx Studio — the workspace, mode by mode Design mode and Publish — the full guides behind the design and publishing videos The written docs stay the reference either way — videos will complement them, not replace them."},{"id":"docs:studio/ai","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/","title":"AI assistant","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.","heading":"","text":"AI assistant Studio 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. Open 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. What it can do 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 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. 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. 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 . In 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. Each 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. Connect a provider The first time you open the sidebar (and any time no key is stored), the chat is replaced by the AI provider key form: Paste an API key. Any OpenAI-compatible key works — OpenAI itself, a compatible hosted provider, or a local model server. Pick a Model . Click Fetch models to list what your key can use, or type a model ID directly. 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 ). Click Save . To 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. The 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. What leaves your machine Nothing 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: your message and the rest of the conversation, any context you attached (the current page reference, the selected element), the full contents of the page open on the canvas, a summary of the open project: its name and settings, component names, and file paths, whatever files the assistant reads while working on your request. Requests 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. Learn the two surfaces The AI sidebar — the chat itself: attaching context, watching edits land, chat history, and reviewing or undoing what the assistant changed. Document assistant — how the assistant works when a page is open on the canvas, and when to use that instead of project-wide edits. Next Create a project for the assistant to work in: New Project The state entries it can add for you are explained in the State panel Working the same project from outside Studio — with a coding agent, or in CI: Working with agents"},{"id":"docs:studio/ai#ai-assistant","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#ai-assistant","title":"AI assistant","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.","heading":"AI assistant","text":"Studio 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. Open 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."},{"id":"docs:studio/ai#what-it-can-do","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#what-it-can-do","title":"AI assistant","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.","heading":"What it can do","text":"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 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. 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. 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 . In 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. Each 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."},{"id":"docs:studio/ai#connect-a-provider","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#connect-a-provider","title":"AI assistant","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.","heading":"Connect a provider","text":"The first time you open the sidebar (and any time no key is stored), the chat is replaced by the AI provider key form: Paste an API key. Any OpenAI-compatible key works — OpenAI itself, a compatible hosted provider, or a local model server. Pick a Model . Click Fetch models to list what your key can use, or type a model ID directly. 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 ). Click Save . To 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. The 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."},{"id":"docs:studio/ai#what-leaves-your-machine","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#what-leaves-your-machine","title":"AI assistant","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.","heading":"What leaves your machine","text":"Nothing 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: your message and the rest of the conversation, any context you attached (the current page reference, the selected element), the full contents of the page open on the canvas, a summary of the open project: its name and settings, component names, and file paths, whatever files the assistant reads while working on your request. Requests 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."},{"id":"docs:studio/ai#learn-the-two-surfaces","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#learn-the-two-surfaces","title":"AI assistant","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.","heading":"Learn the two surfaces","text":"The AI sidebar — the chat itself: attaching context, watching edits land, chat history, and reviewing or undoing what the assistant changed. Document assistant — how the assistant works when a page is open on the canvas, and when to use that instead of project-wide edits."},{"id":"docs:studio/ai#next","collection":"docs","slug":"studio/ai","url":"/docs/studio/ai/#next","title":"AI assistant","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.","heading":"Next","text":"Create a project for the assistant to work in: New Project The state entries it can add for you are explained in the State panel Working the same project from outside Studio — with a coding agent, or in CI: Working with agents"},{"id":"docs:studio/data","collection":"docs","slug":"studio/data","url":"/docs/studio/data/","title":"Databases","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.","heading":"","text":"Databases Your 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. You 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. Where it lives Open the Settings gear at the bottom of the activity bar. Projects with the connector extension enabled get two extra sections in the list: Connections — which database (or databases) the project talks to: Connections . Data Tables — the tables inside them: fields, ids, and access rules: Data tables . Both 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 . The 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 . From empty to live data Add a connection — name where the data lives. A new connection starts as SQLite, a database file inside your project that works immediately. 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. Browse in the grid — insert, edit, and delete rows in a paged spreadsheet view. Pages then use the data through the State panel : 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 , and a form's submit event can point straight at a table insert. Local and deployed While you build, everything runs against local files: A SQLite connection is a file at .jx/data/<name>.sqlite inside your project — created on first use. 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. A Supabase connection is Postgres — a hosted service you connect to with a URL. Anything 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 . Availability The database console needs a Studio backend that can reach databases: the local dev server and the desktop app both can. On a backend without database access the sections still show your definitions, but the Test, Push, and grid actions stay hidden. The command-line equivalent of the push button is jx db push — same plan, same additive rules — documented in the CLI reference . Next Connections — add SQLite, D1, or Supabase Data tables — fields, relationships, permissions, and Push Schema Data grid — browse and edit the rows Auth and secrets — sign-ins, access rules, and the secret store"},{"id":"docs:studio/data#databases","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#databases","title":"Databases","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.","heading":"Databases","text":"Your 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. You 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."},{"id":"docs:studio/data#where-it-lives","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#where-it-lives","title":"Databases","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.","heading":"Where it lives","text":"Open the Settings gear at the bottom of the activity bar. Projects with the connector extension enabled get two extra sections in the list: Connections — which database (or databases) the project talks to: Connections . Data Tables — the tables inside them: fields, ids, and access rules: Data tables . Both 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 . The 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 ."},{"id":"docs:studio/data#from-empty-to-live-data","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#from-empty-to-live-data","title":"Databases","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.","heading":"From empty to live data","text":"Add a connection — name where the data lives. A new connection starts as SQLite, a database file inside your project that works immediately. 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. Browse in the grid — insert, edit, and delete rows in a paged spreadsheet view. Pages then use the data through the State panel : 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 , and a form's submit event can point straight at a table insert."},{"id":"docs:studio/data#local-and-deployed","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#local-and-deployed","title":"Databases","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.","heading":"Local and deployed","text":"While you build, everything runs against local files: A SQLite connection is a file at .jx/data/<name>.sqlite inside your project — created on first use. 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. A Supabase connection is Postgres — a hosted service you connect to with a URL. Anything 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 ."},{"id":"docs:studio/data#availability","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#availability","title":"Databases","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.","heading":"Availability","text":"The database console needs a Studio backend that can reach databases: the local dev server and the desktop app both can. On a backend without database access the sections still show your definitions, but the Test, Push, and grid actions stay hidden. The command-line equivalent of the push button is jx db push — same plan, same additive rules — documented in the CLI reference ."},{"id":"docs:studio/data#next","collection":"docs","slug":"studio/data","url":"/docs/studio/data/#next","title":"Databases","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.","heading":"Next","text":"Connections — add SQLite, D1, or Supabase Data tables — fields, relationships, permissions, and Push Schema Data grid — browse and edit the rows Auth and secrets — sign-ins, access rules, and the secret store"},{"id":"docs:studio/design","collection":"docs","slug":"studio/design","url":"/docs/studio/design/","title":"Design mode","description":"Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.","heading":"","text":"Design mode Design 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. A canvas per breakpoint Instead 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 for the controls. Breakpoints themselves — where they come from and how overrides cascade — are covered in Breakpoints . The panels around the canvas Design mode is the canvas plus a set of panels, each with its own page: Layers panel — the page's structure as a tree: select, rename, reorder, and act on any element. Elements panel — insert HTML elements and your components by click or drag. Properties panel — attributes, link targets, component props, and page settings for the selection. Style inspector — visual CSS controls for the selection, section by section. Hover states and selectors — style :hover , :focus , and selectors of your own. Design tokens — name your colors, fonts, and sizes once and reuse them everywhere. Stylebook — set the default look of every heading, button, and link in one catalog. Working with components — turn a selection into a reusable component, wire props and slots. Repeaters — turn one element into a repeating, data-bound list. Next Learn the shared canvas interactions — selection, drag and drop, the context menu — in The canvas Make it interactive in Script & logic The underlying style format is documented in Styling"},{"id":"docs:studio/design#design-mode","collection":"docs","slug":"studio/design","url":"/docs/studio/design/#design-mode","title":"Design mode","description":"Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.","heading":"Design mode","text":"Design 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."},{"id":"docs:studio/design#a-canvas-per-breakpoint","collection":"docs","slug":"studio/design","url":"/docs/studio/design/#a-canvas-per-breakpoint","title":"Design mode","description":"Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.","heading":"A canvas per breakpoint","text":"Instead 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 for the controls. Breakpoints themselves — where they come from and how overrides cascade — are covered in Breakpoints ."},{"id":"docs:studio/design#the-panels-around-the-canvas","collection":"docs","slug":"studio/design","url":"/docs/studio/design/#the-panels-around-the-canvas","title":"Design mode","description":"Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.","heading":"The panels around the canvas","text":"Design mode is the canvas plus a set of panels, each with its own page: Layers panel — the page's structure as a tree: select, rename, reorder, and act on any element. Elements panel — insert HTML elements and your components by click or drag. Properties panel — attributes, link targets, component props, and page settings for the selection. Style inspector — visual CSS controls for the selection, section by section. Hover states and selectors — style :hover , :focus , and selectors of your own. Design tokens — name your colors, fonts, and sizes once and reuse them everywhere. Stylebook — set the default look of every heading, button, and link in one catalog. Working with components — turn a selection into a reusable component, wire props and slots. Repeaters — turn one element into a repeating, data-bound list."},{"id":"docs:studio/design#next","collection":"docs","slug":"studio/design","url":"/docs/studio/design/#next","title":"Design mode","description":"Design mode in Jx Studio: a live canvas for every breakpoint plus panels for structure, style, tokens, components, and repeaters.","heading":"Next","text":"Learn the shared canvas interactions — selection, drag and drop, the context menu — in The canvas Make it interactive in Script & logic The underlying style format is documented in Styling"},{"id":"docs:studio/desktop","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/","title":"The desktop app","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.","heading":"","text":"The desktop app The 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. Install and update Download the installer for macOS, Windows, or Linux from the install page — it covers each platform's package and what to expect on first launch. Once 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. To 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. Open a project A fresh window greets you with the welcome screen : create a new project , open an existing one, or pick from your recent projects — the recent list is shared across all windows. To open an existing project: Choose File > Open Project… or press ⌘O (macOS) / Ctrl+O (Windows/Linux) — or click Open Project... on the welcome screen. A native file dialog opens. Select the project's project.json file — the file that marks a folder as a Jx project. The project opens in its own window, with the folder around project.json as the project root. You 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. project.json is the project's settings file — Studio creates it for every new project. What's inside is described in Project settings . One window per project Each 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. File > New Window ( ⇧⌘N / Ctrl+Shift+N ) opens a fresh welcome window when you want to start something else. How it differs from Studio in the browser Studio itself is identical in both — every page in this documentation applies to each. The differences are around the edges: Nothing to run. In the browser, Studio is served by a local dev server you start from a terminal (see The dev server ). The desktop app carries its own backend — launch it like any other application. Native dialogs. Opening projects and picking folders use your operating system's file dialogs instead of the browser's folder picker. Windows, menus, and file associations. One window per project, a real File menu, and project.json opening from the file manager. Built-in updates. The app updates itself in the background; a dev-server setup updates with your package manager. The AI assistant, publishing, and everything on the canvas behave the same in both. Platform notes Installers are provided for macOS (Apple Silicon and Intel), Windows (x64), and Linux (x64) — see the install page 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. Next Get it installed: Install Jx Studio Build something: Your first project Find your way around: Studio tour"},{"id":"docs:studio/desktop#the-desktop-app","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#the-desktop-app","title":"The desktop app","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.","heading":"The desktop app","text":"The 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."},{"id":"docs:studio/desktop#install-and-update","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#install-and-update","title":"The desktop app","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.","heading":"Install and update","text":"Download the installer for macOS, Windows, or Linux from the install page — it covers each platform's package and what to expect on first launch. Once 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. To 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."},{"id":"docs:studio/desktop#open-a-project","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#open-a-project","title":"The desktop app","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.","heading":"Open a project","text":"A fresh window greets you with the welcome screen : create a new project , open an existing one, or pick from your recent projects — the recent list is shared across all windows. To open an existing project: Choose File > Open Project… or press ⌘O (macOS) / Ctrl+O (Windows/Linux) — or click Open Project... on the welcome screen. A native file dialog opens. Select the project's project.json file — the file that marks a folder as a Jx project. The project opens in its own window, with the folder around project.json as the project root. You 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. project.json is the project's settings file — Studio creates it for every new project. What's inside is described in Project settings ."},{"id":"docs:studio/desktop#one-window-per-project","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#one-window-per-project","title":"The desktop app","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.","heading":"One window per project","text":"Each 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. File > New Window ( ⇧⌘N / Ctrl+Shift+N ) opens a fresh welcome window when you want to start something else."},{"id":"docs:studio/desktop#how-it-differs-from-studio-in-the-browser","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#how-it-differs-from-studio-in-the-browser","title":"The desktop app","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.","heading":"How it differs from Studio in the browser","text":"Studio itself is identical in both — every page in this documentation applies to each. The differences are around the edges: Nothing to run. In the browser, Studio is served by a local dev server you start from a terminal (see The dev server ). The desktop app carries its own backend — launch it like any other application. Native dialogs. Opening projects and picking folders use your operating system's file dialogs instead of the browser's folder picker. Windows, menus, and file associations. One window per project, a real File menu, and project.json opening from the file manager. Built-in updates. The app updates itself in the background; a dev-server setup updates with your package manager. The AI assistant, publishing, and everything on the canvas behave the same in both."},{"id":"docs:studio/desktop#platform-notes","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#platform-notes","title":"The desktop app","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.","heading":"Platform notes","text":"Installers are provided for macOS (Apple Silicon and Intel), Windows (x64), and Linux (x64) — see the install page 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."},{"id":"docs:studio/desktop#next","collection":"docs","slug":"studio/desktop","url":"/docs/studio/desktop/#next","title":"The desktop app","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.","heading":"Next","text":"Get it installed: Install Jx Studio Build something: Your first project Find your way around: Studio tour"},{"id":"docs:studio/editing","collection":"docs","slug":"studio/editing","url":"/docs/studio/editing/","title":"Edit mode","description":"Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.","heading":"","text":"Edit mode Edit 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. Markdown 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 . What lives here Writing and formatting — typing on the page, paragraphs, the inline formatting toolbar, links, pasting, and editing text inside components. Slash commands — type / to insert headings, lists, images, tables, and more without leaving the keyboard. Frontmatter and page metadata — the Document activity's forms for titles, descriptions, social-share cards, and your content type's fields. Grid mode — a spreadsheet view for whole collections, CSV files, and page metadata, with one batched Save. It's just Markdown Everything 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. Next Structure and style the page in Design mode Organize the content files themselves in Browse your project"},{"id":"docs:studio/editing#edit-mode","collection":"docs","slug":"studio/editing","url":"/docs/studio/editing/#edit-mode","title":"Edit mode","description":"Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.","heading":"Edit mode","text":"Edit 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. Markdown 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 ."},{"id":"docs:studio/editing#what-lives-here","collection":"docs","slug":"studio/editing","url":"/docs/studio/editing/#what-lives-here","title":"Edit mode","description":"Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.","heading":"What lives here","text":"Writing and formatting — typing on the page, paragraphs, the inline formatting toolbar, links, pasting, and editing text inside components. Slash commands — type / to insert headings, lists, images, tables, and more without leaving the keyboard. Frontmatter and page metadata — the Document activity's forms for titles, descriptions, social-share cards, and your content type's fields. Grid mode — a spreadsheet view for whole collections, CSV files, and page metadata, with one batched Save."},{"id":"docs:studio/editing#its-just-markdown","collection":"docs","slug":"studio/editing","url":"/docs/studio/editing/#its-just-markdown","title":"Edit mode","description":"Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.","heading":"It's just Markdown","text":"Everything 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."},{"id":"docs:studio/editing#next","collection":"docs","slug":"studio/editing","url":"/docs/studio/editing/#next","title":"Edit mode","description":"Edit mode in Jx Studio: write on the real page with inline formatting, slash commands, and metadata forms — saved as clean Markdown.","heading":"Next","text":"Structure and style the page in Design mode Organize the content files themselves in Browse your project"},{"id":"docs:studio/interface","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"","text":"The workspace The 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 . Toolbar The toolbar runs across the top of the window. On the left: 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 . Manage opens the project browser — see Browse your project . Publish opens the publish panel — see Git & publish . 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 . Undo and Redo step through the active file's edit history. In the middle, the Search files… field opens Quick Access . When your project has new commits waiting on the remote, a Sync Project button appears here to pull them. On the right: The mode switcher — Edit , Design , Grid , Code , Stylebook — changes how the canvas presents the open file. See Modes and the preview toggle . A toggle collapses and restores the right panel. A chat icon toggles the Assistant sidebar. In the desktop app, the window's minimize, maximize, and close controls also live in this row. Activity bar The vertical icon strip on the far left picks what the left panel shows. From top to bottom: 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. 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. Imports — the components and packages the open document (or project) pulls in. Elements — the palette of elements and components you can insert, organized by category with a search filter. State — the values, data sources, and functions the open component knows about. See Script & logic . Data — the live, resolved values of that state as the page runs. Document — page-level settings: the title, the tags that go in the page head (description, social previews), and the layout the page uses. Source Control — the built-in git client. A badge on the icon counts changed files. See Git & publish . Clicking 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. Left panel The 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. Right panel The right panel inspects whatever is selected on the canvas, in three tabs: Properties — the selected element's settings: its text, links, images, and component options. Events — what the element does on click, input, submit, and other interactions. See Script & logic . Style — the visual inspector: spacing, typography, color, layout, and more. See Design mode . Resize it by dragging its inner edge, or collapse it entirely with the toggle at the right end of the toolbar. Studio remembers your layout — panel widths and which panels are collapsed carry over to your next session. Canvas The 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 . Tab strip and tab bar Two rows sit between the toolbar and the canvas: The tab strip shows one tab per open file, with a dot marking unsaved changes. 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. Both are covered in Tabs and files . Status bar The strip along the bottom of the window shows: 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. Content Mode when the open file is a content document. In Stylebook mode, the style rule you're editing. Short, temporary messages such as \"Saved\" after a save. Next Meet the five canvas modes in Modes and the preview toggle Work directly on the page in The canvas Learn the keyboard shortcuts"},{"id":"docs:studio/interface#the-workspace","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#the-workspace","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"The workspace","text":"The 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 ."},{"id":"docs:studio/interface#toolbar","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#toolbar","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Toolbar","text":"The toolbar runs across the top of the window. On the left: 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 . Manage opens the project browser — see Browse your project . Publish opens the publish panel — see Git & publish . 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 . Undo and Redo step through the active file's edit history. In the middle, the Search files… field opens Quick Access . When your project has new commits waiting on the remote, a Sync Project button appears here to pull them. On the right: The mode switcher — Edit , Design , Grid , Code , Stylebook — changes how the canvas presents the open file. See Modes and the preview toggle . A toggle collapses and restores the right panel. A chat icon toggles the Assistant sidebar. In the desktop app, the window's minimize, maximize, and close controls also live in this row."},{"id":"docs:studio/interface#activity-bar","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#activity-bar","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Activity bar","text":"The vertical icon strip on the far left picks what the left panel shows. From top to bottom: 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. 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. Imports — the components and packages the open document (or project) pulls in. Elements — the palette of elements and components you can insert, organized by category with a search filter. State — the values, data sources, and functions the open component knows about. See Script & logic . Data — the live, resolved values of that state as the page runs. Document — page-level settings: the title, the tags that go in the page head (description, social previews), and the layout the page uses. Source Control — the built-in git client. A badge on the icon counts changed files. See Git & publish . Clicking 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."},{"id":"docs:studio/interface#left-panel","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#left-panel","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Left panel","text":"The 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."},{"id":"docs:studio/interface#right-panel","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#right-panel","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Right panel","text":"The right panel inspects whatever is selected on the canvas, in three tabs: Properties — the selected element's settings: its text, links, images, and component options. Events — what the element does on click, input, submit, and other interactions. See Script & logic . Style — the visual inspector: spacing, typography, color, layout, and more. See Design mode . Resize it by dragging its inner edge, or collapse it entirely with the toggle at the right end of the toolbar. Studio remembers your layout — panel widths and which panels are collapsed carry over to your next session."},{"id":"docs:studio/interface#canvas","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#canvas","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Canvas","text":"The 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 ."},{"id":"docs:studio/interface#tab-strip-and-tab-bar","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#tab-strip-and-tab-bar","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Tab strip and tab bar","text":"Two rows sit between the toolbar and the canvas: The tab strip shows one tab per open file, with a dot marking unsaved changes. 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. Both are covered in Tabs and files ."},{"id":"docs:studio/interface#status-bar","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#status-bar","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Status bar","text":"The strip along the bottom of the window shows: 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. Content Mode when the open file is a content document. In Stylebook mode, the style rule you're editing. Short, temporary messages such as \"Saved\" after a save."},{"id":"docs:studio/interface#next","collection":"docs","slug":"studio/interface","url":"/docs/studio/interface/#next","title":"The workspace","description":"Every region of the Jx Studio window in detail — toolbar, activity bar, left and right panels, canvas, tab strip, and status bar.","heading":"Next","text":"Meet the five canvas modes in Modes and the preview toggle Work directly on the page in The canvas Learn the keyboard shortcuts"},{"id":"docs:studio/logic","collection":"docs","slug":"studio/logic","url":"/docs/studio/logic/","title":"Logic","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.","heading":"","text":"Logic There is no single \"logic mode\" in Studio. Interactivity comes from a few focused surfaces you move between, and each one answers a different question: State — an activity in the left panel. What does this page or component know? Declare values, computed entries, data sources, and functions here. Data — the activity right below it. What are those values right now? It shows the live, resolved data as the page runs. Events — a tab in the right panel. What happens when a visitor clicks or types? Bind behavior to the selected element here. 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 opens when a formula deserves the whole canvas. Code — the escape hatch. A real code editor for function bodies, and a Code canvas mode that shows any file as raw source. Together 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. A counter in 60 seconds Here is the shape of the workflow, end to end: 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. 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. 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 . 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. No files were written by hand: Studio stored the value, the binding, and the handler inside the component's own JSON file. When each surface applies Reach for State first — every other surface refers back to what you declare there. Use Events whenever behavior belongs to one element (\"this button submits\", \"this field filters the list\"). Use formulas for values that are calculated rather than typed, and statements when a handler needs several steps in order. Open Data whenever something looks wrong — it shows what the page actually sees, not what you hoped it sees. Drop into Code when a function outgrows the structured editors, or when you want to read exactly what Studio wrote. Next Declare your first values in the State panel Wire data in from files, APIs, and the browser with Data sources The reactive model underneath it all is documented in Reactivity"},{"id":"docs:studio/logic#logic","collection":"docs","slug":"studio/logic","url":"/docs/studio/logic/#logic","title":"Logic","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.","heading":"Logic","text":"There is no single \"logic mode\" in Studio. Interactivity comes from a few focused surfaces you move between, and each one answers a different question: State — an activity in the left panel. What does this page or component know? Declare values, computed entries, data sources, and functions here. Data — the activity right below it. What are those values right now? It shows the live, resolved data as the page runs. Events — a tab in the right panel. What happens when a visitor clicks or types? Bind behavior to the selected element here. 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 opens when a formula deserves the whole canvas. Code — the escape hatch. A real code editor for function bodies, and a Code canvas mode that shows any file as raw source. Together 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."},{"id":"docs:studio/logic#a-counter-in-60-seconds","collection":"docs","slug":"studio/logic","url":"/docs/studio/logic/#a-counter-in-60-seconds","title":"Logic","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.","heading":"A counter in 60 seconds","text":"Here is the shape of the workflow, end to end: 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. 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. 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 . 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. No files were written by hand: Studio stored the value, the binding, and the handler inside the component's own JSON file."},{"id":"docs:studio/logic#when-each-surface-applies","collection":"docs","slug":"studio/logic","url":"/docs/studio/logic/#when-each-surface-applies","title":"Logic","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.","heading":"When each surface applies","text":"Reach for State first — every other surface refers back to what you declare there. Use Events whenever behavior belongs to one element (\"this button submits\", \"this field filters the list\"). Use formulas for values that are calculated rather than typed, and statements when a handler needs several steps in order. Open Data whenever something looks wrong — it shows what the page actually sees, not what you hoped it sees. Drop into Code when a function outgrows the structured editors, or when you want to read exactly what Studio wrote."},{"id":"docs:studio/logic#next","collection":"docs","slug":"studio/logic","url":"/docs/studio/logic/#next","title":"Logic","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.","heading":"Next","text":"Declare your first values in the State panel Wire data in from files, APIs, and the browser with Data sources The reactive model underneath it all is documented in Reactivity"},{"id":"docs:studio/projects","collection":"docs","slug":"studio/projects","url":"/docs/studio/projects/","title":"Projects","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.","heading":"","text":"Projects A 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 ); the project you edit is always just files. What's in the folder Studio and the folder are two views of the same thing. Each area of your project maps to a subfolder: On disk What it holds In Studio pages/ One file per page of your site Pages in the Manage view layouts/ Shared page shells — headers, footers, wrappers Layouts components/ Reusable building blocks Components content/ Posts, entries, and other collection content Content public/ Images, video, fonts, and other media Media project.json The site's settings — name, URL, design tokens Project settings What each kind of file is for — and when to reach for which — is covered in Pages, layouts, and components . The full folder anatomy is documented in Site architecture . Get a project There are three ways to end up with a project open in Studio, all available from the Welcome screen : 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 . 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. 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. Because 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. Next Create a project — the New Project modal, field by field Browse your project — the Manage view with live previews Publish — commit your project and put it live"},{"id":"docs:studio/projects#projects","collection":"docs","slug":"studio/projects","url":"/docs/studio/projects/#projects","title":"Projects","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.","heading":"Projects","text":"A 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 ); the project you edit is always just files."},{"id":"docs:studio/projects#whats-in-the-folder","collection":"docs","slug":"studio/projects","url":"/docs/studio/projects/#whats-in-the-folder","title":"Projects","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.","heading":"What's in the folder","text":"Studio and the folder are two views of the same thing. Each area of your project maps to a subfolder: On disk What it holds In Studio pages/ One file per page of your site Pages in the Manage view layouts/ Shared page shells — headers, footers, wrappers Layouts components/ Reusable building blocks Components content/ Posts, entries, and other collection content Content public/ Images, video, fonts, and other media Media project.json The site's settings — name, URL, design tokens Project settings What each kind of file is for — and when to reach for which — is covered in Pages, layouts, and components . The full folder anatomy is documented in Site architecture ."},{"id":"docs:studio/projects#get-a-project","collection":"docs","slug":"studio/projects","url":"/docs/studio/projects/#get-a-project","title":"Projects","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.","heading":"Get a project","text":"There are three ways to end up with a project open in Studio, all available from the Welcome screen : 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 . 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. 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. Because 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."},{"id":"docs:studio/projects#next","collection":"docs","slug":"studio/projects","url":"/docs/studio/projects/#next","title":"Projects","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.","heading":"Next","text":"Create a project — the New Project modal, field by field Browse your project — the Manage view with live previews Publish — commit your project and put it live"},{"id":"docs:studio/publish","collection":"docs","slug":"studio/publish","url":"/docs/studio/publish/","title":"Publish","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.","heading":"","text":"Publish Shipping 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. How it goes live In the Source Control panel, you write a message and Commit and sync — Studio records the change and pushes it to your repository. The push wakes your host (or CI), which builds the project into plain HTML, CSS, and a little JavaScript. The host serves that output from a CDN. Your site is live. The deployment adapter you picked when creating the project — Static, Cloudflare Pages, Node, or Bun, with Cloudflare Workers also on offer in Project 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. Every 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 lists what each adapter emits. Because every Jx file is plain JSON or Markdown, each publish is a clean, reviewable set of changes — no binary blobs, no database dumps. The two surfaces Source control — the built-in git client: review and stage changes, commit and sync, branches, pulling, and history. GitHub — connect your GitHub account and publish a brand-new project as a repository in one flow. Next Understand the build and routing in Site architecture"},{"id":"docs:studio/publish#publish","collection":"docs","slug":"studio/publish","url":"/docs/studio/publish/#publish","title":"Publish","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.","heading":"Publish","text":"Shipping 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."},{"id":"docs:studio/publish#how-it-goes-live","collection":"docs","slug":"studio/publish","url":"/docs/studio/publish/#how-it-goes-live","title":"Publish","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.","heading":"How it goes live","text":"In the Source Control panel, you write a message and Commit and sync — Studio records the change and pushes it to your repository. The push wakes your host (or CI), which builds the project into plain HTML, CSS, and a little JavaScript. The host serves that output from a CDN. Your site is live. The deployment adapter you picked when creating the project — Static, Cloudflare Pages, Node, or Bun, with Cloudflare Workers also on offer in Project 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. Every 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 lists what each adapter emits. Because every Jx file is plain JSON or Markdown, each publish is a clean, reviewable set of changes — no binary blobs, no database dumps."},{"id":"docs:studio/publish#the-two-surfaces","collection":"docs","slug":"studio/publish","url":"/docs/studio/publish/#the-two-surfaces","title":"Publish","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.","heading":"The two surfaces","text":"Source control — the built-in git client: review and stage changes, commit and sync, branches, pulling, and history. GitHub — connect your GitHub account and publish a brand-new project as a repository in one flow."},{"id":"docs:studio/publish#next","collection":"docs","slug":"studio/publish","url":"/docs/studio/publish/#next","title":"Publish","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.","heading":"Next","text":"Understand the build and routing in Site architecture"},{"id":"docs:extending/contributing/ai-evals","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/","title":"AI assistant evals","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.","heading":"","text":"AI assistant evals Studio's AI assistant 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. 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. The invariant 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. The 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. Golden tasks Each task in evals/tasks/*.json is one isolated, unambiguous specification: { \"id\" : \"counter-button\" , \"prompt\" : \"Add a button that increments a counter, and show the current count above it. Wire up the reactive state.\" , \"tags\" : [ \"intent\" , \"state\" , \"binding\" ], \"intent\" : [ \"declares numeric state for the count\" , \"a button increments the count on click\" , \"the count is shown via a ${...} template binding\" ], \"initialDoc\" : { \"tagName\" : \"counter-widget\" , \"state\" : { \"count\" : { \"type\" : \"integer\" , \"default\" : 0 } }, \"children\" : [] } } 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. Running it # Whole suite, k=3 trials per task (the default) OPENAI_API_KEY = sk-… bun run eval # One task, single trial — a smoke check OPENAI_API_KEY = sk-… bun run eval --tasks counter-button --k 1 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. Each run writes a timestamped directory under evals/runs/ , which is gitignored: 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. results.json — the same numbers, machine-readable, minus the transcripts. 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. 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. The two graders 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. 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. A 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. pass@k and pass^k Models are non-deterministic, so a single trial per task measures luck. Each task runs k times (3 by default) and the scoreboard reports both: pass@k — at least one of the k trials passed. Capability: can the assistant do this at all? pass^k — all k trials passed. Reliability: can a user count on it? passRate (passes ÷ k) sits between them and is what the mean and the Δrate column are computed from. The merge rule Run bun run eval and note the baseline mean pass-rate; collect the failing transcripts. 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. Re-run. Keep the change only if the mean pass-rate improves and regressed is empty. Otherwise discard it. The 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. Never 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. The headless rubric harness 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. Out of scope The 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. Related AI assistant — what the harness is measuring, from the user's side Working in the monorepo — the testing policy the rest of the repo follows Working with agents — external agents on a Jx project, and the checks that hold them honest"},{"id":"docs:extending/contributing/ai-evals#ai-assistant-evals","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#ai-assistant-evals","title":"AI assistant evals","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.","heading":"AI assistant evals","text":"Studio's AI assistant 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. 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."},{"id":"docs:extending/contributing/ai-evals#the-invariant","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#the-invariant","title":"AI assistant evals","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.","heading":"The invariant","text":"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. The 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."},{"id":"docs:extending/contributing/ai-evals#golden-tasks","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#golden-tasks","title":"AI assistant evals","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.","heading":"Golden tasks","text":"Each task in evals/tasks/*.json is one isolated, unambiguous specification: { \"id\" : \"counter-button\" , \"prompt\" : \"Add a button that increments a counter, and show the current count above it. Wire up the reactive state.\" , \"tags\" : [ \"intent\" , \"state\" , \"binding\" ], \"intent\" : [ \"declares numeric state for the count\" , \"a button increments the count on click\" , \"the count is shown via a ${...} template binding\" ], \"initialDoc\" : { \"tagName\" : \"counter-widget\" , \"state\" : { \"count\" : { \"type\" : \"integer\" , \"default\" : 0 } }, \"children\" : [] } } 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."},{"id":"docs:extending/contributing/ai-evals#running-it","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#running-it","title":"AI assistant evals","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.","heading":"Running it","text":"# Whole suite, k=3 trials per task (the default) OPENAI_API_KEY = sk-… bun run eval # One task, single trial — a smoke check OPENAI_API_KEY = sk-… bun run eval --tasks counter-button --k 1 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. Each run writes a timestamped directory under evals/runs/ , which is gitignored: 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. results.json — the same numbers, machine-readable, minus the transcripts. 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. 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."},{"id":"docs:extending/contributing/ai-evals#the-two-graders","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#the-two-graders","title":"AI assistant evals","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.","heading":"The two graders","text":"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. 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. A 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."},{"id":"docs:extending/contributing/ai-evals#passk-and-passk","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#passk-and-passk","title":"AI assistant evals","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.","heading":"pass@k and pass^k","text":"Models are non-deterministic, so a single trial per task measures luck. Each task runs k times (3 by default) and the scoreboard reports both: pass@k — at least one of the k trials passed. Capability: can the assistant do this at all? pass^k — all k trials passed. Reliability: can a user count on it? passRate (passes ÷ k) sits between them and is what the mean and the Δrate column are computed from."},{"id":"docs:extending/contributing/ai-evals#the-merge-rule","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#the-merge-rule","title":"AI assistant evals","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.","heading":"The merge rule","text":"Run bun run eval and note the baseline mean pass-rate; collect the failing transcripts. 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. Re-run. Keep the change only if the mean pass-rate improves and regressed is empty. Otherwise discard it. The 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. Never 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."},{"id":"docs:extending/contributing/ai-evals#the-headless-rubric-harness","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#the-headless-rubric-harness","title":"AI assistant evals","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.","heading":"The headless rubric harness","text":"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."},{"id":"docs:extending/contributing/ai-evals#out-of-scope","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#out-of-scope","title":"AI assistant evals","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.","heading":"Out of scope","text":"The 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."},{"id":"docs:extending/contributing/ai-evals#related","collection":"docs","slug":"extending/contributing/ai-evals","url":"/docs/extending/contributing/ai-evals/#related","title":"AI assistant evals","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.","heading":"Related","text":"AI assistant — what the harness is measuring, from the user's side Working in the monorepo — the testing policy the rest of the repo follows Working with agents — external agents on a Jx project, and the checks that hold them honest"},{"id":"docs:extending/contributing/docs","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"","text":"Contributing to these docs The 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/ ). Adding a page Create the Markdown file under the right section ( start/ , studio/ , framework/ , extending/ ). Add frontmatter: title (sentence case, no site suffix) and description (≤155 characters). 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). Run bun run docs:check before pushing. Optional frontmatter associates a page with its sources, validated by CI: spec : - spec.md#19.4 # a specs/ file and numbered section code : - packages/runtime/src/runtime.ts # repo paths that must exist In 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. These 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. Your 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 . Voice and style Second person, present tense, imperative steps: \"Click New Project \", not \"The New Project button can be clicked\". Sentence-case headings; one H1 matching the frontmatter title. Studio and Start pages assume no code knowledge. Code belongs in Framework/Extending pages — or inside a :::doc-note aside. 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. Chevron click paths for navigation chains: Settings > CSS Variables . American English, short paragraphs, no marketing superlatives. Canonical UI names Never 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 . Callouts Three container directives render as styled asides: :::doc-note Neutral context — including \"behind the scenes\" notes naming what Studio writes to disk. ::: :::doc-tip Shortcuts and good practices. ::: :::doc-warning Data loss or surprising behavior only. ::: Use at most a couple per screenful, and never open a page with one. Page shapes 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. 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. Tutorial : outcome + finished screenshot + rough duration + prerequisites → numbered steps with expected-result sentences (\"You should now see…\") → \"What you built\" recap → next steps. 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 and Spec changelog , so regenerate in the same change set. Internal links Write 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. 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. Screenshots All screenshots come from the automated pipeline — none are hand-taken, so every image can be regenerated when the UI changes: Declare the shot in scripts/screenshots/manifest.json (project, file, actions, regions). Give it a docs field listing the page slugs it illustrates. Run bun run screenshots — output lands in docs/images/ and is committed. Reference it relative to your page , e.g. ![descriptive alt text](../images/<name>.png) from docs/start/ , ../../images/<name>.png one level deeper. Relative 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 ), which is also how a site page outside /docs references one. Alt 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."},{"id":"docs:extending/contributing/docs#contributing-to-these-docs","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#contributing-to-these-docs","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Contributing to these docs","text":"The 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/ )."},{"id":"docs:extending/contributing/docs#adding-a-page","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#adding-a-page","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Adding a page","text":"Create the Markdown file under the right section ( start/ , studio/ , framework/ , extending/ ). Add frontmatter: title (sentence case, no site suffix) and description (≤155 characters). 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). Run bun run docs:check before pushing. Optional frontmatter associates a page with its sources, validated by CI: spec : - spec.md#19.4 # a specs/ file and numbered section code : - packages/runtime/src/runtime.ts # repo paths that must exist In 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. These 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. Your 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 ."},{"id":"docs:extending/contributing/docs#voice-and-style","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#voice-and-style","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Voice and style","text":"Second person, present tense, imperative steps: \"Click New Project \", not \"The New Project button can be clicked\". Sentence-case headings; one H1 matching the frontmatter title. Studio and Start pages assume no code knowledge. Code belongs in Framework/Extending pages — or inside a :::doc-note aside. 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. Chevron click paths for navigation chains: Settings > CSS Variables . American English, short paragraphs, no marketing superlatives."},{"id":"docs:extending/contributing/docs#canonical-ui-names","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#canonical-ui-names","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Canonical UI names","text":"Never 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 ."},{"id":"docs:extending/contributing/docs#callouts","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#callouts","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Callouts","text":"Three container directives render as styled asides: :::doc-note Neutral context — including \"behind the scenes\" notes naming what Studio writes to disk. ::: :::doc-tip Shortcuts and good practices. ::: :::doc-warning Data loss or surprising behavior only. ::: Use at most a couple per screenful, and never open a page with one."},{"id":"docs:extending/contributing/docs#page-shapes","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#page-shapes","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Page shapes","text":"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. 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. Tutorial : outcome + finished screenshot + rough duration + prerequisites → numbered steps with expected-result sentences (\"You should now see…\") → \"What you built\" recap → next steps. 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 and Spec changelog , so regenerate in the same change set."},{"id":"docs:extending/contributing/docs#internal-links","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#internal-links","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Internal links","text":"Write 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. 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."},{"id":"docs:extending/contributing/docs#screenshots","collection":"docs","slug":"extending/contributing/docs","url":"/docs/extending/contributing/docs/#screenshots","title":"Contributing to these docs","description":"How the Jx documentation is written, structured, screenshotted, and checked — the style guide for every page in /docs.","heading":"Screenshots","text":"All screenshots come from the automated pipeline — none are hand-taken, so every image can be regenerated when the UI changes: Declare the shot in scripts/screenshots/manifest.json (project, file, actions, regions). Give it a docs field listing the page slugs it illustrates. Run bun run screenshots — output lands in docs/images/ and is committed. Reference it relative to your page , e.g. ![descriptive alt text](../images/<name>.png) from docs/start/ , ../../images/<name>.png one level deeper. Relative 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 ), which is also how a site page outside /docs references one. Alt 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."},{"id":"docs:extending/contributing/monorepo","collection":"docs","slug":"extending/contributing/monorepo","url":"/docs/extending/contributing/monorepo/","title":"Working in the monorepo","description":"Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.","heading":"","text":"Working in the monorepo The Jx monorepo ( github.com/jxsuite/jx ) is a Bun workspace. Layout packages/ — the @jxsuite/* core packages: runtime, compiler, schema, server, studio, desktop, protocol, formulas, collab, ai, markup, import, starters, create. extensions/ — extension packages built on the public hooks: parser (Markdown/CSV formats and content), connector (databases), auth. specs/ — the numbered specifications. These are the living source of truth: consult and update them before implementing a feature. sites/ — real sites built with Jx, including jxsuite.com. docs/ — this documentation (see Contributing to these docs ). Everyday commands bun install — set up the workspace. bun run dev — start the dev server and open Studio in a browser. bun test --isolate (per package) — the supported test mode; plain bun test has known order-dependent failures. bun run typecheck , bun run lint , bun run format — tsgo, oxlint (all categories at error), oxfmt. Testing policy Every 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."},{"id":"docs:extending/contributing/monorepo#working-in-the-monorepo","collection":"docs","slug":"extending/contributing/monorepo","url":"/docs/extending/contributing/monorepo/#working-in-the-monorepo","title":"Working in the monorepo","description":"Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.","heading":"Working in the monorepo","text":"The Jx monorepo ( github.com/jxsuite/jx ) is a Bun workspace."},{"id":"docs:extending/contributing/monorepo#layout","collection":"docs","slug":"extending/contributing/monorepo","url":"/docs/extending/contributing/monorepo/#layout","title":"Working in the monorepo","description":"Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.","heading":"Layout","text":"packages/ — the @jxsuite/* core packages: runtime, compiler, schema, server, studio, desktop, protocol, formulas, collab, ai, markup, import, starters, create. extensions/ — extension packages built on the public hooks: parser (Markdown/CSV formats and content), connector (databases), auth. specs/ — the numbered specifications. These are the living source of truth: consult and update them before implementing a feature. sites/ — real sites built with Jx, including jxsuite.com. docs/ — this documentation (see Contributing to these docs )."},{"id":"docs:extending/contributing/monorepo#everyday-commands","collection":"docs","slug":"extending/contributing/monorepo","url":"/docs/extending/contributing/monorepo/#everyday-commands","title":"Working in the monorepo","description":"Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.","heading":"Everyday commands","text":"bun install — set up the workspace. bun run dev — start the dev server and open Studio in a browser. bun test --isolate (per package) — the supported test mode; plain bun test has known order-dependent failures. bun run typecheck , bun run lint , bun run format — tsgo, oxlint (all categories at error), oxfmt."},{"id":"docs:extending/contributing/monorepo#testing-policy","collection":"docs","slug":"extending/contributing/monorepo","url":"/docs/extending/contributing/monorepo/#testing-policy","title":"Working in the monorepo","description":"Repo layout, running Studio from source, tests, and the conventions the Jx monorepo enforces in CI.","heading":"Testing policy","text":"Every 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."},{"id":"docs:extending/embedding/backend-protocol","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"","text":"The backend protocol The 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 is your specification; if you're writing a platform adapter instead, these are still the semantics your adapter must preserve. What exactly is the contract The 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. Each entry in STUDIO_ROUTES carries its method, path, a one-line contract summary, and — for optional routes — what Studio does without it: // packages/protocol/src/routes.ts export interface StudioRoute { path : string ; method : StudioRouteMethod ; /** True when a backend may omit the route (its PAL member is optional). */ optional : boolean ; /** One-line contract summary. */ summary : string ; /** What Studio does when an optional route is absent. */ degradation ?: string ; } The full table is generated into the protocol route reference — consult it there rather than re-reading the source. Core vs optional Optional 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. The 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. Versioning 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 . The error shape Every failure is an ErrorBody with a meaningful HTTP status: // packages/protocol/src/types.ts export interface ErrorBody { error : string ; /** Machine-readable discriminator (e.g. \"remote_moved\", \"cf_not_connected\"). */ code ?: string ; detail ?: unknown ; } 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. Behaviors implementers must match Route shapes alone don't capture everything. These semantics are part of the contract: 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). git/pull returns 409 { code: \"pull_conflict\", conflicts: [paths] } when local dirty files overlap remote changes; a clean pull fast-forwards. format dispatches { format, action: \"parse\" | \"serialize\", source? | doc?, options? } through the project's format registry. Without it, only .json documents open. 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. 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. 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. 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 and the extension security model . Related Protocol route reference — the generated table: every route, method, summary, and degradation Dev server internals — the reference implementation of these routes Writing a platform adapter — the in-page interface these routes back"},{"id":"docs:extending/embedding/backend-protocol#the-backend-protocol","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#the-backend-protocol","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"The backend protocol","text":"The 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 is your specification; if you're writing a platform adapter instead, these are still the semantics your adapter must preserve."},{"id":"docs:extending/embedding/backend-protocol#what-exactly-is-the-contract","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#what-exactly-is-the-contract","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"What exactly is the contract","text":"The 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. Each entry in STUDIO_ROUTES carries its method, path, a one-line contract summary, and — for optional routes — what Studio does without it: // packages/protocol/src/routes.ts export interface StudioRoute { path : string ; method : StudioRouteMethod ; /** True when a backend may omit the route (its PAL member is optional). */ optional : boolean ; /** One-line contract summary. */ summary : string ; /** What Studio does when an optional route is absent. */ degradation ?: string ; } The full table is generated into the protocol route reference — consult it there rather than re-reading the source."},{"id":"docs:extending/embedding/backend-protocol#core-vs-optional","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#core-vs-optional","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"Core vs optional","text":"Optional 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. The 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."},{"id":"docs:extending/embedding/backend-protocol#versioning","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#versioning","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"Versioning","text":"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 ."},{"id":"docs:extending/embedding/backend-protocol#the-error-shape","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#the-error-shape","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"The error shape","text":"Every failure is an ErrorBody with a meaningful HTTP status: // packages/protocol/src/types.ts export interface ErrorBody { error : string ; /** Machine-readable discriminator (e.g. \"remote_moved\", \"cf_not_connected\"). */ code ?: string ; detail ?: unknown ; } 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."},{"id":"docs:extending/embedding/backend-protocol#behaviors-implementers-must-match","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#behaviors-implementers-must-match","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"Behaviors implementers must match","text":"Route shapes alone don't capture everything. These semantics are part of the contract: 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). git/pull returns 409 { code: \"pull_conflict\", conflicts: [paths] } when local dirty files overlap remote changes; a clean pull fast-forwards. format dispatches { format, action: \"parse\" | \"serialize\", source? | doc?, options? } through the project's format registry. Without it, only .json documents open. 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. 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. 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. 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 and the extension security model ."},{"id":"docs:extending/embedding/backend-protocol#related","collection":"docs","slug":"extending/embedding/backend-protocol","url":"/docs/extending/embedding/backend-protocol/#related","title":"The backend protocol","description":"The Studio Backend Protocol contract — core vs optional routes, transport freedom, versioning, the error shape, and behaviors implementers must match.","heading":"Related","text":"Protocol route reference — the generated table: every route, method, summary, and degradation Dev server internals — the reference implementation of these routes Writing a platform adapter — the in-page interface these routes back"},{"id":"docs:extending/embedding/dev-server","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"","text":"Dev server internals @jxsuite/server is the reference implementation of the 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 in the Framework section. One handler, an ordered route chain 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: /__reload — the SSE live-reload endpoint (skipped when watch: false ). POST /__jx_resolve__ — the $prototype / $src resolution proxy. POST /__jx_server__ — the timing: \"server\" function proxy. /_jx/* — extension server mounts. /__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. Custom middleware , if the embedder passed one. 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. Ordering 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. The Studio API mount and activation 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 . Format 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. The resolve proxies 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. 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. Extension server mounts 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 for the extension-author side. The security model The 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: 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. 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. 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. 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. The 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. Related The dev server — the user-level page: options, live reload, the proxies from the document author's view The backend protocol — the contract this server is the reference for Protocol route reference — every /__studio/* route it serves Extension security model — the trust boundaries extensions themselves live under"},{"id":"docs:extending/embedding/dev-server#dev-server-internals","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#dev-server-internals","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"Dev server internals","text":"@jxsuite/server is the reference implementation of the 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 in the Framework section."},{"id":"docs:extending/embedding/dev-server#one-handler-an-ordered-route-chain","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#one-handler-an-ordered-route-chain","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"One handler, an ordered route chain","text":"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: /__reload — the SSE live-reload endpoint (skipped when watch: false ). POST /__jx_resolve__ — the $prototype / $src resolution proxy. POST /__jx_server__ — the timing: \"server\" function proxy. /_jx/* — extension server mounts. /__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. Custom middleware , if the embedder passed one. 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. Ordering 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."},{"id":"docs:extending/embedding/dev-server#the-studio-api-mount-and-activation","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#the-studio-api-mount-and-activation","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"The Studio API mount and activation","text":"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 . Format 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."},{"id":"docs:extending/embedding/dev-server#the-resolve-proxies","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#the-resolve-proxies","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"The resolve proxies","text":"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. 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."},{"id":"docs:extending/embedding/dev-server#extension-server-mounts","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#extension-server-mounts","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"Extension server mounts","text":"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 for the extension-author side."},{"id":"docs:extending/embedding/dev-server#the-security-model","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#the-security-model","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"The security model","text":"The 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: 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. 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. 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. 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. The 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."},{"id":"docs:extending/embedding/dev-server#related","collection":"docs","slug":"extending/embedding/dev-server","url":"/docs/extending/embedding/dev-server/#related","title":"Dev server internals","description":"Inside @jxsuite/server, the protocol's reference implementation: the route chain, the Studio API mount, the resolve proxies, and the security model.","heading":"Related","text":"The dev server — the user-level page: options, live reload, the proxies from the document author's view The backend protocol — the contract this server is the reference for Protocol route reference — every /__studio/* route it serves Extension security model — the trust boundaries extensions themselves live under"},{"id":"docs:extending/embedding/platform-adapter","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"","text":"Writing a platform adapter A 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 for that decision). The 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. The interface surface Core members are required on every adapter. Grouped by family: Family Members Identity id , projectRoot (get/set), canvasUrl? Session/project activate , openProject , probeRootProject , createDestination , createProject , resolveSiteContext Filesystem listDirectory , readFile , writeFile , uploadFile , deleteFile , renameFile , createDirectory , locateFile , searchFiles , discoverComponents Git gitStatus , gitBranches , gitLog , gitStage , gitUnstage , gitCommit , gitPush , gitPull , gitFetch , gitCheckout , gitCreateBranch , gitDiff , gitShow , gitDiscard , gitInit , gitAddRemote Packages listPackages , addPackage , removePackage Services codeService , fetchPluginSchema , aiChatUrl All 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. 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. Optional members and degradation Everything 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: Family Members When absent Live sync subscribeFileEvents , collab Sidebar is manual-refresh; editing is solo with file-level saves Install pipeline installDependencies , dependenciesNeedInstall , outdatedPackages , setPackageVersions Install/update affordances hide; manifest-only edits still work 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 Formats/schemas listFormats , listExtensions , fetchProjectSchemas , formatAction , resolveClass Only .json documents open; editors fall back to bundled schemas Data + secrets dataConnections , dataConnectionTest , dataPush , dataRows , row CRUD, listSecrets , setSecrets The Data grid and connection/push/secret controls hide Publish cfConnection , cfConnect , cfApi , createPullRequest Publish panel explains git-push publishing; PRs go via user token Desktop shell getAppInfo , openProjectInNewWindow , newWindow , setWindowProject , getProjectRoot , recents, settings Single-window; recents and settings persist in localStorage Cloud identity getUser , getAccountStatus , listRepos , importProject No signed-in identity or repository picker Studio always checks for presence before calling an optional member, so an omitted member is never an error. The protocol route reference is the complete degradation catalogue. A 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. Registration Registration 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 : // packages/studio/src/platform.ts const g = globalThis as unknown as { __jxPlatform ?: StudioPlatform }; export function registerPlatform ( platform : StudioPlatform ) { g.__jxPlatform = platform; } export function getPlatform () { if ( ! g.__jxPlatform) { throw new Error ( \"No platform registered. Call registerPlatform() before starting Studio.\" ); } return g.__jxPlatform; } The desktop app does exactly this — a four-line init bundle injected ahead of the Studio bundle: // packages/desktop/src/init.ts — loaded before studio.js import { registerPlatform } from \"@jxsuite/studio/platform\" ; import { createDesktopPlatform } from \"./platform\" ; registerPlatform ( createDesktopPlatform ()); If 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. The project-open flow Opening a project is the one flow the adapter owns end to end, because the picking UI is inherently platform-specific: The user triggers Open Project ; Studio calls getPlatform().openProject() . 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 . It returns { config, handle: { root, name, projectConfig } } , or null if the user cancelled (never throw for a cancel). Studio initializes project state from the handle: file tree, component registry, expanded directories. Two 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. Two real adapters Dev server ( packages/studio/src/platforms/devserver.ts ) The reference adapter is a stateless wrapper over fetch : every member maps 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: // openProject, abbreviated: match the picked directory to a server-known project const sitesRes = await fetch ( \"/__studio/sites\" ); const sites = await readJson < SiteEntry []>(sitesRes); const match = sites. find (( s : SiteEntry ) => JSON . stringify (s.config) === JSON . stringify (config)); if ( ! match) { // Project is outside dev server root — ask the server to find it by directory name const findRes = await fetch ( `/__studio/find-project?name=${ encodeURIComponent ( dirHandle . name ) }` ); // … } Desktop ( packages/desktop/src/platform.ts ) The 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 . Related Embedding overview — choosing between an adapter and the HTTP protocol The backend protocol — the semantics your adapter must preserve Protocol route reference — every route with optionality and degradation"},{"id":"docs:extending/embedding/platform-adapter#writing-a-platform-adapter","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#writing-a-platform-adapter","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Writing a platform adapter","text":"A 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 for that decision). The 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."},{"id":"docs:extending/embedding/platform-adapter#the-interface-surface","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#the-interface-surface","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"The interface surface","text":"Core members are required on every adapter. Grouped by family: Family Members Identity id , projectRoot (get/set), canvasUrl? Session/project activate , openProject , probeRootProject , createDestination , createProject , resolveSiteContext Filesystem listDirectory , readFile , writeFile , uploadFile , deleteFile , renameFile , createDirectory , locateFile , searchFiles , discoverComponents Git gitStatus , gitBranches , gitLog , gitStage , gitUnstage , gitCommit , gitPush , gitPull , gitFetch , gitCheckout , gitCreateBranch , gitDiff , gitShow , gitDiscard , gitInit , gitAddRemote Packages listPackages , addPackage , removePackage Services codeService , fetchPluginSchema , aiChatUrl All 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. 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."},{"id":"docs:extending/embedding/platform-adapter#optional-members-and-degradation","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#optional-members-and-degradation","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Optional members and degradation","text":"Everything 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: Family Members When absent Live sync subscribeFileEvents , collab Sidebar is manual-refresh; editing is solo with file-level saves Install pipeline installDependencies , dependenciesNeedInstall , outdatedPackages , setPackageVersions Install/update affordances hide; manifest-only edits still work 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 Formats/schemas listFormats , listExtensions , fetchProjectSchemas , formatAction , resolveClass Only .json documents open; editors fall back to bundled schemas Data + secrets dataConnections , dataConnectionTest , dataPush , dataRows , row CRUD, listSecrets , setSecrets The Data grid and connection/push/secret controls hide Publish cfConnection , cfConnect , cfApi , createPullRequest Publish panel explains git-push publishing; PRs go via user token Desktop shell getAppInfo , openProjectInNewWindow , newWindow , setWindowProject , getProjectRoot , recents, settings Single-window; recents and settings persist in localStorage Cloud identity getUser , getAccountStatus , listRepos , importProject No signed-in identity or repository picker Studio always checks for presence before calling an optional member, so an omitted member is never an error. The protocol route reference is the complete degradation catalogue. A 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."},{"id":"docs:extending/embedding/platform-adapter#registration","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#registration","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Registration","text":"Registration 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 : // packages/studio/src/platform.ts const g = globalThis as unknown as { __jxPlatform ?: StudioPlatform }; export function registerPlatform ( platform : StudioPlatform ) { g.__jxPlatform = platform; } export function getPlatform () { if ( ! g.__jxPlatform) { throw new Error ( \"No platform registered. Call registerPlatform() before starting Studio.\" ); } return g.__jxPlatform; } The desktop app does exactly this — a four-line init bundle injected ahead of the Studio bundle: // packages/desktop/src/init.ts — loaded before studio.js import { registerPlatform } from \"@jxsuite/studio/platform\" ; import { createDesktopPlatform } from \"./platform\" ; registerPlatform ( createDesktopPlatform ()); If 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."},{"id":"docs:extending/embedding/platform-adapter#the-project-open-flow","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#the-project-open-flow","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"The project-open flow","text":"Opening a project is the one flow the adapter owns end to end, because the picking UI is inherently platform-specific: The user triggers Open Project ; Studio calls getPlatform().openProject() . 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 . It returns { config, handle: { root, name, projectConfig } } , or null if the user cancelled (never throw for a cancel). Studio initializes project state from the handle: file tree, component registry, expanded directories. Two 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."},{"id":"docs:extending/embedding/platform-adapter#two-real-adapters","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#two-real-adapters","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Two real adapters","text":""},{"id":"docs:extending/embedding/platform-adapter#dev-server-packagesstudiosrcplatformsdevserverts","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#dev-server-packagesstudiosrcplatformsdevserverts","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Dev server ( packages/studio/src/platforms/devserver.ts )","text":"The reference adapter is a stateless wrapper over fetch : every member maps 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: // openProject, abbreviated: match the picked directory to a server-known project const sitesRes = await fetch ( \"/__studio/sites\" ); const sites = await readJson < SiteEntry []>(sitesRes); const match = sites. find (( s : SiteEntry ) => JSON . stringify (s.config) === JSON . stringify (config)); if ( ! match) { // Project is outside dev server root — ask the server to find it by directory name const findRes = await fetch ( `/__studio/find-project?name=${ encodeURIComponent ( dirHandle . name ) }` ); // … }"},{"id":"docs:extending/embedding/platform-adapter#desktop-packagesdesktopsrcplatformts","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#desktop-packagesdesktopsrcplatformts","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Desktop ( packages/desktop/src/platform.ts )","text":"The 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 ."},{"id":"docs:extending/embedding/platform-adapter#related","collection":"docs","slug":"extending/embedding/platform-adapter","url":"/docs/extending/embedding/platform-adapter/#related","title":"Writing a platform adapter","description":"Implement the StudioPlatform interface for your host: core and optional members, registration before Studio boots, and the project-open flow.","heading":"Related","text":"Embedding overview — choosing between an adapter and the HTTP protocol The backend protocol — the semantics your adapter must preserve Protocol route reference — every route with optionality and degradation"},{"id":"docs:extending/extensions/anatomy","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"","text":"Extension anatomy Jx 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. An extension contributes up to four kinds of things, all declared in JSON before any code runs: Classes — .class.json descriptors that become $prototype -visible names ( Custom classes ). Formats — file-extension handling for parsing, saving, and content loading ( Custom formats ). Project sections — top-level project.json keys with data loading and Studio settings UI ( Project sections and settings ). Schema fragments — JSON Schema documents composed into the project's effective schema ( Schema composition ). Package layout The parser is the reference extension — its layout is the template: extensions/ parser/ # @jxsuite/parser — content collections, markdown, CSV package.json # \"jx\": \"./jx-extension.json\"; exports manifest, schemas, classes jx-extension.json schemas/ project.fragment.schema.json document.fragment.schema.json src/ Markdown.class.json # class descriptors (admission blocks + capability methods) markdown.ts # implementations connector/ # @jxsuite/connector — connections + dynamic data tables auth/ # @jxsuite/auth — Better Auth sessions and permissions Three 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. Publish 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. Dependency rules In the Jx monorepo, packages/* is core and extensions/* is extensions — and the arrow between them points one way: Extensions may depend on core packages and on each other. @jxsuite/auth depends on @jxsuite/connector for its database connection. 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 . 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. Third-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. Declaring extensions in a project A project enables extensions in project.json : { \"$schema\" : \"./project.schema.json\" , \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"./my-local-ext\" ], \"imports\" : { \"PostCard\" : \"./components/post-card.class.json\" }, \"content\" : { \"posts\" : { \"source\" : \"./content/posts/\" , \"format\" : \"Markdown\" , \"schema\" : {} } } } 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 . imports keeps its original, reduced job: mapping $prototype names to project-local class files. It does not register formats or extensions. 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. No implicit defaults. A project with no extensions supports only .json documents and core state prototypes. Section keys Extensions 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: the property name in project.json , the key under _project in resolved scope ( config._project.content ), the wire-path segment where applicable. Two extensions claiming the same section key is a registry error. See Project sections and settings for what owning a section means. The jx-extension.json manifest The manifest lives at the package root, referenced by \"jx\": \"./jx-extension.json\" in package.json . The parser's real manifest, in full: { \"name\" : \"@jxsuite/parser\" , \"title\" : \"Content & Markdown\" , \"description\" : \"File-based content collections with Markdown and CSV formats\" , \"classes\" : { \"Markdown\" : \"./src/Markdown.class.json\" , \"Csv\" : \"./src/Csv.class.json\" , \"MarkdownCollection\" : \"./src/MarkdownCollection.class.json\" , \"ContentCollection\" : \"./src/ContentCollection.class.json\" , \"ContentEntry\" : \"./src/ContentEntry.class.json\" , \"Content\" : \"./src/Content.class.json\" }, \"schemas\" : { \"project\" : \"./schemas/project.fragment.schema.json\" , \"document\" : \"./schemas/document.fragment.schema.json\" } } Key Type Meaning name string (required) Package name; must match package.json . title string Human-readable name for Studio surfaces. description string One-line description. classes Record<string, string> $prototype -visible name → class descriptor path, relative to the manifest. schemas object Schema fragments this package contributes: project (project.json sections), document (document positions). The 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. The 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. Related Schema composition — what the schemas fragments contribute Custom classes — the descriptors the classes map points to Tutorial: a TOML format extension — a complete third-party package, end to end First-party extensions — what parser, connector, and auth each provide"},{"id":"docs:extending/extensions/anatomy#extension-anatomy","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#extension-anatomy","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Extension anatomy","text":"Jx 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. An extension contributes up to four kinds of things, all declared in JSON before any code runs: Classes — .class.json descriptors that become $prototype -visible names ( Custom classes ). Formats — file-extension handling for parsing, saving, and content loading ( Custom formats ). Project sections — top-level project.json keys with data loading and Studio settings UI ( Project sections and settings ). Schema fragments — JSON Schema documents composed into the project's effective schema ( Schema composition )."},{"id":"docs:extending/extensions/anatomy#package-layout","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#package-layout","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Package layout","text":"The parser is the reference extension — its layout is the template: extensions/ parser/ # @jxsuite/parser — content collections, markdown, CSV package.json # \"jx\": \"./jx-extension.json\"; exports manifest, schemas, classes jx-extension.json schemas/ project.fragment.schema.json document.fragment.schema.json src/ Markdown.class.json # class descriptors (admission blocks + capability methods) markdown.ts # implementations connector/ # @jxsuite/connector — connections + dynamic data tables auth/ # @jxsuite/auth — Better Auth sessions and permissions Three 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. Publish 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."},{"id":"docs:extending/extensions/anatomy#dependency-rules","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#dependency-rules","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Dependency rules","text":"In the Jx monorepo, packages/* is core and extensions/* is extensions — and the arrow between them points one way: Extensions may depend on core packages and on each other. @jxsuite/auth depends on @jxsuite/connector for its database connection. 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 . 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. Third-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."},{"id":"docs:extending/extensions/anatomy#declaring-extensions-in-a-project","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#declaring-extensions-in-a-project","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Declaring extensions in a project","text":"A project enables extensions in project.json : { \"$schema\" : \"./project.schema.json\" , \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"./my-local-ext\" ], \"imports\" : { \"PostCard\" : \"./components/post-card.class.json\" }, \"content\" : { \"posts\" : { \"source\" : \"./content/posts/\" , \"format\" : \"Markdown\" , \"schema\" : {} } } } 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 . imports keeps its original, reduced job: mapping $prototype names to project-local class files. It does not register formats or extensions. 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. No implicit defaults. A project with no extensions supports only .json documents and core state prototypes."},{"id":"docs:extending/extensions/anatomy#section-keys","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#section-keys","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Section keys","text":"Extensions 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: the property name in project.json , the key under _project in resolved scope ( config._project.content ), the wire-path segment where applicable. Two extensions claiming the same section key is a registry error. See Project sections and settings for what owning a section means."},{"id":"docs:extending/extensions/anatomy#the-jx-extensionjson-manifest","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#the-jx-extensionjson-manifest","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"The jx-extension.json manifest","text":"The manifest lives at the package root, referenced by \"jx\": \"./jx-extension.json\" in package.json . The parser's real manifest, in full: { \"name\" : \"@jxsuite/parser\" , \"title\" : \"Content & Markdown\" , \"description\" : \"File-based content collections with Markdown and CSV formats\" , \"classes\" : { \"Markdown\" : \"./src/Markdown.class.json\" , \"Csv\" : \"./src/Csv.class.json\" , \"MarkdownCollection\" : \"./src/MarkdownCollection.class.json\" , \"ContentCollection\" : \"./src/ContentCollection.class.json\" , \"ContentEntry\" : \"./src/ContentEntry.class.json\" , \"Content\" : \"./src/Content.class.json\" }, \"schemas\" : { \"project\" : \"./schemas/project.fragment.schema.json\" , \"document\" : \"./schemas/document.fragment.schema.json\" } } Key Type Meaning name string (required) Package name; must match package.json . title string Human-readable name for Studio surfaces. description string One-line description. classes Record<string, string> $prototype -visible name → class descriptor path, relative to the manifest. schemas object Schema fragments this package contributes: project (project.json sections), document (document positions). The 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. The 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."},{"id":"docs:extending/extensions/anatomy#related","collection":"docs","slug":"extending/extensions/anatomy","url":"/docs/extending/extensions/anatomy/#related","title":"Extension anatomy","description":"What an extension package contains: layout and dependency rules, the project.json declaration model, section keys, and the jx-extension.json manifest.","heading":"Related","text":"Schema composition — what the schemas fragments contribute Custom classes — the descriptors the classes map points to Tutorial: a TOML format extension — a complete third-party package, end to end First-party extensions — what parser, connector, and auth each provide"},{"id":"docs:extending/extensions/capabilities","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"","text":"Capability methods Capabilities 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. The contract A capability declaration is an ordinary method entry with a reserved role . The parser's Content class declares its $paths expansion capability like this (abbreviated): \"resolvePaths\" : { \"role\" : \"resolvePaths\" , \"scope\" : \"static\" , \"identifier\" : \"resolvePaths\" , \"discriminator\" : \"contentType\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"pathsDef\" , \"type\" : { \"type\" : \"object\" , \"properties\" : { \"contentType\" : { \"type\" : \"string\" }, \"param\" : { \"type\" : \"string\" , \"default\" : \"slug\" }, \"field\" : { \"type\" : \"string\" , \"default\" : \"id\" } }, \"required\" : [ \"contentType\" ] } }, { \"identifier\" : \"ctx\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"$ref\" : \"#/$defs/returnTypes/PathEntries\" } } To 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. The roles Role Block Signature Consumers parse format (source, options?) → JxDocument compiler, server, Studio (open file) serialize format (doc, options?) → string Studio (save), site build (export sidecars) discover format (source, { baseDir }) → string[] content loading (list entry files) load format (path, { schema, directiveOptions }) → ContentLoaderEntry[] content loading (parse one source) projectData project (sectionValue, { projectConfig, root, registry, io }) → unknown site build, dev server — result stored as _project[<key>] resolvePaths project (pathsDef, { data, projectConfig, root }) → Record<string, unknown>[] pages discovery ( $paths expansion), Studio preview lower any (def, context) → JxStateDefinition compiler — rewrites a state def into a core shape emit project (sectionValue, { projectConfig, root, sections, routes }) → EmitFile[] site build — writes derived assets into the build output assets project (sectionValue, { projectConfig, root }) → AssetMount[] site build, dev server — publishes directories at site URLs mount server (options, ctx) → (request, env) => Promise<Response> generated site worker, dev server dialect connector (connection, env) → Kysely Dialect data mounts, auth, deploy deploySchema connector (tables, connection, { env, dryRun }) → { statements, applied, warnings } jx db push , Studio push bindings connector (connection) → wrangler config fragment scaffolding, jx db push testConnection connector (connection, env) → { ok, error? } Studio connections UI, CLI 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. The format roles are covered in depth in Custom formats ; the project roles in Project sections and settings ; mount , dialect , deploySchema , bindings , and testConnection in Server mounts and Connectors . timing Each capability may declare a timing array — the environments allowed to invoke it directly: Values: \"compiler\" , \"server\" , \"client\" . Default when omitted: [\"compiler\", \"server\"] — assume node-only. A host whose environment is excluded round-trips through the dev server ( POST /__studio/format , POST /__jx_resolve__ ) instead of importing the implementation. 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\"] . 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. Options as parameters Capability options are declared as ordinary parameters with JSON-Schema types — there is no separate options vocabulary. Markdown.serialize declares its second parameter as: { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" , \"properties\" : { \"mode\" : { \"type\" : \"string\" , \"enum\" : [ \"roundtrip\" , \"export\" ], \"default\" : \"roundtrip\" }, \"frontmatter\" : { \"type\" : \"boolean\" , \"default\" : true } } } } Because 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. lower lower lets a state class compile away. At build time, when a state entry's 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: \"lower\" : { \"role\" : \"lower\" , \"scope\" : \"static\" , \"identifier\" : \"lower\" , \"timing\" : [ \"compiler\" ], \"parameters\" : [ { \"identifier\" : \"def\" , \"type\" : { \"type\" : \"object\" } }, { \"identifier\" : \"context\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"object\" }, \"description\" : \"Rewrite into a core Request def targeting /_jx/data\" } A 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). assets assets publishes a directory your section already reads from at a site URL, so the files beside its sources are reachable: \"assets\" : { \"role\" : \"assets\" , \"scope\" : \"static\" , \"identifier\" : \"assets\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"sectionValue\" , \"type\" : { \"type\" : \"object\" } }, { \"identifier\" : \"ctx\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" } } Each 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. Hosts do three things with a mount, and your extension writes none of them: the site build resolves mounted URLs for image optimization , then copies only the files the compiled output references to their URL path in dist/ ; the dev and desktop servers serve the mount ahead of the project root, so previews match the built site; jx preview needs nothing — it serves dist/ , where the files already are. 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. Related Custom classes — the descriptors capabilities live in Custom formats — the four format roles in context Project sections and settings — projectData and resolvePaths in context Timing — the state-entry timing model lower keys off"},{"id":"docs:extending/extensions/capabilities#capability-methods","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#capability-methods","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"Capability methods","text":"Capabilities 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."},{"id":"docs:extending/extensions/capabilities#the-contract","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#the-contract","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"The contract","text":"A capability declaration is an ordinary method entry with a reserved role . The parser's Content class declares its $paths expansion capability like this (abbreviated): \"resolvePaths\" : { \"role\" : \"resolvePaths\" , \"scope\" : \"static\" , \"identifier\" : \"resolvePaths\" , \"discriminator\" : \"contentType\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"pathsDef\" , \"type\" : { \"type\" : \"object\" , \"properties\" : { \"contentType\" : { \"type\" : \"string\" }, \"param\" : { \"type\" : \"string\" , \"default\" : \"slug\" }, \"field\" : { \"type\" : \"string\" , \"default\" : \"id\" } }, \"required\" : [ \"contentType\" ] } }, { \"identifier\" : \"ctx\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"$ref\" : \"#/$defs/returnTypes/PathEntries\" } } To 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."},{"id":"docs:extending/extensions/capabilities#the-roles","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#the-roles","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"The roles","text":"Role Block Signature Consumers parse format (source, options?) → JxDocument compiler, server, Studio (open file) serialize format (doc, options?) → string Studio (save), site build (export sidecars) discover format (source, { baseDir }) → string[] content loading (list entry files) load format (path, { schema, directiveOptions }) → ContentLoaderEntry[] content loading (parse one source) projectData project (sectionValue, { projectConfig, root, registry, io }) → unknown site build, dev server — result stored as _project[<key>] resolvePaths project (pathsDef, { data, projectConfig, root }) → Record<string, unknown>[] pages discovery ( $paths expansion), Studio preview lower any (def, context) → JxStateDefinition compiler — rewrites a state def into a core shape emit project (sectionValue, { projectConfig, root, sections, routes }) → EmitFile[] site build — writes derived assets into the build output assets project (sectionValue, { projectConfig, root }) → AssetMount[] site build, dev server — publishes directories at site URLs mount server (options, ctx) → (request, env) => Promise<Response> generated site worker, dev server dialect connector (connection, env) → Kysely Dialect data mounts, auth, deploy deploySchema connector (tables, connection, { env, dryRun }) → { statements, applied, warnings } jx db push , Studio push bindings connector (connection) → wrangler config fragment scaffolding, jx db push testConnection connector (connection, env) → { ok, error? } Studio connections UI, CLI 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. The format roles are covered in depth in Custom formats ; the project roles in Project sections and settings ; mount , dialect , deploySchema , bindings , and testConnection in Server mounts and Connectors ."},{"id":"docs:extending/extensions/capabilities#timing","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#timing","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"timing","text":"Each capability may declare a timing array — the environments allowed to invoke it directly: Values: \"compiler\" , \"server\" , \"client\" . Default when omitted: [\"compiler\", \"server\"] — assume node-only. A host whose environment is excluded round-trips through the dev server ( POST /__studio/format , POST /__jx_resolve__ ) instead of importing the implementation. 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\"] . 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."},{"id":"docs:extending/extensions/capabilities#options-as-parameters","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#options-as-parameters","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"Options as parameters","text":"Capability options are declared as ordinary parameters with JSON-Schema types — there is no separate options vocabulary. Markdown.serialize declares its second parameter as: { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" , \"properties\" : { \"mode\" : { \"type\" : \"string\" , \"enum\" : [ \"roundtrip\" , \"export\" ], \"default\" : \"roundtrip\" }, \"frontmatter\" : { \"type\" : \"boolean\" , \"default\" : true } } } } Because 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."},{"id":"docs:extending/extensions/capabilities#lower","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#lower","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"lower","text":"lower lets a state class compile away. At build time, when a state entry's 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: \"lower\" : { \"role\" : \"lower\" , \"scope\" : \"static\" , \"identifier\" : \"lower\" , \"timing\" : [ \"compiler\" ], \"parameters\" : [ { \"identifier\" : \"def\" , \"type\" : { \"type\" : \"object\" } }, { \"identifier\" : \"context\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"object\" }, \"description\" : \"Rewrite into a core Request def targeting /_jx/data\" } A 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)."},{"id":"docs:extending/extensions/capabilities#assets","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#assets","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"assets","text":"assets publishes a directory your section already reads from at a site URL, so the files beside its sources are reachable: \"assets\" : { \"role\" : \"assets\" , \"scope\" : \"static\" , \"identifier\" : \"assets\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"sectionValue\" , \"type\" : { \"type\" : \"object\" } }, { \"identifier\" : \"ctx\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" } } Each 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. Hosts do three things with a mount, and your extension writes none of them: the site build resolves mounted URLs for image optimization , then copies only the files the compiled output references to their URL path in dist/ ; the dev and desktop servers serve the mount ahead of the project root, so previews match the built site; jx preview needs nothing — it serves dist/ , where the files already are. 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."},{"id":"docs:extending/extensions/capabilities#related","collection":"docs","slug":"extending/extensions/capabilities","url":"/docs/extending/extensions/capabilities/#related","title":"Capability methods","description":"The static method contract behind every extension integration point: well-known roles, timing, options as parameters, and the lower compile-away hook.","heading":"Related","text":"Custom classes — the descriptors capabilities live in Custom formats — the four format roles in context Project sections and settings — projectData and resolvePaths in context Timing — the state-entry timing model lower keys off"},{"id":"docs:extending/extensions/classes","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"","text":"Custom classes An 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. Each class named in the manifest '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 : { \"state\" : { \"about\" : { \"$prototype\" : \"Markdown\" , \"src\" : \"./content/about.md\" } } } A descriptor at a glance The parser's Markdown.class.json , heavily abbreviated: { \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"title\" : \"Markdown\" , \"$prototype\" : \"Class\" , \"extends\" : \"Object\" , \"$implementation\" : \"./markdown.js\" , \"format\" : { \"extensions\" : [ \".md\" ], \"mediaType\" : \"text/markdown\" , \"…\" : \"…\" }, \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } }, \"…\" : {} }, \"constructor\" : { \"role\" : \"constructor\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }, \"…\" ], \"body\" : [ \"this.config = config;\" ] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"scope\" : \"instance\" , \"identifier\" : \"resolve\" }, \"parse\" : { \"role\" : \"parse\" , \"scope\" : \"static\" , \"timing\" : [ \"compiler\" , \"server\" , \"client\" ] }, \"serialize\" : { \"role\" : \"serialize\" , \"scope\" : \"static\" , \"…\" : \"…\" }, \"…\" : {} } } } The $defs object is organized into well-known categories: Category Purpose parameters Constructor parameter properties (config object fields) fields Instance fields (private if # -prefixed) constructor Constructor body and super args methods Instance methods and accessors, plus static capability methods by role returnTypes Named return-type schemas for tooling The external class contract Every class — extension-shipped or project-local — satisfies the same runtime contract: 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\" } . Value resolution is checked in order: instance.resolve() (async, awaited) → instance.value (getter or property) → the instance itself. Reactivity is optional : implement subscribe(callback) / unsubscribe() and the runtime re-renders when you notify. Methods 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. $implementation $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 ). When $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 . Admission blocks A 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: Block Grants Covered in format File-extension dispatch: parsing, serialization, content discovery/loading, Studio editing. Custom formats project Ownership of a project.json section: data loading, $paths resolution, Studio settings UI. Project sections server A mounted route subtree in the deployed-site worker and the dev server. Server mounts connector A database connection provider: dialect, schema deploy, bindings, connection testing. Connectors 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. How hosts introspect Hosts follow a strict JSON-first contract — the registry ( buildExtensionRegistry() in @jxsuite/schema/extension-registry ) implements it once for node and browser hosts: Resolve each extensions entry to a package root; read the manifest named by its \"jx\" field; read each listed .class.json . Detect participation via the admission blocks. Find capabilities by scanning $defs.methods for well-known role values; the method's identifier (fallback: its key) names the static method. To invoke: import $implementation , take the export named by the class title , call Export[identifier](...args) . Respect timing : if the host's environment is not listed, delegate to the dev server. Step 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. Capability methods Capabilities 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 . Related Capability methods — the static method contract in depth Custom formats — the format admission block Data prototypes — the built-in classes yours sit beside Tutorial: a TOML format extension — a full descriptor built from scratch"},{"id":"docs:extending/extensions/classes#custom-classes","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#custom-classes","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"Custom classes","text":"An 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. Each class named in the manifest '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 : { \"state\" : { \"about\" : { \"$prototype\" : \"Markdown\" , \"src\" : \"./content/about.md\" } } }"},{"id":"docs:extending/extensions/classes#a-descriptor-at-a-glance","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#a-descriptor-at-a-glance","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"A descriptor at a glance","text":"The parser's Markdown.class.json , heavily abbreviated: { \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"title\" : \"Markdown\" , \"$prototype\" : \"Class\" , \"extends\" : \"Object\" , \"$implementation\" : \"./markdown.js\" , \"format\" : { \"extensions\" : [ \".md\" ], \"mediaType\" : \"text/markdown\" , \"…\" : \"…\" }, \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } }, \"…\" : {} }, \"constructor\" : { \"role\" : \"constructor\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }, \"…\" ], \"body\" : [ \"this.config = config;\" ] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"scope\" : \"instance\" , \"identifier\" : \"resolve\" }, \"parse\" : { \"role\" : \"parse\" , \"scope\" : \"static\" , \"timing\" : [ \"compiler\" , \"server\" , \"client\" ] }, \"serialize\" : { \"role\" : \"serialize\" , \"scope\" : \"static\" , \"…\" : \"…\" }, \"…\" : {} } } } The $defs object is organized into well-known categories: Category Purpose parameters Constructor parameter properties (config object fields) fields Instance fields (private if # -prefixed) constructor Constructor body and super args methods Instance methods and accessors, plus static capability methods by role returnTypes Named return-type schemas for tooling"},{"id":"docs:extending/extensions/classes#the-external-class-contract","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#the-external-class-contract","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"The external class contract","text":"Every class — extension-shipped or project-local — satisfies the same runtime contract: 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\" } . Value resolution is checked in order: instance.resolve() (async, awaited) → instance.value (getter or property) → the instance itself. Reactivity is optional : implement subscribe(callback) / unsubscribe() and the runtime re-renders when you notify. Methods 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."},{"id":"docs:extending/extensions/classes#implementation","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#implementation","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"$implementation","text":"$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 ). When $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 ."},{"id":"docs:extending/extensions/classes#admission-blocks","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#admission-blocks","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"Admission blocks","text":"A 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: Block Grants Covered in format File-extension dispatch: parsing, serialization, content discovery/loading, Studio editing. Custom formats project Ownership of a project.json section: data loading, $paths resolution, Studio settings UI. Project sections server A mounted route subtree in the deployed-site worker and the dev server. Server mounts connector A database connection provider: dialect, schema deploy, bindings, connection testing. Connectors 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."},{"id":"docs:extending/extensions/classes#how-hosts-introspect","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#how-hosts-introspect","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"How hosts introspect","text":"Hosts follow a strict JSON-first contract — the registry ( buildExtensionRegistry() in @jxsuite/schema/extension-registry ) implements it once for node and browser hosts: Resolve each extensions entry to a package root; read the manifest named by its \"jx\" field; read each listed .class.json . Detect participation via the admission blocks. Find capabilities by scanning $defs.methods for well-known role values; the method's identifier (fallback: its key) names the static method. To invoke: import $implementation , take the export named by the class title , call Export[identifier](...args) . Respect timing : if the host's environment is not listed, delegate to the dev server. Step 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."},{"id":"docs:extending/extensions/classes#capability-methods","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#capability-methods","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"Capability methods","text":"Capabilities 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 ."},{"id":"docs:extending/extensions/classes#related","collection":"docs","slug":"extending/extensions/classes","url":"/docs/extending/extensions/classes/#related","title":"Custom classes","description":"Writing .class.json descriptors: the external class contract, $implementation, admission blocks, host introspection, and capability methods.","heading":"Related","text":"Capability methods — the static method contract in depth Custom formats — the format admission block Data prototypes — the built-in classes yours sit beside Tutorial: a TOML format extension — a full descriptor built from scratch"},{"id":"docs:extending/extensions/connectors","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"","text":"Connectors A 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 . This 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 instead. The connector block The D1 provider class, extensions/connector/src/D1.class.json , declares: \"connector\" : { \"provider\" : \"d1\" , \"kind\" : \"sqlite\" , \"local\" : \"sqlite\" , \"serve\" : \"@jxsuite/connector/worker\" , \"module\" : \"@jxsuite/connector/d1\" } Key Meaning provider Identifier for the backing service ( d1 , supabase , sqlite ). kind SQL dialect family: \"sqlite\" | \"postgres\" . Consumed by dependents needing a dialect type (e.g. auth). local When \"sqlite\" , the dev server stands this connector in with a local SQLite file (auto-synced on first use). serve The data-mount module serving this connector's tables. (The class also carries a module bare specifier naming the provider implementation for bundler-robust imports, mirroring the server block's module key.) Connector capabilities The block grants nothing by itself — behavior attaches through static capability methods declared in $defs.methods by role : Role Signature Consumers dialect (connection, env) → Kysely Dialect data mounts, auth, deploy deploySchema (tables, connection, { env, dryRun }) → { statements, applied, warnings } jx db push , Studio push bindings (connection) → wrangler config fragment scaffolding, jx db push testConnection (connection, env) → { ok, error? } Studio connections UI, CLI All 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 ). The reference implementation @jxsuite/connector builds a complete data layer from these hooks, wired exactly the way a third-party connector would be. Providers Three 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 . Project sections The extension also owns two project sections : connections — named connections ( Connections.class.json ). Entries carry identifiers and env-var names only; the Studio settings form uses the secret control for values. 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 . Schema push 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. The data mount Data.class.json also declares a server mount — /_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. State classes and lowering Pages 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. Grid support Studio's 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 ). Everything an editor touches — connections, table schemas, permissions, the push button — is documented for Studio users under Databases , Connections , and Data tables . This page describes the extension machinery those surfaces are built on. Related Server mounts — the /_jx/data mount and the shared context. Capability methods — roles, timing, and lowering. First-party extensions — how connector, auth, and parser fit together. Tutorial: a guestbook extension — a table-backed feature built on the connector."},{"id":"docs:extending/extensions/connectors#connectors","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#connectors","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Connectors","text":"A 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 . This 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 instead."},{"id":"docs:extending/extensions/connectors#the-connector-block","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#the-connector-block","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"The connector block","text":"The D1 provider class, extensions/connector/src/D1.class.json , declares: \"connector\" : { \"provider\" : \"d1\" , \"kind\" : \"sqlite\" , \"local\" : \"sqlite\" , \"serve\" : \"@jxsuite/connector/worker\" , \"module\" : \"@jxsuite/connector/d1\" } Key Meaning provider Identifier for the backing service ( d1 , supabase , sqlite ). kind SQL dialect family: \"sqlite\" | \"postgres\" . Consumed by dependents needing a dialect type (e.g. auth). local When \"sqlite\" , the dev server stands this connector in with a local SQLite file (auto-synced on first use). serve The data-mount module serving this connector's tables. (The class also carries a module bare specifier naming the provider implementation for bundler-robust imports, mirroring the server block's module key.)"},{"id":"docs:extending/extensions/connectors#connector-capabilities","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#connector-capabilities","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Connector capabilities","text":"The block grants nothing by itself — behavior attaches through static capability methods declared in $defs.methods by role : Role Signature Consumers dialect (connection, env) → Kysely Dialect data mounts, auth, deploy deploySchema (tables, connection, { env, dryRun }) → { statements, applied, warnings } jx db push , Studio push bindings (connection) → wrangler config fragment scaffolding, jx db push testConnection (connection, env) → { ok, error? } Studio connections UI, CLI All 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 )."},{"id":"docs:extending/extensions/connectors#the-reference-implementation","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#the-reference-implementation","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"The reference implementation","text":"@jxsuite/connector builds a complete data layer from these hooks, wired exactly the way a third-party connector would be."},{"id":"docs:extending/extensions/connectors#providers","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#providers","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Providers","text":"Three 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 ."},{"id":"docs:extending/extensions/connectors#project-sections","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#project-sections","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Project sections","text":"The extension also owns two project sections : connections — named connections ( Connections.class.json ). Entries carry identifiers and env-var names only; the Studio settings form uses the secret control for values. 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 ."},{"id":"docs:extending/extensions/connectors#schema-push","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#schema-push","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Schema push","text":"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."},{"id":"docs:extending/extensions/connectors#the-data-mount","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#the-data-mount","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"The data mount","text":"Data.class.json also declares a server mount — /_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."},{"id":"docs:extending/extensions/connectors#state-classes-and-lowering","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#state-classes-and-lowering","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"State classes and lowering","text":"Pages 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."},{"id":"docs:extending/extensions/connectors#grid-support","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#grid-support","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Grid support","text":"Studio's 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 ). Everything an editor touches — connections, table schemas, permissions, the push button — is documented for Studio users under Databases , Connections , and Data tables . This page describes the extension machinery those surfaces are built on."},{"id":"docs:extending/extensions/connectors#related","collection":"docs","slug":"extending/extensions/connectors","url":"/docs/extending/extensions/connectors/#related","title":"Connectors","description":"The connector block that makes a class a database connection provider, and how @jxsuite/connector implements it for D1, Supabase, and SQLite.","heading":"Related","text":"Server mounts — the /_jx/data mount and the shared context. Capability methods — roles, timing, and lowering. First-party extensions — how connector, auth, and parser fit together. Tutorial: a guestbook extension — a table-backed feature built on the connector."},{"id":"docs:extending/extensions/first-party","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"","text":"First-party extensions Four 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. Package Sections Formats Server mounts Connector providers @jxsuite/parser content Markdown, CSV — — @jxsuite/connector connections , data — /_jx/data (order 20) D1, Supabase, Sqlite @jxsuite/auth auth — /_jx/auth (order 10) — @jxsuite/search search — — — @jxsuite/parser — content and Markdown The content layer, and the model format extension . 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. 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). Classes : MarkdownCollection , ContentCollection , ContentEntry — glob-and-query state prototypes over content files. Schemas : the only first-party package shipping a document fragment as well as a project one — its content-source shapes join both composed schemas . Studio settings : the Content Types section, a layout: \"map\" master-detail with the schema-builder control for frontmatter fields. User-level docs: Content collections and Jx Markdown . @jxsuite/connector — connections and data tables The data layer, and the reference for both the connector block and a server mount . Sections : connections (named database connections; identifiers and env-var names only) and data (dynamic tables: column schema, id strategy, indexes, permissions, ownerField ; referenceable ). Providers : D1 , Supabase , and Sqlite classes, each with the four connector capabilities ( dialect , deploySchema , bindings , testConnection ) over a shared Kysely bridge. Mount : /_jx/data (order 20) — the canonical table wire contract, fail-closed against ctx.auth . Classes : TableQuery , TableEntry , TableInsert , TableUpdate , TableDelete — state prototypes that lower to core Request / Function defs in compiled sites. 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. User-level docs: Databases , Connections , Data tables , Data grid . @jxsuite/auth — sessions and permissions Better Auth behind the connector's tables, and the reference for mount cooperation through the shared context. Section : auth — sign-in methods, redirects, roles, and the connection its system tables ( user , session , account , verification ) live on. 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. Push contribution : a section-owner deploySchema capability contributes the system-table migration to jx db push as kind: \"auth\" steps ( Server mounts ). 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 . Studio settings : the Authentication section ( layout: \"form\" ), with the secret control for secretEnv — the signing secret itself never touches project.json ( Security and secrets ). User-level docs: Auth and secrets . @jxsuite/search — build-time site search Headless full-text search, and the reference for the emit capability . Section : search — which content collections to index, per-collection fields/boosts, and the output path. projectData normalizes it into _project.search . 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). Classes : Search — reactive query results in page state, lowered to a core Function computed that lazy-loads the bundled MiniSearch client. Defaults to client timing via $studio.stateDefaults . 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. Studio settings : the Site Search section ( layout: \"form\" ). User-level docs: Site search . How they depend on each other Auth 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. Related The anatomy of an extension — manifests, admission blocks, and the registry. Connectors and Server mounts — the machinery connector and auth are built on. Tutorial: a TOML format extension and Tutorial: a guestbook extension — build your own alongside these references."},{"id":"docs:extending/extensions/first-party#first-party-extensions","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#first-party-extensions","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"First-party extensions","text":"Four 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. Package Sections Formats Server mounts Connector providers @jxsuite/parser content Markdown, CSV — — @jxsuite/connector connections , data — /_jx/data (order 20) D1, Supabase, Sqlite @jxsuite/auth auth — /_jx/auth (order 10) — @jxsuite/search search — — —"},{"id":"docs:extending/extensions/first-party#jxsuiteparser-content-and-markdown","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#jxsuiteparser-content-and-markdown","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"@jxsuite/parser — content and Markdown","text":"The content layer, and the model format extension . 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. 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). Classes : MarkdownCollection , ContentCollection , ContentEntry — glob-and-query state prototypes over content files. Schemas : the only first-party package shipping a document fragment as well as a project one — its content-source shapes join both composed schemas . Studio settings : the Content Types section, a layout: \"map\" master-detail with the schema-builder control for frontmatter fields. User-level docs: Content collections and Jx Markdown ."},{"id":"docs:extending/extensions/first-party#jxsuiteconnector-connections-and-data-tables","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#jxsuiteconnector-connections-and-data-tables","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"@jxsuite/connector — connections and data tables","text":"The data layer, and the reference for both the connector block and a server mount . Sections : connections (named database connections; identifiers and env-var names only) and data (dynamic tables: column schema, id strategy, indexes, permissions, ownerField ; referenceable ). Providers : D1 , Supabase , and Sqlite classes, each with the four connector capabilities ( dialect , deploySchema , bindings , testConnection ) over a shared Kysely bridge. Mount : /_jx/data (order 20) — the canonical table wire contract, fail-closed against ctx.auth . Classes : TableQuery , TableEntry , TableInsert , TableUpdate , TableDelete — state prototypes that lower to core Request / Function defs in compiled sites. 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. User-level docs: Databases , Connections , Data tables , Data grid ."},{"id":"docs:extending/extensions/first-party#jxsuiteauth-sessions-and-permissions","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#jxsuiteauth-sessions-and-permissions","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"@jxsuite/auth — sessions and permissions","text":"Better Auth behind the connector's tables, and the reference for mount cooperation through the shared context. Section : auth — sign-in methods, redirects, roles, and the connection its system tables ( user , session , account , verification ) live on. 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. Push contribution : a section-owner deploySchema capability contributes the system-table migration to jx db push as kind: \"auth\" steps ( Server mounts ). 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 . Studio settings : the Authentication section ( layout: \"form\" ), with the secret control for secretEnv — the signing secret itself never touches project.json ( Security and secrets ). User-level docs: Auth and secrets ."},{"id":"docs:extending/extensions/first-party#jxsuitesearch-build-time-site-search","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#jxsuitesearch-build-time-site-search","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"@jxsuite/search — build-time site search","text":"Headless full-text search, and the reference for the emit capability . Section : search — which content collections to index, per-collection fields/boosts, and the output path. projectData normalizes it into _project.search . 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). Classes : Search — reactive query results in page state, lowered to a core Function computed that lazy-loads the bundled MiniSearch client. Defaults to client timing via $studio.stateDefaults . 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. Studio settings : the Site Search section ( layout: \"form\" ). User-level docs: Site search ."},{"id":"docs:extending/extensions/first-party#how-they-depend-on-each-other","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#how-they-depend-on-each-other","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"How they depend on each other","text":"Auth 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."},{"id":"docs:extending/extensions/first-party#related","collection":"docs","slug":"extending/extensions/first-party","url":"/docs/extending/extensions/first-party/#related","title":"First-party extensions","description":"What @jxsuite/parser, @jxsuite/connector, @jxsuite/auth, and @jxsuite/search each contribute: sections, formats, classes, mounts, and settings.","heading":"Related","text":"The anatomy of an extension — manifests, admission blocks, and the registry. Connectors and Server mounts — the machinery connector and auth are built on. Tutorial: a TOML format extension and Tutorial: a guestbook extension — build your own alongside these references."},{"id":"docs:extending/extensions/formats","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"","text":"Custom formats A 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. The format block The parser's Markdown.class.json declares, verbatim: \"format\" : { \"extensions\" : [ \".md\" ], \"mediaType\" : \"text/markdown\" , \"documentKinds\" : [ \"page\" , \"component\" , \"content\" ], \"exportTarget\" : true , \"remote\" : false } Key Type Default Meaning extensions string[] (required) — File extensions claimed, with leading dot. mediaType string — MIME type; used for icons, labels, HTTP responses. documentKinds (\"page\"|\"component\"|\"content\")[] [] page / component admit the extension into pages/components discovery globs; content admits it as a content source. exportTarget boolean false When true, site builds emit a serialized sidecar per page in this format (requires a serialize capability). remote boolean false When true, the load capability accepts http(s) URLs as sources. Remote content sources must name a remote-capable format explicitly. Two 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 . Format capabilities The block declares what the class handles; the class's capability methods declare how . Four roles belong to the format block: Role Signature Consumers parse (source, options?) → JxDocument compiler, server, Studio (open file) serialize (doc, options?) → string Studio (save), site build (export sidecars) discover (source, { baseDir }) → string[] content loading (list entry files) load (path, { schema, directiveOptions }) → ContentLoaderEntry[] content loading (parse one source) A 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. The 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 as every other class. How the pipeline dispatches Hosts never hard-code file types. Each one builds a format registry from the enabled extensions' manifests and routes by extension: 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. 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 . 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 ). Export sidecars — with exportTarget: true , the build serializes each page into the format next to its HTML output. Studio hints Format 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: \"$studio\" : { \"icon\" : \"markdown\" , \"modes\" : [ \"edit\" , \"design\" , \"preview\" , \"source\" ], \"documentMode\" : { \"default\" : \"content\" , \"componentWhen\" : { \"frontmatterKey\" : \"tagName\" , \"matches\" : \".+-.+\" } }, \"newFileTemplate\" : \"--- \\n title: Untitled \\n --- \\n\\n \" , \"elements\" : { \"block\" : [ \"h1\" , \"h2\" , \"h3\" , \"p\" , \"blockquote\" , \"ul\" , \"ol\" , \"li\" , \"pre\" , \"…\" ], \"inline\" : [ \"em\" , \"strong\" , \"del\" , \"code\" , \"a\" , \"img\" , \"br\" ], \"void\" : [ \"hr\" , \"br\" , \"img\" ], \"textOnly\" : [ \"code\" ], \"nesting\" : { \"h1\" : { \"block\" : false , \"inline\" : true , \"directive\" : false }, \"ul\" : { \"only\" : [ \"li\" ] }, \"…\" : {} } } } icon — file icon in the Files panel; modes — which canvas modes the format supports. 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\"). newFileTemplate — the seed content for New File in this format. 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. One 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. Related Tutorial: a TOML format extension — build a working format end to end Capability methods — timing and the options contract Content collections — the consumer of discover / load Jx Markdown — what the reference format's dialect looks like"},{"id":"docs:extending/extensions/formats#custom-formats","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#custom-formats","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"Custom formats","text":"A 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."},{"id":"docs:extending/extensions/formats#the-format-block","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#the-format-block","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"The format block","text":"The parser's Markdown.class.json declares, verbatim: \"format\" : { \"extensions\" : [ \".md\" ], \"mediaType\" : \"text/markdown\" , \"documentKinds\" : [ \"page\" , \"component\" , \"content\" ], \"exportTarget\" : true , \"remote\" : false } Key Type Default Meaning extensions string[] (required) — File extensions claimed, with leading dot. mediaType string — MIME type; used for icons, labels, HTTP responses. documentKinds (\"page\"|\"component\"|\"content\")[] [] page / component admit the extension into pages/components discovery globs; content admits it as a content source. exportTarget boolean false When true, site builds emit a serialized sidecar per page in this format (requires a serialize capability). remote boolean false When true, the load capability accepts http(s) URLs as sources. Remote content sources must name a remote-capable format explicitly. Two 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 ."},{"id":"docs:extending/extensions/formats#format-capabilities","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#format-capabilities","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"Format capabilities","text":"The block declares what the class handles; the class's capability methods declare how . Four roles belong to the format block: Role Signature Consumers parse (source, options?) → JxDocument compiler, server, Studio (open file) serialize (doc, options?) → string Studio (save), site build (export sidecars) discover (source, { baseDir }) → string[] content loading (list entry files) load (path, { schema, directiveOptions }) → ContentLoaderEntry[] content loading (parse one source) A 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. The 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 as every other class."},{"id":"docs:extending/extensions/formats#how-the-pipeline-dispatches","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#how-the-pipeline-dispatches","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"How the pipeline dispatches","text":"Hosts never hard-code file types. Each one builds a format registry from the enabled extensions' manifests and routes by extension: 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. 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 . 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 ). Export sidecars — with exportTarget: true , the build serializes each page into the format next to its HTML output."},{"id":"docs:extending/extensions/formats#studio-hints","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#studio-hints","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"Studio hints","text":"Format 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: \"$studio\" : { \"icon\" : \"markdown\" , \"modes\" : [ \"edit\" , \"design\" , \"preview\" , \"source\" ], \"documentMode\" : { \"default\" : \"content\" , \"componentWhen\" : { \"frontmatterKey\" : \"tagName\" , \"matches\" : \".+-.+\" } }, \"newFileTemplate\" : \"--- \\n title: Untitled \\n --- \\n\\n \" , \"elements\" : { \"block\" : [ \"h1\" , \"h2\" , \"h3\" , \"p\" , \"blockquote\" , \"ul\" , \"ol\" , \"li\" , \"pre\" , \"…\" ], \"inline\" : [ \"em\" , \"strong\" , \"del\" , \"code\" , \"a\" , \"img\" , \"br\" ], \"void\" : [ \"hr\" , \"br\" , \"img\" ], \"textOnly\" : [ \"code\" ], \"nesting\" : { \"h1\" : { \"block\" : false , \"inline\" : true , \"directive\" : false }, \"ul\" : { \"only\" : [ \"li\" ] }, \"…\" : {} } } } icon — file icon in the Files panel; modes — which canvas modes the format supports. 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\"). newFileTemplate — the seed content for New File in this format. 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. One 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."},{"id":"docs:extending/extensions/formats#related","collection":"docs","slug":"extending/extensions/formats","url":"/docs/extending/extensions/formats/#related","title":"Custom formats","description":"Claiming file extensions with the format block: parse, serialize, discover, and load capabilities, remote sources, and Studio mode hints.","heading":"Related","text":"Tutorial: a TOML format extension — build a working format end to end Capability methods — timing and the options contract Content collections — the consumer of discover / load Jx Markdown — what the reference format's dialect looks like"},{"id":"docs:extending/extensions/project-sections","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"","text":"Project sections and settings A 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. The project block The parser's Content.class.json declares, verbatim: \"project\" : { \"key\" : \"content\" , \"title\" : \"Content Types\" , \"description\" : \"File-based content collections loaded at build and dev-serve time\" , \"referenceable\" : true } Key Type Default Meaning key string — The project.json top-level property this class owns (single word by convention; exclusive across extensions). title string — Studio label. description string — Studio help text. referenceable boolean false Opts the section's named entries into the relationships vocabulary ( Relationships ). The section's value schema is not duplicated here — it lives in the package's project fragment (the manifest's schemas.project , see Schema composition ). Hosts that need the entry shape — Studio settings forms, reference pickers — read properties[<key>] from the fragment via the registry. Loading section data: projectData Behavior attaches through capability methods on the same class. The projectData capability turns the raw section value into loaded data: projectData(sectionValue, { projectConfig, root, registry, io }) → unknown The 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). Expanding routes: resolvePaths and discriminators A 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: { \"$paths\" : { \"contentType\" : \"blog\" , \"param\" : \"slug\" } } dispatches 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 for the authoring side. Publishing files: assets If your section reads from directories, the assets capability publishes them at a site URL so the files beside its sources are reachable: assets(sectionValue, { projectConfig, root }) → [{ urlPrefix: \"/content/blog\", dir: \"/abs/content/blog\" }] The 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 . Studio settings: $studio.settings The section class's $studio block may declare a settings section, rendered generically by Studio under Settings . The auth extension's, verbatim: \"$studio\" : { \"settings\" : { \"icon\" : \"sp-icon-lock-closed\" , \"label\" : \"Authentication\" , \"order\" : 58 , \"layout\" : \"form\" , \"entry\" : { \"ui\" : { \"connection\" : { \"enum\" : { \"$ref\" : \"#/$context/connections\" } }, \"secretEnv\" : { \"control\" : \"secret\" } } } } } Key Meaning icon Section icon in the settings nav. label Section label (defaults to project.title ). order Sort position among contributed sections. layout \"form\" (default) — one form over the whole section value. \"map\" — master-detail for type: object + additionalProperties sections: key list left, entry form right. entry.ui Per-field control overrides for the entry form: { \"<field>\": { \"control\": \"<name>\" } } . entry.newEntry Template for freshly created entries, with ${key} substitution. renderer Escape hatch: names a Studio-registered custom section renderer. First-party extensions use the generic path. Everything 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. The 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 surface — no parser-specific Studio code exists. Controls and dynamic enums Built-in controls you can name in entry.ui : \"schema-builder\" — the visual JSON-Schema field editor. \"secret\" — the value is committed via the platform's secret store, never project.json. \"binding\" — signal/route-param binding. Implicit defaults per type, enum, and format cover everything else. Enum 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. Secrets 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 . Related Schema composition — where the section's value schema lives Capability methods — projectData and resolvePaths contracts Server mounts — giving a section a route subtree Project settings in Studio — what users see"},{"id":"docs:extending/extensions/project-sections#project-sections-and-settings","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#project-sections-and-settings","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Project sections and settings","text":"A 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."},{"id":"docs:extending/extensions/project-sections#the-project-block","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#the-project-block","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"The project block","text":"The parser's Content.class.json declares, verbatim: \"project\" : { \"key\" : \"content\" , \"title\" : \"Content Types\" , \"description\" : \"File-based content collections loaded at build and dev-serve time\" , \"referenceable\" : true } Key Type Default Meaning key string — The project.json top-level property this class owns (single word by convention; exclusive across extensions). title string — Studio label. description string — Studio help text. referenceable boolean false Opts the section's named entries into the relationships vocabulary ( Relationships ). The section's value schema is not duplicated here — it lives in the package's project fragment (the manifest's schemas.project , see Schema composition ). Hosts that need the entry shape — Studio settings forms, reference pickers — read properties[<key>] from the fragment via the registry."},{"id":"docs:extending/extensions/project-sections#loading-section-data-projectdata","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#loading-section-data-projectdata","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Loading section data: projectData","text":"Behavior attaches through capability methods on the same class. The projectData capability turns the raw section value into loaded data: projectData(sectionValue, { projectConfig, root, registry, io }) → unknown The 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)."},{"id":"docs:extending/extensions/project-sections#expanding-routes-resolvepaths-and-discriminators","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#expanding-routes-resolvepaths-and-discriminators","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Expanding routes: resolvePaths and discriminators","text":"A 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: { \"$paths\" : { \"contentType\" : \"blog\" , \"param\" : \"slug\" } } dispatches 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 for the authoring side."},{"id":"docs:extending/extensions/project-sections#publishing-files-assets","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#publishing-files-assets","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Publishing files: assets","text":"If your section reads from directories, the assets capability publishes them at a site URL so the files beside its sources are reachable: assets(sectionValue, { projectConfig, root }) → [{ urlPrefix: \"/content/blog\", dir: \"/abs/content/blog\" }] The 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 ."},{"id":"docs:extending/extensions/project-sections#studio-settings-studiosettings","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#studio-settings-studiosettings","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Studio settings: $studio.settings","text":"The section class's $studio block may declare a settings section, rendered generically by Studio under Settings . The auth extension's, verbatim: \"$studio\" : { \"settings\" : { \"icon\" : \"sp-icon-lock-closed\" , \"label\" : \"Authentication\" , \"order\" : 58 , \"layout\" : \"form\" , \"entry\" : { \"ui\" : { \"connection\" : { \"enum\" : { \"$ref\" : \"#/$context/connections\" } }, \"secretEnv\" : { \"control\" : \"secret\" } } } } } Key Meaning icon Section icon in the settings nav. label Section label (defaults to project.title ). order Sort position among contributed sections. layout \"form\" (default) — one form over the whole section value. \"map\" — master-detail for type: object + additionalProperties sections: key list left, entry form right. entry.ui Per-field control overrides for the entry form: { \"<field>\": { \"control\": \"<name>\" } } . entry.newEntry Template for freshly created entries, with ${key} substitution. renderer Escape hatch: names a Studio-registered custom section renderer. First-party extensions use the generic path. Everything 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. The 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 surface — no parser-specific Studio code exists."},{"id":"docs:extending/extensions/project-sections#controls-and-dynamic-enums","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#controls-and-dynamic-enums","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Controls and dynamic enums","text":"Built-in controls you can name in entry.ui : \"schema-builder\" — the visual JSON-Schema field editor. \"secret\" — the value is committed via the platform's secret store, never project.json. \"binding\" — signal/route-param binding. Implicit defaults per type, enum, and format cover everything else. Enum 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. Secrets 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 ."},{"id":"docs:extending/extensions/project-sections#related","collection":"docs","slug":"extending/extensions/project-sections","url":"/docs/extending/extensions/project-sections/#related","title":"Project sections and settings","description":"Owning a project.json section: the project block, projectData and resolvePaths capabilities, and contributing a settings section to Studio.","heading":"Related","text":"Schema composition — where the section's value schema lives Capability methods — projectData and resolvePaths contracts Server mounts — giving a section a route subtree Project settings in Studio — what users see"},{"id":"docs:extending/extensions/schema-composition","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/","title":"Schema composition","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.","heading":"","text":"Schema composition A 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. Fragments Two kinds of fragment feed the composition: 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 ). The core fragment is open — closure happens in the generated entry document. Each extension ships the fragments named in its manifest '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 . The parser's project fragment, abbreviated — it contributes the content section: { \"$id\" : \"https://jxsuite.com/schema/ext/parser/project/v1\" , \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"type\" : \"object\" , \"properties\" : { \"content\" : { \"type\" : \"object\" , \"additionalProperties\" : { \"type\" : \"object\" , \"properties\" : { \"source\" : { \"type\" : \"string\" }, \"format\" : { \"type\" : \"string\" }, \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"properties\" : { \"additionalProperties\" : { \"$ref\" : \"https://jxsuite.com/schema/project/fields/v2\" } } } } } } } } } Note 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. Generated entry documents 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): { \"$comment\" : \"Generated by `jx schema` from project.json#/extensions — do not edit.\" , \"$defs\" : { \"Fields\" : { \"anyOf\" : [ { \"$ref\" : \"#/$defs/project-core-v2/$defs/JxFieldSchema\" }, { \"$ref\" : \"#/$defs/project-core-v2/$defs/RelationshipRef\" } ] }, \"project-core-v2\" : { \"…\" : \"embedded core fragment\" }, \"ext-parser-project-v1\" : { \"…\" : \"embedded parser fragment\" } }, \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"allOf\" : [{ \"$ref\" : \"#/$defs/project-core-v2\" }, { \"$ref\" : \"#/$defs/ext-parser-project-v1\" }], \"type\" : \"object\" , \"unevaluatedProperties\" : false } Top-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. Each $defs key is a slug of the embedded resource's $id — https://jxsuite.com/schema/project/core/v2 becomes project-core-v2 . 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). project.json binds via \"$schema\": \"./project.schema.json\" . The two union resources Two positions are open recursion points where a fragment must reference the effective union without knowing what other extensions contribute: Resource $id Position Shipped default → entry-document union 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. 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. The 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. An 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. $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 . Validation 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. 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. 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. Root 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. Regenerating Run jx schema whenever the extensions list changes (and re-run jx validate after). Regeneration also happens without the CLI: Studio regenerates the entry documents when settings are saved. 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). The outputs are committed artifacts — check project.schema.json and document.schema.json into the repo so editors and CI validate without a build step. Related CLI commands — jx schema and jx validate Extension anatomy — where fragments are declared project.json — the document being validated Project sections and settings — the behavior side of a contributed section"},{"id":"docs:extending/extensions/schema-composition#schema-composition","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#schema-composition","title":"Schema composition","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.","heading":"Schema composition","text":"A 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."},{"id":"docs:extending/extensions/schema-composition#fragments","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#fragments","title":"Schema composition","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.","heading":"Fragments","text":"Two kinds of fragment feed the composition: 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 ). The core fragment is open — closure happens in the generated entry document. Each extension ships the fragments named in its manifest '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 . The parser's project fragment, abbreviated — it contributes the content section: { \"$id\" : \"https://jxsuite.com/schema/ext/parser/project/v1\" , \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"type\" : \"object\" , \"properties\" : { \"content\" : { \"type\" : \"object\" , \"additionalProperties\" : { \"type\" : \"object\" , \"properties\" : { \"source\" : { \"type\" : \"string\" }, \"format\" : { \"type\" : \"string\" }, \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"properties\" : { \"additionalProperties\" : { \"$ref\" : \"https://jxsuite.com/schema/project/fields/v2\" } } } } } } } } } Note 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."},{"id":"docs:extending/extensions/schema-composition#generated-entry-documents","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#generated-entry-documents","title":"Schema composition","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.","heading":"Generated entry documents","text":"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): { \"$comment\" : \"Generated by `jx schema` from project.json#/extensions — do not edit.\" , \"$defs\" : { \"Fields\" : { \"anyOf\" : [ { \"$ref\" : \"#/$defs/project-core-v2/$defs/JxFieldSchema\" }, { \"$ref\" : \"#/$defs/project-core-v2/$defs/RelationshipRef\" } ] }, \"project-core-v2\" : { \"…\" : \"embedded core fragment\" }, \"ext-parser-project-v1\" : { \"…\" : \"embedded parser fragment\" } }, \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"allOf\" : [{ \"$ref\" : \"#/$defs/project-core-v2\" }, { \"$ref\" : \"#/$defs/ext-parser-project-v1\" }], \"type\" : \"object\" , \"unevaluatedProperties\" : false } Top-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. Each $defs key is a slug of the embedded resource's $id — https://jxsuite.com/schema/project/core/v2 becomes project-core-v2 . 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). project.json binds via \"$schema\": \"./project.schema.json\" ."},{"id":"docs:extending/extensions/schema-composition#the-two-union-resources","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#the-two-union-resources","title":"Schema composition","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.","heading":"The two union resources","text":"Two positions are open recursion points where a fragment must reference the effective union without knowing what other extensions contribute: Resource $id Position Shipped default → entry-document union 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. 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. The 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. An 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. $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 ."},{"id":"docs:extending/extensions/schema-composition#validation","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#validation","title":"Schema composition","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.","heading":"Validation","text":"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. 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. 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. Root 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."},{"id":"docs:extending/extensions/schema-composition#regenerating","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#regenerating","title":"Schema composition","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.","heading":"Regenerating","text":"Run jx schema whenever the extensions list changes (and re-run jx validate after). Regeneration also happens without the CLI: Studio regenerates the entry documents when settings are saved. 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). The outputs are committed artifacts — check project.schema.json and document.schema.json into the repo so editors and CI validate without a build step."},{"id":"docs:extending/extensions/schema-composition#related","collection":"docs","slug":"extending/extensions/schema-composition","url":"/docs/extending/extensions/schema-composition/#related","title":"Schema composition","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.","heading":"Related","text":"CLI commands — jx schema and jx validate Extension anatomy — where fragments are declared project.json — the document being validated Project sections and settings — the behavior side of a contributed section"},{"id":"docs:extending/extensions/search","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"","text":"Search indexes @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. If you want to add search to a site rather than study the extension, start with the user-level Site search page. What the manifest declares { \"name\" : \"@jxsuite/search\" , \"classes\" : { \"SearchIndex\" : \"./src/SearchIndex.class.json\" , \"Search\" : \"./src/Search.class.json\" }, \"schemas\" : { \"project\" : \"./schemas/project.fragment.schema.json\" } } Two classes, one schema fragment: SearchIndex owns the search section ( project.key: \"search\" ) and carries two capabilities: projectData (normalizes the section into _project.search ) and emit (builds the index). Search is a state class pages query results through; it compiles away via lower . The fragment contributes the search property to the composed project schema : an engine (an enum of minisearch today, room for more), an output path, and per-collection index settings. The emit capability 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: emit (sectionValue, { projectConfig, root, sections, routes }) → [{ path: \"/search-index.json\" , content: \"…json…\" }] SearchIndex.emit walks each configured collection in sections.content and produces two document granularities per entry: a page document — title and description from frontmatter, full text extracted from the entry's rendered $children ; 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 ). The result is one JSON envelope at the configured output path: { \"version\" : 1 , \"engine\" : \"minisearch\" , \"fields\" : [ \"title\" , \"heading\" , \"text\" ], \"boost\" : { \"title\" : 4 , \"heading\" : 2 }, \"documents\" : [ { \"id\" : \"docs:framework/site\" , \"url\" : \"/docs/framework/site/\" , \"heading\" : \"\" , \"text\" : \"…\" }, { \"id\" : \"docs:framework/site#assets\" , \"url\" : \"/docs/framework/site/#assets\" , \"heading\" : \"Assets\" , \"text\" : \"…\" } ] } The 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. Lowering Search to client code A page declares reactive results with the Search prototype: \"state\" : { \"q\" : \"\" , \"results\" : { \"$prototype\" : \"Search\" , \"query\" : { \"$ref\" : \"#/state/q\" }, \"timing\" : \"client\" } } In 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): { \"$bundle\" : [ \"npm:@jxsuite/search/client\" ], \"$prototype\" : \"Function\" , \"timing\" : \"client\" , \"body\" : \"…import('/assets/jxsuite-search-client.js') … m.query(state.q, {…})…\" } The 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. Inside compiled components , use the client's $src surface instead — components are not lowered; see Site search . Under node 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. The 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."},{"id":"docs:extending/extensions/search#search-indexes","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/#search-indexes","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"Search indexes","text":"@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. If you want to add search to a site rather than study the extension, start with the user-level Site search page."},{"id":"docs:extending/extensions/search#what-the-manifest-declares","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/#what-the-manifest-declares","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"What the manifest declares","text":"{ \"name\" : \"@jxsuite/search\" , \"classes\" : { \"SearchIndex\" : \"./src/SearchIndex.class.json\" , \"Search\" : \"./src/Search.class.json\" }, \"schemas\" : { \"project\" : \"./schemas/project.fragment.schema.json\" } } Two classes, one schema fragment: SearchIndex owns the search section ( project.key: \"search\" ) and carries two capabilities: projectData (normalizes the section into _project.search ) and emit (builds the index). Search is a state class pages query results through; it compiles away via lower . The fragment contributes the search property to the composed project schema : an engine (an enum of minisearch today, room for more), an output path, and per-collection index settings."},{"id":"docs:extending/extensions/search#the-emit-capability","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/#the-emit-capability","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"The emit capability","text":"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: emit (sectionValue, { projectConfig, root, sections, routes }) → [{ path: \"/search-index.json\" , content: \"…json…\" }] SearchIndex.emit walks each configured collection in sections.content and produces two document granularities per entry: a page document — title and description from frontmatter, full text extracted from the entry's rendered $children ; 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 ). The result is one JSON envelope at the configured output path: { \"version\" : 1 , \"engine\" : \"minisearch\" , \"fields\" : [ \"title\" , \"heading\" , \"text\" ], \"boost\" : { \"title\" : 4 , \"heading\" : 2 }, \"documents\" : [ { \"id\" : \"docs:framework/site\" , \"url\" : \"/docs/framework/site/\" , \"heading\" : \"\" , \"text\" : \"…\" }, { \"id\" : \"docs:framework/site#assets\" , \"url\" : \"/docs/framework/site/#assets\" , \"heading\" : \"Assets\" , \"text\" : \"…\" } ] } The 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."},{"id":"docs:extending/extensions/search#lowering-search-to-client-code","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/#lowering-search-to-client-code","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"Lowering Search to client code","text":"A page declares reactive results with the Search prototype: \"state\" : { \"q\" : \"\" , \"results\" : { \"$prototype\" : \"Search\" , \"query\" : { \"$ref\" : \"#/state/q\" }, \"timing\" : \"client\" } } In 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): { \"$bundle\" : [ \"npm:@jxsuite/search/client\" ], \"$prototype\" : \"Function\" , \"timing\" : \"client\" , \"body\" : \"…import('/assets/jxsuite-search-client.js') … m.query(state.q, {…})…\" } The 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. Inside compiled components , use the client's $src surface instead — components are not lowered; see Site search ."},{"id":"docs:extending/extensions/search#under-node","collection":"docs","slug":"extending/extensions/search","url":"/docs/extending/extensions/search/#under-node","title":"Search indexes","description":"How @jxsuite/search owns the search section, emits a build-time index with the emit capability, and lowers Search defs to client code.","heading":"Under node","text":"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. The 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."},{"id":"docs:extending/extensions/security","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"","text":"Security and secrets Extensions 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. Names ride the wire, values never Secrets never enter project.json . Committed config carries identifiers and env-var names only: { \"connections\" : { \"main\" : { \"provider\" : \"supabase\" , \"urlEnv\" : \"SUPABASE_DB_URL\" } }, \"auth\" : { \"secretEnv\" : \"BETTER_AUTH_SECRET\" } } The 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. Where the values actually live: 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 ). Production — wrangler secret put <NAME> , or the hosting platform's secret store. 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 writes there, never to project.json . The user-facing walkthrough is Auth and secrets . Permission boundaries Two layers of authorization exist on a running site, and they are deliberately different: 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 . 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. 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 ). A 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. What extensions may do Own a basePath under /_jx/ and serve any routes inside it. Conflicts are a registry error, so no extension can shadow another's subtree. Read secret values from env at request time, and publish cooperation hooks on the shared context for later mounts ( ctx.auth is the model). Contribute steps to the schema push via deploySchema , tagged with their own section key. Declare a \"secret\" control in $studio.settings so Studio routes the value to the secret store. What extensions may not do Write secret values into project.json , return them from projectData , or accept them through mount options — the generated worker inlines options as committed JSON. Claim routes outside /_jx/ , or a basePath another extension owns. Grant access when their auth dependency is missing — fail closed, always. 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. Related Server mounts — the shared context and the fail-closed rule in situ. Connectors — how connections keep secrets out of committed config. Auth and secrets — the same model from the Studio user's chair. Protocol route reference — the /__studio/* surface, including secrets and data routes."},{"id":"docs:extending/extensions/security#security-and-secrets","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#security-and-secrets","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"Security and secrets","text":"Extensions 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."},{"id":"docs:extending/extensions/security#names-ride-the-wire-values-never","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#names-ride-the-wire-values-never","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"Names ride the wire, values never","text":"Secrets never enter project.json . Committed config carries identifiers and env-var names only: { \"connections\" : { \"main\" : { \"provider\" : \"supabase\" , \"urlEnv\" : \"SUPABASE_DB_URL\" } }, \"auth\" : { \"secretEnv\" : \"BETTER_AUTH_SECRET\" } } The 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. Where the values actually live: 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 ). Production — wrangler secret put <NAME> , or the hosting platform's secret store. 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 writes there, never to project.json . The user-facing walkthrough is Auth and secrets ."},{"id":"docs:extending/extensions/security#permission-boundaries","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#permission-boundaries","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"Permission boundaries","text":"Two layers of authorization exist on a running site, and they are deliberately different: 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 . 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. 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 ). A 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."},{"id":"docs:extending/extensions/security#what-extensions-may-do","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#what-extensions-may-do","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"What extensions may do","text":"Own a basePath under /_jx/ and serve any routes inside it. Conflicts are a registry error, so no extension can shadow another's subtree. Read secret values from env at request time, and publish cooperation hooks on the shared context for later mounts ( ctx.auth is the model). Contribute steps to the schema push via deploySchema , tagged with their own section key. Declare a \"secret\" control in $studio.settings so Studio routes the value to the secret store."},{"id":"docs:extending/extensions/security#what-extensions-may-not-do","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#what-extensions-may-not-do","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"What extensions may not do","text":"Write secret values into project.json , return them from projectData , or accept them through mount options — the generated worker inlines options as committed JSON. Claim routes outside /_jx/ , or a basePath another extension owns. Grant access when their auth dependency is missing — fail closed, always. 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."},{"id":"docs:extending/extensions/security#related","collection":"docs","slug":"extending/extensions/security","url":"/docs/extending/extensions/security/#related","title":"Security and secrets","description":"The extension security model: secret names ride the wire, values never do — plus the permission boundaries extensions and hosts must respect.","heading":"Related","text":"Server mounts — the shared context and the fail-closed rule in situ. Connectors — how connections keep secrets out of committed config. Auth and secrets — the same model from the Studio user's chair. Protocol route reference — the /__studio/* surface, including secrets and data routes."},{"id":"docs:extending/extensions/server","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/","title":"Server mounts","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.","heading":"","text":"Server mounts An 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 and dispatch requests to the handler your mount method returns. This 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. The server block The auth extension's section class, extensions/auth/src/Auth.class.json , declares its mount in one line: \"server\" : { \"basePath\" : \"/_jx/auth\" , \"order\" : 10 , \"module\" : \"@jxsuite/auth/worker\" } Key Meaning basePath The route subtree this mount owns. Must be under /_jx/ . Conflicts are a registry error. order Mount order (ascending). Earlier mounts run first and may populate the shared context. module Bare specifier the generated worker imports (robust under bundlers); falls back to $implementation . Every 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. The mount capability mount is a static capability method with timing: [\"server\"] . It returns a fetch-style handler : ( request : Request , env : Record < string , unknown >) => Promise < Response > ; You 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. mount(options, ctx) receives: 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 . ctx — the shared server context, described below. Here is the auth extension's real mount, abbreviated from extensions/auth/src/worker.ts : export const Auth = { mount ( options : AuthMountOptions , ctx : JxServerContext ) { const state = createAuthMountState (); ctx.auth = { authorize : ( input ) => Promise . resolve ( evaluatePermission (input)), getSession : async ( request , env ) => { try { const auth = await getAuthForEnv (state, options, env); return await getSessionContext (auth, request); } catch { return null ; // fail-closed: an unreachable auth backend means signed out } }, }; return ( request : Request , env : Env ) : Promise < Response > => handleAuthRequest (request, env, options, state); }, }; The 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. Mount order and the shared context Each worker isolate creates one mutable JxServerContext object and passes it to every mount in ascending order . Mounts communicate through it: interface JxServerContext { auth ?: { getSession ( request : Request , env : Record < string , unknown >) : Promise < SessionInfo | null >; authorize ( input : AuthorizeInput , env : Record < string , unknown >) : Promise < AuthorizeDecision >; }; [ key : string ] : unknown ; } The 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. 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. The connector's data mount is the canonical consumer, serving the wire contract every table rides on: GET /_jx/data/:table ?filter=<json>&sort=<json>&limit=&offset=&include= GET /_jx/data/:table/:id POST /_jx/data/:table PATCH /_jx/data/:table/:id DELETE /_jx/data/:table/:id The 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 . Section-owner deploySchema Connector classes declare a deploySchema capability to sync tables ( Connectors ). A non-connector project-section class 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: deploySchema(sectionValue, projectConfig, { env, dryRun?, connection?, connectors? }) → { steps, applied, warnings, connection } 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. connectors carries the same provider stand-ins the mounts receive, so dev pushes hit the local: stand-in databases. When a push is filtered to a connection the section does not live on, return empty steps. Hosts 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. Related Capability methods — roles, timing , and how hosts invoke statics. Security and secrets — why mount options carry identifiers only. Tutorial: a guestbook extension — a mount built end to end. Protocol route reference — the /__studio/* routes, a separate surface from /_jx/* mounts."},{"id":"docs:extending/extensions/server#server-mounts","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#server-mounts","title":"Server mounts","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.","heading":"Server mounts","text":"An 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 and dispatch requests to the handler your mount method returns. This 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."},{"id":"docs:extending/extensions/server#the-server-block","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#the-server-block","title":"Server mounts","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.","heading":"The server block","text":"The auth extension's section class, extensions/auth/src/Auth.class.json , declares its mount in one line: \"server\" : { \"basePath\" : \"/_jx/auth\" , \"order\" : 10 , \"module\" : \"@jxsuite/auth/worker\" } Key Meaning basePath The route subtree this mount owns. Must be under /_jx/ . Conflicts are a registry error. order Mount order (ascending). Earlier mounts run first and may populate the shared context. module Bare specifier the generated worker imports (robust under bundlers); falls back to $implementation . Every 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."},{"id":"docs:extending/extensions/server#the-mount-capability","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#the-mount-capability","title":"Server mounts","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.","heading":"The mount capability","text":"mount is a static capability method with timing: [\"server\"] . It returns a fetch-style handler : ( request : Request , env : Record < string , unknown >) => Promise < Response > ; You 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. mount(options, ctx) receives: 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 . ctx — the shared server context, described below. Here is the auth extension's real mount, abbreviated from extensions/auth/src/worker.ts : export const Auth = { mount ( options : AuthMountOptions , ctx : JxServerContext ) { const state = createAuthMountState (); ctx.auth = { authorize : ( input ) => Promise . resolve ( evaluatePermission (input)), getSession : async ( request , env ) => { try { const auth = await getAuthForEnv (state, options, env); return await getSessionContext (auth, request); } catch { return null ; // fail-closed: an unreachable auth backend means signed out } }, }; return ( request : Request , env : Env ) : Promise < Response > => handleAuthRequest (request, env, options, state); }, }; The 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."},{"id":"docs:extending/extensions/server#mount-order-and-the-shared-context","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#mount-order-and-the-shared-context","title":"Server mounts","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.","heading":"Mount order and the shared context","text":"Each worker isolate creates one mutable JxServerContext object and passes it to every mount in ascending order . Mounts communicate through it: interface JxServerContext { auth ?: { getSession ( request : Request , env : Record < string , unknown >) : Promise < SessionInfo | null >; authorize ( input : AuthorizeInput , env : Record < string , unknown >) : Promise < AuthorizeDecision >; }; [ key : string ] : unknown ; } The 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. 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. The connector's data mount is the canonical consumer, serving the wire contract every table rides on: GET /_jx/data/:table ?filter=<json>&sort=<json>&limit=&offset=&include= GET /_jx/data/:table/:id POST /_jx/data/:table PATCH /_jx/data/:table/:id DELETE /_jx/data/:table/:id The 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 ."},{"id":"docs:extending/extensions/server#section-owner-deployschema","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#section-owner-deployschema","title":"Server mounts","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.","heading":"Section-owner deploySchema","text":"Connector classes declare a deploySchema capability to sync tables ( Connectors ). A non-connector project-section class 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: deploySchema(sectionValue, projectConfig, { env, dryRun?, connection?, connectors? }) → { steps, applied, warnings, connection } 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. connectors carries the same provider stand-ins the mounts receive, so dev pushes hit the local: stand-in databases. When a push is filtered to a connection the section does not live on, return empty steps. Hosts 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."},{"id":"docs:extending/extensions/server#related","collection":"docs","slug":"extending/extensions/server","url":"/docs/extending/extensions/server/#related","title":"Server mounts","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.","heading":"Related","text":"Capability methods — roles, timing , and how hosts invoke statics. Security and secrets — why mount options carry identifiers only. Tutorial: a guestbook extension — a mount built end to end. Protocol route reference — the /__studio/* routes, a separate surface from /_jx/* mounts."},{"id":"docs:extending/extensions/tutorial-guestbook","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"","text":"Tutorial: a guestbook extension This 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 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. Prerequisites : Project sections , Server mounts , and Connectors ; a test project with @jxsuite/connector and @jxsuite/auth already enabled ( Databases ). The 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. 1. Manifest and fragment Scaffold the package the same way as the TOML tutorial : 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 listed under schemas.project . The 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: \"project\" : { \"key\" : \"guestbook\" }, \"$studio\" : { \"settings\" : { \"layout\" : \"form\" } } You 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. 2. The table The guestbook stores entries in an ordinary connector table instead of hand-rolled SQL. Declare a projectData capability 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: { \"permissions\" : { \"read\" : \"public\" , \"insert\" : \"authenticated\" }, \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"name\" : { \"type\" : \"string\" }, \"message\" : { \"type\" : \"string\" } } } } Because 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 doubles as your moderation console. No custom SQL anywhere. 3. The mount Moderation needs server-side logic, so the class also declares a server block : \"server\" : { \"basePath\" : \"/_jx/guestbook\" , \"order\" : 30 } Order 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 . You should now see jx dev answering under /_jx/guestbook , with the same behavior the generated worker will have in production. 4. The page The 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 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. 5. Ship it A project adds the package and syncs: { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"@jxsuite/auth\" , \"@acme/jx-guestbook\" ] } Then 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. What you built Piece Mechanism First-party reference guestbook section project block + schema fragment extensions/connector/src/Data.class.json Settings form $studio.settings ( layout: \"form\" ) extensions/auth/src/Auth.class.json Entries table projectData → connector table definition extensions/connector/src/Data.class.json /_jx/guestbook server block + mount , order 30 extensions/auth/src/worker.ts Page state TableQuery / TableInsert , lowered extensions/connector/src/TableQuery.class.json Next steps Security and secrets — the fail-closed rules your mount inherits. Server mounts — the shared-context contract in full. First-party extensions — the three packages this tutorial leaned on."},{"id":"docs:extending/extensions/tutorial-guestbook#tutorial-a-guestbook-extension","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#tutorial-a-guestbook-extension","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"Tutorial: a guestbook extension","text":"This 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 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. Prerequisites : Project sections , Server mounts , and Connectors ; a test project with @jxsuite/connector and @jxsuite/auth already enabled ( Databases ). The 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."},{"id":"docs:extending/extensions/tutorial-guestbook#1-manifest-and-fragment","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#1-manifest-and-fragment","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"1. Manifest and fragment","text":"Scaffold the package the same way as the TOML tutorial : 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 listed under schemas.project . The 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: \"project\" : { \"key\" : \"guestbook\" }, \"$studio\" : { \"settings\" : { \"layout\" : \"form\" } } You 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."},{"id":"docs:extending/extensions/tutorial-guestbook#2-the-table","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#2-the-table","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"2. The table","text":"The guestbook stores entries in an ordinary connector table instead of hand-rolled SQL. Declare a projectData capability 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: { \"permissions\" : { \"read\" : \"public\" , \"insert\" : \"authenticated\" }, \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"name\" : { \"type\" : \"string\" }, \"message\" : { \"type\" : \"string\" } } } } Because 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 doubles as your moderation console. No custom SQL anywhere."},{"id":"docs:extending/extensions/tutorial-guestbook#3-the-mount","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#3-the-mount","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"3. The mount","text":"Moderation needs server-side logic, so the class also declares a server block : \"server\" : { \"basePath\" : \"/_jx/guestbook\" , \"order\" : 30 } Order 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 . You should now see jx dev answering under /_jx/guestbook , with the same behavior the generated worker will have in production."},{"id":"docs:extending/extensions/tutorial-guestbook#4-the-page","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#4-the-page","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"4. The page","text":"The 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 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."},{"id":"docs:extending/extensions/tutorial-guestbook#5-ship-it","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#5-ship-it","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"5. Ship it","text":"A project adds the package and syncs: { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"@jxsuite/auth\" , \"@acme/jx-guestbook\" ] } Then 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."},{"id":"docs:extending/extensions/tutorial-guestbook#what-you-built","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#what-you-built","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"What you built","text":"Piece Mechanism First-party reference guestbook section project block + schema fragment extensions/connector/src/Data.class.json Settings form $studio.settings ( layout: \"form\" ) extensions/auth/src/Auth.class.json Entries table projectData → connector table definition extensions/connector/src/Data.class.json /_jx/guestbook server block + mount , order 30 extensions/auth/src/worker.ts Page state TableQuery / TableInsert , lowered extensions/connector/src/TableQuery.class.json"},{"id":"docs:extending/extensions/tutorial-guestbook#next-steps","collection":"docs","slug":"extending/extensions/tutorial-guestbook","url":"/docs/extending/extensions/tutorial-guestbook/#next-steps","title":"Tutorial: a guestbook extension","description":"Assemble a moderated, auth-gated guestbook from extension parts: a project section, a dynamic table, a /_jx/ server mount, and lowered page actions.","heading":"Next steps","text":"Security and secrets — the fail-closed rules your mount inherits. Server mounts — the shared-context contract in full. First-party extensions — the three packages this tutorial leaned on."},{"id":"docs:extending/extensions/tutorial-toml-format","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"","text":"Tutorial: a TOML format extension By 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. Prerequisites : you have read The anatomy of an extension and Formats , and you have a Jx site project to test against ( Your first project ). 1. Scaffold the package Create a package with a jx field pointing at the manifest, and export the manifest and class descriptor alongside the code: { \"name\" : \"@acme/jx-toml\" , \"type\" : \"module\" , \"jx\" : \"./jx-extension.json\" , \"files\" : [ \"src/\" , \"jx-extension.json\" ], \"exports\" : { \".\" : \"./src/toml.ts\" , \"./jx-extension.json\" : \"./jx-extension.json\" , \"./Toml.class.json\" : \"./src/Toml.class.json\" } } The \"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 . 2. Write the manifest jx-extension.json at the package root enumerates what the package provides — here, a single class: { \"name\" : \"@acme/jx-toml\" , \"title\" : \"TOML\" , \"classes\" : { \"Toml\" : \"./src/Toml.class.json\" } } The 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. 3. Declare the format class src/Toml.class.json is an ordinary Jx class descriptor. Its top-level format block is the admission block that puts it into file-extension dispatch: { \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"title\" : \"Toml\" , \"description\" : \"TOML content files as Jx content entries\" , \"$prototype\" : \"Class\" , \"$implementation\" : \"./toml.js\" , \"format\" : { \"extensions\" : [ \".toml\" ], \"mediaType\" : \"application/toml\" , \"documentKinds\" : [ \"content\" ] } } 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. $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. 4. Declare the constructor and runtime access Next, the class's $defs describe its instance surface — the standard external-class contract of a config -taking constructor plus an instance resolve() : \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } } }, \"constructor\" : { \"role\" : \"constructor\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }], \"body\" : [ \"this.config = config;\" ] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"scope\" : \"instance\" , \"identifier\" : \"resolve\" , \"returnType\" : { \"type\" : \"object\" } } } } This is what makes { \"$prototype\": \"Toml\", \"src\": \"./x.toml\" } work as a state entry at runtime. 5. Declare the parse capability Capability methods are static methods declared by well-known role values, alongside resolve in $defs.methods : \"parse\" : { \"role\" : \"parse\" , \"scope\" : \"static\" , \"identifier\" : \"parse\" , \"timing\" : [ \"compiler\" , \"server\" , \"client\" ], \"parameters\" : [{ \"identifier\" : \"source\" , \"type\" : { \"type\" : \"string\" } }], \"returnType\" : { \"type\" : \"object\" } } timing includes \"client\" because TOML parsing needs no filesystem — declaring it lets Studio parse in-process instead of round-tripping through the dev server. 6. Declare the content capabilities discover lists entry files for a content source; load parses one source into entries. Both are node-only, so timing stops at \"server\" : \"discover\" : { \"role\" : \"discover\" , \"scope\" : \"static\" , \"identifier\" : \"discover\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"source\" , \"type\" : { \"type\" : \"string\" } }, { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" , \"items\" : { \"type\" : \"string\" } } }, \"load\" : { \"role\" : \"load\" , \"scope\" : \"static\" , \"identifier\" : \"load\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"path\" , \"type\" : { \"type\" : \"string\" } }, { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" } } 7. Implement the statics The spec leaves src/toml.ts to you, and so does this tutorial — the contract is the interesting part. Per the host introspection contract , 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. If 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. 8. Use it in a project A project enables the extension with one line, then names the format on a content type: { \"extensions\" : [ \"@jxsuite/parser\" , \"@acme/jx-toml\" ], \"content\" : { \"settings\" : { \"source\" : \"./content/settings/\" , \"format\" : \"Toml\" } } } ( @jxsuite/parser stays in the list because the content section itself belongs to it — your package contributes only the format.) Run jx schema to regenerate the project's entry schema documents. This extension ships no schema fragment — it contributes no project.json section — but the generator still picks up the class, so \"format\": \"Toml\" validates in the regenerated format-name enum. You 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). What you built One 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. Next steps Tutorial: a guestbook extension — the same pattern extended to project sections, tables, and a server mount. Formats — everything the format block can declare, including exportTarget and remote . Content collections — the user-facing model your format now feeds."},{"id":"docs:extending/extensions/tutorial-toml-format#tutorial-a-toml-format-extension","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#tutorial-a-toml-format-extension","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"Tutorial: a TOML format extension","text":"By 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. Prerequisites : you have read The anatomy of an extension and Formats , and you have a Jx site project to test against ( Your first project )."},{"id":"docs:extending/extensions/tutorial-toml-format#1-scaffold-the-package","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#1-scaffold-the-package","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"1. Scaffold the package","text":"Create a package with a jx field pointing at the manifest, and export the manifest and class descriptor alongside the code: { \"name\" : \"@acme/jx-toml\" , \"type\" : \"module\" , \"jx\" : \"./jx-extension.json\" , \"files\" : [ \"src/\" , \"jx-extension.json\" ], \"exports\" : { \".\" : \"./src/toml.ts\" , \"./jx-extension.json\" : \"./jx-extension.json\" , \"./Toml.class.json\" : \"./src/Toml.class.json\" } } The \"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 ."},{"id":"docs:extending/extensions/tutorial-toml-format#2-write-the-manifest","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#2-write-the-manifest","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"2. Write the manifest","text":"jx-extension.json at the package root enumerates what the package provides — here, a single class: { \"name\" : \"@acme/jx-toml\" , \"title\" : \"TOML\" , \"classes\" : { \"Toml\" : \"./src/Toml.class.json\" } } The 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."},{"id":"docs:extending/extensions/tutorial-toml-format#3-declare-the-format-class","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#3-declare-the-format-class","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"3. Declare the format class","text":"src/Toml.class.json is an ordinary Jx class descriptor. Its top-level format block is the admission block that puts it into file-extension dispatch: { \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\" , \"title\" : \"Toml\" , \"description\" : \"TOML content files as Jx content entries\" , \"$prototype\" : \"Class\" , \"$implementation\" : \"./toml.js\" , \"format\" : { \"extensions\" : [ \".toml\" ], \"mediaType\" : \"application/toml\" , \"documentKinds\" : [ \"content\" ] } } 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. $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."},{"id":"docs:extending/extensions/tutorial-toml-format#4-declare-the-constructor-and-runtime-access","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#4-declare-the-constructor-and-runtime-access","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"4. Declare the constructor and runtime access","text":"Next, the class's $defs describe its instance surface — the standard external-class contract of a config -taking constructor plus an instance resolve() : \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } } }, \"constructor\" : { \"role\" : \"constructor\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }], \"body\" : [ \"this.config = config;\" ] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"scope\" : \"instance\" , \"identifier\" : \"resolve\" , \"returnType\" : { \"type\" : \"object\" } } } } This is what makes { \"$prototype\": \"Toml\", \"src\": \"./x.toml\" } work as a state entry at runtime."},{"id":"docs:extending/extensions/tutorial-toml-format#5-declare-the-parse-capability","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#5-declare-the-parse-capability","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"5. Declare the parse capability","text":"Capability methods are static methods declared by well-known role values, alongside resolve in $defs.methods : \"parse\" : { \"role\" : \"parse\" , \"scope\" : \"static\" , \"identifier\" : \"parse\" , \"timing\" : [ \"compiler\" , \"server\" , \"client\" ], \"parameters\" : [{ \"identifier\" : \"source\" , \"type\" : { \"type\" : \"string\" } }], \"returnType\" : { \"type\" : \"object\" } } timing includes \"client\" because TOML parsing needs no filesystem — declaring it lets Studio parse in-process instead of round-tripping through the dev server."},{"id":"docs:extending/extensions/tutorial-toml-format#6-declare-the-content-capabilities","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#6-declare-the-content-capabilities","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"6. Declare the content capabilities","text":"discover lists entry files for a content source; load parses one source into entries. Both are node-only, so timing stops at \"server\" : \"discover\" : { \"role\" : \"discover\" , \"scope\" : \"static\" , \"identifier\" : \"discover\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"source\" , \"type\" : { \"type\" : \"string\" } }, { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" , \"items\" : { \"type\" : \"string\" } } }, \"load\" : { \"role\" : \"load\" , \"scope\" : \"static\" , \"identifier\" : \"load\" , \"timing\" : [ \"compiler\" , \"server\" ], \"parameters\" : [ { \"identifier\" : \"path\" , \"type\" : { \"type\" : \"string\" } }, { \"identifier\" : \"options\" , \"type\" : { \"type\" : \"object\" } } ], \"returnType\" : { \"type\" : \"array\" } }"},{"id":"docs:extending/extensions/tutorial-toml-format#7-implement-the-statics","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#7-implement-the-statics","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"7. Implement the statics","text":"The spec leaves src/toml.ts to you, and so does this tutorial — the contract is the interesting part. Per the host introspection contract , 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. If 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."},{"id":"docs:extending/extensions/tutorial-toml-format#8-use-it-in-a-project","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#8-use-it-in-a-project","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"8. Use it in a project","text":"A project enables the extension with one line, then names the format on a content type: { \"extensions\" : [ \"@jxsuite/parser\" , \"@acme/jx-toml\" ], \"content\" : { \"settings\" : { \"source\" : \"./content/settings/\" , \"format\" : \"Toml\" } } } ( @jxsuite/parser stays in the list because the content section itself belongs to it — your package contributes only the format.) Run jx schema to regenerate the project's entry schema documents. This extension ships no schema fragment — it contributes no project.json section — but the generator still picks up the class, so \"format\": \"Toml\" validates in the regenerated format-name enum. You 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)."},{"id":"docs:extending/extensions/tutorial-toml-format#what-you-built","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#what-you-built","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"What you built","text":"One 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."},{"id":"docs:extending/extensions/tutorial-toml-format#next-steps","collection":"docs","slug":"extending/extensions/tutorial-toml-format","url":"/docs/extending/extensions/tutorial-toml-format/#next-steps","title":"Tutorial: a TOML format extension","description":"Build a third-party format extension end to end: package, manifest, class descriptor, parse and content capabilities, and .toml content in a project.","heading":"Next steps","text":"Tutorial: a guestbook extension — the same pattern extended to project sections, tables, and a server mount. Formats — everything the format block can declare, including exportTarget and remote . Content collections — the user-facing model your format now feeds."},{"id":"docs:extending/reference/implementation-status","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"","text":"Implementation status This 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 . Specs at a glance Spec Version Status Updated ai.md 0.1.2-draft Partial 2026-07-25 collab.md 0.1.1-draft Partial 2026-07-22 compiler.md 0.1.23-draft Partial 2026-07-24 desktop.md 0.3.1-draft Pending 2026-07-25 extensions.md 0.3.3-draft Partial 2026-07-25 imports.md 0.1.6-draft Partial 2026-07-22 jx-markdown.md 0.1.7-draft Partial 2026-07-22 parser.md 0.2.4-draft Partial 2026-07-23 relationships.md 0.1.3-draft Partial 2026-07-22 schema.md 0.2.8-draft Partial 2026-07-22 server.md 0.2.1 Implemented 2026-07-25 site-architecture.md 0.1.37-draft Pending 2026-07-24 spec.md 0.4.24-draft Partial 2026-07-24 studio-ui-guidelines.md 0.2.0 Implemented 2026-07-26 studio.md 0.3.0-draft Partial 2026-07-27 Sections not yet implemented Partial spec.md §5.6 — Private State ( # prefix) spec.md §11.4 — Server Timing — RPC Function Boundary spec.md §16.8 — CEM-Compatible Annotations Pending desktop.md §4.3 — Single File Mode site-architecture.md §7.4 — Content Entry Editor site-architecture.md §8.6 — Studio SEO Panel site-architecture.md §9.4 — Studio Media Browser site-architecture.md §11.4 — Studio Redirect Editor Future desktop.md §9.3 — Nix Package Removed spec.md §13.1 — Component Instances"},{"id":"docs:extending/reference/implementation-status#implementation-status","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#implementation-status","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Implementation status","text":"This 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 ."},{"id":"docs:extending/reference/implementation-status#specs-at-a-glance","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#specs-at-a-glance","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Specs at a glance","text":"Spec Version Status Updated ai.md 0.1.2-draft Partial 2026-07-25 collab.md 0.1.1-draft Partial 2026-07-22 compiler.md 0.1.23-draft Partial 2026-07-24 desktop.md 0.3.1-draft Pending 2026-07-25 extensions.md 0.3.3-draft Partial 2026-07-25 imports.md 0.1.6-draft Partial 2026-07-22 jx-markdown.md 0.1.7-draft Partial 2026-07-22 parser.md 0.2.4-draft Partial 2026-07-23 relationships.md 0.1.3-draft Partial 2026-07-22 schema.md 0.2.8-draft Partial 2026-07-22 server.md 0.2.1 Implemented 2026-07-25 site-architecture.md 0.1.37-draft Pending 2026-07-24 spec.md 0.4.24-draft Partial 2026-07-24 studio-ui-guidelines.md 0.2.0 Implemented 2026-07-26 studio.md 0.3.0-draft Partial 2026-07-27"},{"id":"docs:extending/reference/implementation-status#sections-not-yet-implemented","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#sections-not-yet-implemented","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Sections not yet implemented","text":""},{"id":"docs:extending/reference/implementation-status#partial","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#partial","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Partial","text":"spec.md §5.6 — Private State ( # prefix) spec.md §11.4 — Server Timing — RPC Function Boundary spec.md §16.8 — CEM-Compatible Annotations"},{"id":"docs:extending/reference/implementation-status#pending","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#pending","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Pending","text":"desktop.md §4.3 — Single File Mode site-architecture.md §7.4 — Content Entry Editor site-architecture.md §8.6 — Studio SEO Panel site-architecture.md §9.4 — Studio Media Browser site-architecture.md §11.4 — Studio Redirect Editor"},{"id":"docs:extending/reference/implementation-status#future","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#future","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Future","text":"desktop.md §9.3 — Nix Package"},{"id":"docs:extending/reference/implementation-status#removed","collection":"docs","slug":"extending/reference/implementation-status","url":"/docs/extending/reference/implementation-status/#removed","title":"Implementation status","description":"Generated from the specs' status markers: which spec sections are implemented, partial, pending, future, or removed.","heading":"Removed","text":"spec.md §13.1 — Component Instances"},{"id":"docs:extending/reference/spec-changelog","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"","text":"Spec changelog This 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>\" . Versions 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 . ai.md 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. 0.1.1-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.0-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 ). collab.md 0.1.1-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.0-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 ). compiler.md 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. 0.1.22-draft (2026-07-23) — Image src resolution consults extension asset mounts before public/ (§7.3). 0.1.21-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.20-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.19-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.1.18-draft (2026-07-17) — Bundle the site worker self-contained per adapter ( 4096ba12 ). 0.1.17-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.1.16-draft (2026-07-17) — Image pruning for persistent site build cache + github ci cache ( b45096ed ). 0.1.15-draft (2026-06-10) — Update site architecture to reflect new changes ( c0bdba08 ). 0.1.14-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.13-draft (2026-06-03) — Use .cache isntead of .jx-cache to support cloudflare build cache ( 1103d2d6 ). 0.1.12-draft (2026-06-01) — Stronger typing ( fcbb5b5d ). 0.1.11-draft (2026-06-01) — Convert to typescript ( e352e265 ). 0.1.10-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.9-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.1.8-draft (2026-05-15) — Image optimization specs ( 7d2ee67f ). 0.1.7-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.1.6-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.5-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.4-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.3-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.2-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.1-draft (2026-04-10) — Finalize vision for site architecture ( da594993 ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f ). desktop.md 0.3.1-draft (2026-07-25) — Repo picker gains a repository-access footer: per-installation manage links, install-on-another-account, and Refresh. 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. 0.2.7-draft (2026-07-24) — Cloud platform registers inside studio.js via a window.__jxCloud signal (single yjs for collab). 0.2.6-draft (2026-07-24) — Document packaged static-data staging into app/bun (create templates, starters) and postBuild bundle verification. 0.2.5-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.4-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.3-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.2.2-draft (2026-07-13) — Run formatter ( 9e776783 ). 0.2.1-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.2.0-draft (2026-05-17) — Remove auto-detection script and update documentation for NixOS desktop runtime ( 6b746644 ). 0.1.6-draft (2026-05-16) — Update desktop spec ( 9453ea1f ). 0.1.5-draft (2026-04-23) — Oxfmt ( af32c08c ). 0.1.4-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.3-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-15) — Implement platform abstraction ( 962ba588 ). extensions.md 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). 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). 0.3.1-draft (2026-07-25) — $paths validates against the source union instead of accepting any object (§5.3). 0.3.0-draft (2026-07-25) — Committed entry documents are single-resource: every $ref a root pointer (§5.2, §5.4). 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. 0.2.8-draft (2026-07-23) — Add the assets capability (§8.5): section owners publish source directories at site URLs. 0.2.7-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.6-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.5-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.2.4-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.2.3-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.2.2-draft (2026-07-11) — Better Auth extension — sessions, permissions, auth-gated data ( bf472285 ). 0.2.1-draft (2026-07-08) — Shipped schema fragments + per-project schema emitters ( 9e4a8936 ). 0.2.0-draft (2026-07-08) — Extensions v2 framework + docs ( 3fb8795f ). 0.1.0-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). imports.md 0.1.6-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.5-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.4-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.3-draft (2026-06-01) — Remove old glob-based content type references ( 6bcbfdaf ). 0.1.2-draft (2026-05-19) — Reflect new content type transition ( 6eb3d2b6 ). 0.1.1-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.0-draft (2026-04-22) — External web component support ( a9d0fbe4 ). jx-markdown.md 0.1.7-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.6-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.5-draft (2026-07-17) — Build-time syntax highlighting for markdown code fences ( b2e7a561 ). 0.1.4-draft (2026-06-15) — Arrays as pseudo-element ( 0b8b3070 ). 0.1.3-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.2-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.1.1-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.0-draft (2026-05-11) — Jx markdown ( 7b102340 ). parser.md 0.2.4-draft (2026-07-23) — Document the Content project-section class: asset mounts and content-relative reference rewriting (§9). 0.2.3-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.2-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.1-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.2.0-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.6-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.5-draft (2026-05-08) — Pass markdown attributes as properties ( 407b70fc ). 0.1.4-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.3-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f ). relationships.md 0.1.3-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.2-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.1-draft (2026-07-08) — Shipped schema fragments + per-project schema emitters ( 9e4a8936 ). 0.1.0-draft (2026-07-08) — Extensions v2 framework + docs ( 3fb8795f ). schema.md 0.2.8-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.7-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.6-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.2.5-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.2.4-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.2.3-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.2.2-draft (2026-04-23) — Site build ( ffe60ddc ). 0.2.1-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.2.0-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.3-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f ). server.md 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. 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. 0.1.9 (2026-07-23) — Serve extension asset mounts ahead of the project root in the static-file chain (§3). 0.1.8 (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.7 (2026-07-22) — Fix failing tests ( 56e073f8 ). 0.1.6 (2026-07-22) — Harden dev server and unify runtime/compiler evaluation ( 47a1d4c9 ). 0.1.5 (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.1.4 (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.3 (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.2 (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1 (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0 (2026-04-10) — Consolidate specs ( 80ca313f ). site-architecture.md 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. 0.1.36-draft (2026-07-23) — Note the mounted-asset copy step in the build pipeline (§12.1). 0.1.35-draft (2026-07-23) — Content entries address media relative to themselves; collections publish their directory at /content/ (§9.3). 0.1.34-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.33-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.32-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.1.31-draft (2026-07-17) — Bundle the site worker self-contained per adapter ( 4096ba12 ). 0.1.30-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.1.29-draft (2026-07-17) — Image pruning for persistent site build cache + github ci cache ( b45096ed ). 0.1.28-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.1.27-draft (2026-06-25) — Sitemap generation ( 948c7a67 ). 0.1.26-draft (2026-06-10) — Update site architecture to reflect new changes ( c0bdba08 ). 0.1.25-draft (2026-06-10) — Use cloudflare cgi for image optimization ( 96228874 ). 0.1.24-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.23-draft (2026-06-03) — Use .cache isntead of .jx-cache to support cloudflare build cache ( 1103d2d6 ). 0.1.22-draft (2026-06-01) — Remove old glob-based content type references ( 6bcbfdaf ). 0.1.21-draft (2026-05-28) — Separate directory + format for content type defs ( c43186ac ). 0.1.20-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.1.19-draft (2026-05-25) — Allow nested global styles ( 1159d585 ). 0.1.18-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.17-draft (2026-05-19) — Reflect new content type transition ( 6eb3d2b6 ). 0.1.16-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.1.15-draft (2026-05-18) — Remove unused 'rendered' property from JSON and CSV entries ( 7478a87c ). 0.1.14-draft (2026-05-15) — Image optimization specs ( 7d2ee67f ). 0.1.13-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.1.12-draft (2026-05-04) — Longhand/shorthand property input sync ( 05c7da35 ). 0.1.11-draft (2026-04-29) — Update site architecture progress ( 3305a8f0 ). 0.1.10-draft (2026-04-29) — Project browser ( 11c1fe7c ). 0.1.9-draft (2026-04-23) — Site build ( ffe60ddc ). 0.1.8-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.1.7-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.6-draft (2026-04-20) — Better project-level scoping ( 0cba233c ). 0.1.5-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.4-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.3-draft (2026-04-15) — Importmap support ( c1b329d4 ). 0.1.2-draft (2026-04-10) — WinterTC server-side conventions ( 60eba6dd ). 0.1.1-draft (2026-04-10) — Site architecture update ( 86d1c515 ). 0.1.0-draft (2026-04-10) — Enhanced font picker ( 9d388a32 ). spec.md 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. 0.4.23-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.4.22-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.4.21-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 ). 0.4.20-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.4.19-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.4.18-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.4.17-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.4.16-draft (2026-07-17) — Clean up spec ( 897e8c1e ). 0.4.15-draft (2026-07-15) — Pure method operators and the composite formula catalog (spec §19.4d) ( 58be3b1a ). 0.4.14-draft (2026-07-15) — Conditional operators and editor evaluation trace (spec §19.4b, §19.9) ( 79926245 ). 0.4.13-draft (2026-06-15) — Arrays as pseudo-element ( 0b8b3070 ). 0.4.12-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.4.11-draft (2026-06-02) — Returns type in the class definition ( 32e4737a ). 0.4.10-draft (2026-06-01) — Declarative expressions ( 472aeb15 ). 0.4.9-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.4.8-draft (2026-05-20) — \"format\" on fields for image fields ( 02f87d29 ). 0.4.7-draft (2026-05-20) — Release 0.11.0 ( 4a0e17ed ). 0.4.6-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.4.5-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.4.4-draft (2026-05-15) — Add markdown prototypes at top-level ( 24020906 ). 0.4.3-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.4.2-draft (2026-04-23) — Oxfmt ( af32c08c ). 0.4.1-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.4.0-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.3.7-draft (2026-04-22) — External web component support ( a9d0fbe4 ). 0.3.6-draft (2026-04-22) — Init new site ( f33d319b ). 0.3.5-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.3.4-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.3.3-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.3.2-draft (2026-04-15) — Importmap support ( c1b329d4 ). 0.3.1-draft (2026-04-15) — Require json based entrypoints for external class integration ( 86dc4383 ). 0.3.0-draft (2026-04-10) — Consolidate specs ( 80ca313f ). 0.2.3-draft (2026-04-07) — Custom element spec ( 4f377be3 ). 0.2.2-draft (2026-04-06) — Server-side timing (scaffolding) ( 23932590 ). 0.2.1-draft (2026-04-06) — Transition to vue reactivity ( 70cb8445 ). 0.2.0-draft (2026-04-06) — Pure js in defs and strings ( 6494999d ). 0.1.3-draft (2026-04-06) — External/md parser ( 5161ec0e ). 0.1.2-draft (2026-04-04) — Declarative media breakpoints ( 3142b64e ). 0.1.1-draft (2026-04-04) — Rebrand as JSONsx ( 0daa94f7 ). 0.1.0-draft (2026-04-04) — Init ( a93852ac ). studio-ui-guidelines.md 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). 0.1.8 (2026-07-26) — Modal surfaces own the keyboard: showDialog focus handoff, Escape dismissal, and the isModalOpen() shortcut gate (§8.7). 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. 0.1.6 (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.5 (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.4 (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control ( ccdc1d3e ). 0.1.3 (2026-06-01) — Convert to typescript ( e352e265 ). 0.1.2 (2026-04-22) — External web component support ( a9d0fbe4 ). 0.1.1 (2026-04-22) — Init new site ( f33d319b ). 0.1.0 (2026-04-18) — Ui guidelines ( 91f2b29e ). studio.md 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). 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). 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. 0.1.28-draft (2026-07-25) — The Cloud platform target composes per-project schemas server-side (§3.4). 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). 0.1.26-draft (2026-07-25) — PAL table records the destination members: createDestination, createProject's user-chosen destination, and pickDirectory. 0.1.25-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.24-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.23-draft (2026-07-17) — Scheme-variant editing — token overrides, scheme-layer routing, live feedback ( 49f0c525 ). 0.1.22-draft (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control ( ccdc1d3e ). 0.1.21-draft (2026-07-17) — Consolidated field mode switcher ( 0a135ed1 ). 0.1.20-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.19-draft (2026-05-25) — Allow nested global styles ( 1159d585 ). 0.1.18-draft (2026-05-20) — \"format\" on fields for image fields ( 02f87d29 ). 0.1.17-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.16-draft (2026-05-15) — Git sidebar ( 79663844 ). 0.1.15-draft (2026-04-23) — Include global styling ( d8d25640 ). 0.1.14-draft (2026-04-23) — Site build ( ffe60ddc ). 0.1.13-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.1.12-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.11-draft (2026-04-22) — External web component support ( a9d0fbe4 ). 0.1.10-draft (2026-04-22) — Init new site ( f33d319b ). 0.1.9-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.8-draft (2026-04-20) — Better project-level scoping ( 0cba233c ). 0.1.7-draft (2026-04-18) — Dedicated combo/picker component for style preview ( d8d07921 ). 0.1.6-draft (2026-04-18) — Fix the test path handling on windows ( 26ea0d70 ). 0.1.5-draft (2026-04-17) — Update studio specs ( d0e5475a ). 0.1.4-draft (2026-04-17) — Reorganize code tree ( d5ee04c4 ). 0.1.3-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.2-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.1-draft (2026-04-10) — Finalize vision for site architecture ( da594993 ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/spec-changelog#spec-changelog","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#spec-changelog","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"Spec changelog","text":"This 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>\" . Versions 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 ."},{"id":"docs:extending/reference/spec-changelog#aimd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#aimd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"ai.md","text":"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. 0.1.1-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.0-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 )."},{"id":"docs:extending/reference/spec-changelog#collabmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#collabmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"collab.md","text":"0.1.1-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.0-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 )."},{"id":"docs:extending/reference/spec-changelog#compilermd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#compilermd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"compiler.md","text":"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. 0.1.22-draft (2026-07-23) — Image src resolution consults extension asset mounts before public/ (§7.3). 0.1.21-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.20-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.19-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.1.18-draft (2026-07-17) — Bundle the site worker self-contained per adapter ( 4096ba12 ). 0.1.17-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.1.16-draft (2026-07-17) — Image pruning for persistent site build cache + github ci cache ( b45096ed ). 0.1.15-draft (2026-06-10) — Update site architecture to reflect new changes ( c0bdba08 ). 0.1.14-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.13-draft (2026-06-03) — Use .cache isntead of .jx-cache to support cloudflare build cache ( 1103d2d6 ). 0.1.12-draft (2026-06-01) — Stronger typing ( fcbb5b5d ). 0.1.11-draft (2026-06-01) — Convert to typescript ( e352e265 ). 0.1.10-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.9-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.1.8-draft (2026-05-15) — Image optimization specs ( 7d2ee67f ). 0.1.7-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.1.6-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.5-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.4-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.3-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.2-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.1-draft (2026-04-10) — Finalize vision for site architecture ( da594993 ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/spec-changelog#desktopmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#desktopmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"desktop.md","text":"0.3.1-draft (2026-07-25) — Repo picker gains a repository-access footer: per-installation manage links, install-on-another-account, and Refresh. 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. 0.2.7-draft (2026-07-24) — Cloud platform registers inside studio.js via a window.__jxCloud signal (single yjs for collab). 0.2.6-draft (2026-07-24) — Document packaged static-data staging into app/bun (create templates, starters) and postBuild bundle verification. 0.2.5-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.4-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.3-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.2.2-draft (2026-07-13) — Run formatter ( 9e776783 ). 0.2.1-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.2.0-draft (2026-05-17) — Remove auto-detection script and update documentation for NixOS desktop runtime ( 6b746644 ). 0.1.6-draft (2026-05-16) — Update desktop spec ( 9453ea1f ). 0.1.5-draft (2026-04-23) — Oxfmt ( af32c08c ). 0.1.4-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.3-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-15) — Implement platform abstraction ( 962ba588 )."},{"id":"docs:extending/reference/spec-changelog#extensionsmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#extensionsmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"extensions.md","text":"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). 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). 0.3.1-draft (2026-07-25) — $paths validates against the source union instead of accepting any object (§5.3). 0.3.0-draft (2026-07-25) — Committed entry documents are single-resource: every $ref a root pointer (§5.2, §5.4). 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. 0.2.8-draft (2026-07-23) — Add the assets capability (§8.5): section owners publish source directories at site URLs. 0.2.7-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.6-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.5-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.2.4-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.2.3-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.2.2-draft (2026-07-11) — Better Auth extension — sessions, permissions, auth-gated data ( bf472285 ). 0.2.1-draft (2026-07-08) — Shipped schema fragments + per-project schema emitters ( 9e4a8936 ). 0.2.0-draft (2026-07-08) — Extensions v2 framework + docs ( 3fb8795f ). 0.1.0-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da )."},{"id":"docs:extending/reference/spec-changelog#importsmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#importsmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"imports.md","text":"0.1.6-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.5-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.4-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.3-draft (2026-06-01) — Remove old glob-based content type references ( 6bcbfdaf ). 0.1.2-draft (2026-05-19) — Reflect new content type transition ( 6eb3d2b6 ). 0.1.1-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.0-draft (2026-04-22) — External web component support ( a9d0fbe4 )."},{"id":"docs:extending/reference/spec-changelog#jx-markdownmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#jx-markdownmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"jx-markdown.md","text":"0.1.7-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.6-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.5-draft (2026-07-17) — Build-time syntax highlighting for markdown code fences ( b2e7a561 ). 0.1.4-draft (2026-06-15) — Arrays as pseudo-element ( 0b8b3070 ). 0.1.3-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.2-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.1.1-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.0-draft (2026-05-11) — Jx markdown ( 7b102340 )."},{"id":"docs:extending/reference/spec-changelog#parsermd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#parsermd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"parser.md","text":"0.2.4-draft (2026-07-23) — Document the Content project-section class: asset mounts and content-relative reference rewriting (§9). 0.2.3-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.2-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.1-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.2.0-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.6-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.5-draft (2026-05-08) — Pass markdown attributes as properties ( 407b70fc ). 0.1.4-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.3-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/spec-changelog#relationshipsmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#relationshipsmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"relationships.md","text":"0.1.3-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.2-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.1-draft (2026-07-08) — Shipped schema fragments + per-project schema emitters ( 9e4a8936 ). 0.1.0-draft (2026-07-08) — Extensions v2 framework + docs ( 3fb8795f )."},{"id":"docs:extending/reference/spec-changelog#schemamd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#schemamd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"schema.md","text":"0.2.8-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.2.7-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.2.6-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.2.5-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.2.4-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.2.3-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.2.2-draft (2026-04-23) — Site build ( ffe60ddc ). 0.2.1-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.2.0-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.3-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.2-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/spec-changelog#servermd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#servermd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"server.md","text":"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. 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. 0.1.9 (2026-07-23) — Serve extension asset mounts ahead of the project root in the static-file chain (§3). 0.1.8 (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.7 (2026-07-22) — Fix failing tests ( 56e073f8 ). 0.1.6 (2026-07-22) — Harden dev server and unify runtime/compiler evaluation ( 47a1d4c9 ). 0.1.5 (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.1.4 (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.3 (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.1.2 (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.1 (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.0 (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/spec-changelog#site-architecturemd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#site-architecturemd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"site-architecture.md","text":"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. 0.1.36-draft (2026-07-23) — Note the mounted-asset copy step in the build pipeline (§12.1). 0.1.35-draft (2026-07-23) — Content entries address media relative to themselves; collections publish their directory at /content/ (§9.3). 0.1.34-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.33-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.32-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.1.31-draft (2026-07-17) — Bundle the site worker self-contained per adapter ( 4096ba12 ). 0.1.30-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.1.29-draft (2026-07-17) — Image pruning for persistent site build cache + github ci cache ( b45096ed ). 0.1.28-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.1.27-draft (2026-06-25) — Sitemap generation ( 948c7a67 ). 0.1.26-draft (2026-06-10) — Update site architecture to reflect new changes ( c0bdba08 ). 0.1.25-draft (2026-06-10) — Use cloudflare cgi for image optimization ( 96228874 ). 0.1.24-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.23-draft (2026-06-03) — Use .cache isntead of .jx-cache to support cloudflare build cache ( 1103d2d6 ). 0.1.22-draft (2026-06-01) — Remove old glob-based content type references ( 6bcbfdaf ). 0.1.21-draft (2026-05-28) — Separate directory + format for content type defs ( c43186ac ). 0.1.20-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.1.19-draft (2026-05-25) — Allow nested global styles ( 1159d585 ). 0.1.18-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.17-draft (2026-05-19) — Reflect new content type transition ( 6eb3d2b6 ). 0.1.16-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.1.15-draft (2026-05-18) — Remove unused 'rendered' property from JSON and CSV entries ( 7478a87c ). 0.1.14-draft (2026-05-15) — Image optimization specs ( 7d2ee67f ). 0.1.13-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.1.12-draft (2026-05-04) — Longhand/shorthand property input sync ( 05c7da35 ). 0.1.11-draft (2026-04-29) — Update site architecture progress ( 3305a8f0 ). 0.1.10-draft (2026-04-29) — Project browser ( 11c1fe7c ). 0.1.9-draft (2026-04-23) — Site build ( ffe60ddc ). 0.1.8-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.1.7-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.6-draft (2026-04-20) — Better project-level scoping ( 0cba233c ). 0.1.5-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.4-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.3-draft (2026-04-15) — Importmap support ( c1b329d4 ). 0.1.2-draft (2026-04-10) — WinterTC server-side conventions ( 60eba6dd ). 0.1.1-draft (2026-04-10) — Site architecture update ( 86d1c515 ). 0.1.0-draft (2026-04-10) — Enhanced font picker ( 9d388a32 )."},{"id":"docs:extending/reference/spec-changelog#specmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#specmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"spec.md","text":"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. 0.4.23-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.4.22-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.4.21-draft (2026-07-22) — Reconcile spec with shipped behavior; document the eval surface ( c8d1d580 ). 0.4.20-draft (2026-07-22) — Align specs and docs with the bundled-schema validation contract ( ae861ff6 ). 0.4.19-draft (2026-07-17) — Forced color-scheme contract — dual emission, color-scheme triplet, pre-paint script ( e629684d ). 0.4.18-draft (2026-07-17) — Sidecar bundling, extension emit capability, heading anchors ( 07e28bc3 ). 0.4.17-draft (2026-07-17) — Align spec.md, site-architecture, desktop, server, extensions with reality ( c61ba567 ). 0.4.16-draft (2026-07-17) — Clean up spec ( 897e8c1e ). 0.4.15-draft (2026-07-15) — Pure method operators and the composite formula catalog (spec §19.4d) ( 58be3b1a ). 0.4.14-draft (2026-07-15) — Conditional operators and editor evaluation trace (spec §19.4b, §19.9) ( 79926245 ). 0.4.13-draft (2026-06-15) — Arrays as pseudo-element ( 0b8b3070 ). 0.4.12-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.4.11-draft (2026-06-02) — Returns type in the class definition ( 32e4737a ). 0.4.10-draft (2026-06-01) — Declarative expressions ( 472aeb15 ). 0.4.9-draft (2026-05-25) — Element annotations (title/description) ( c9137e50 ). 0.4.8-draft (2026-05-20) — \"format\" on fields for image fields ( 02f87d29 ). 0.4.7-draft (2026-05-20) — Release 0.11.0 ( 4a0e17ed ). 0.4.6-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.4.5-draft (2026-05-18) — Always emit worker.js for cloudflare ( 3dd37c2d ). 0.4.4-draft (2026-05-15) — Add markdown prototypes at top-level ( 24020906 ). 0.4.3-draft (2026-05-15) — Provider-sepcific Site-Wide Bundling ( 51cb5cf6 ). 0.4.2-draft (2026-04-23) — Oxfmt ( af32c08c ). 0.4.1-draft (2026-04-23) — Rebrand to jxsuite ( 2897a4e8 ). 0.4.0-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.3.7-draft (2026-04-22) — External web component support ( a9d0fbe4 ). 0.3.6-draft (2026-04-22) — Init new site ( f33d319b ). 0.3.5-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.3.4-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.3.3-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.3.2-draft (2026-04-15) — Importmap support ( c1b329d4 ). 0.3.1-draft (2026-04-15) — Require json based entrypoints for external class integration ( 86dc4383 ). 0.3.0-draft (2026-04-10) — Consolidate specs ( 80ca313f ). 0.2.3-draft (2026-04-07) — Custom element spec ( 4f377be3 ). 0.2.2-draft (2026-04-06) — Server-side timing (scaffolding) ( 23932590 ). 0.2.1-draft (2026-04-06) — Transition to vue reactivity ( 70cb8445 ). 0.2.0-draft (2026-04-06) — Pure js in defs and strings ( 6494999d ). 0.1.3-draft (2026-04-06) — External/md parser ( 5161ec0e ). 0.1.2-draft (2026-04-04) — Declarative media breakpoints ( 3142b64e ). 0.1.1-draft (2026-04-04) — Rebrand as JSONsx ( 0daa94f7 ). 0.1.0-draft (2026-04-04) — Init ( a93852ac )."},{"id":"docs:extending/reference/spec-changelog#studio-ui-guidelinesmd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#studio-ui-guidelinesmd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"studio-ui-guidelines.md","text":"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). 0.1.8 (2026-07-26) — Modal surfaces own the keyboard: showDialog focus handoff, Escape dismissal, and the isModalOpen() shortcut gate (§8.7). 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. 0.1.6 (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.5 (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.4 (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control ( ccdc1d3e ). 0.1.3 (2026-06-01) — Convert to typescript ( e352e265 ). 0.1.2 (2026-04-22) — External web component support ( a9d0fbe4 ). 0.1.1 (2026-04-22) — Init new site ( f33d319b ). 0.1.0 (2026-04-18) — Ui guidelines ( 91f2b29e )."},{"id":"docs:extending/reference/spec-changelog#studiomd","collection":"docs","slug":"extending/reference/spec-changelog","url":"/docs/extending/reference/spec-changelog/#studiomd","title":"Spec changelog","description":"Release history for every specification, generated from each spec's changelog section: version, date, and what changed.","heading":"studio.md","text":"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). 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). 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. 0.1.28-draft (2026-07-25) — The Cloud platform target composes per-project schemas server-side (§3.4). 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). 0.1.26-draft (2026-07-25) — PAL table records the destination members: createDestination, createProject's user-chosen destination, and pickDirectory. 0.1.25-draft (2026-07-22) — Proper spec versioning ( fb0f3ec7 ). 0.1.24-draft (2026-07-22) — Machine-readable spec status vocabulary + generated status page ( 79daba23 ). 0.1.23-draft (2026-07-17) — Scheme-variant editing — token overrides, scheme-layer routing, live feedback ( 49f0c525 ). 0.1.22-draft (2026-07-17) — Color-scheme canvas preview — Auto/Light/Dark tab-bar control ( ccdc1d3e ). 0.1.21-draft (2026-07-17) — Consolidated field mode switcher ( 0a135ed1 ). 0.1.20-draft (2026-06-10) — Consolidate markdown and csv handling to the parser package ( 8b1ba6da ). 0.1.19-draft (2026-05-25) — Allow nested global styles ( 1159d585 ). 0.1.18-draft (2026-05-20) — \"format\" on fields for image fields ( 02f87d29 ). 0.1.17-draft (2026-05-20) — Run formatter ( 8ba47930 ). 0.1.16-draft (2026-05-15) — Git sidebar ( 79663844 ). 0.1.15-draft (2026-04-23) — Include global styling ( d8d25640 ). 0.1.14-draft (2026-04-23) — Site build ( ffe60ddc ). 0.1.13-draft (2026-04-23) — Compiler cli + published site ( 4607ebbc ). 0.1.12-draft (2026-04-22) — Consolidate project config schema and rename as such ( e3523dbf ). 0.1.11-draft (2026-04-22) — External web component support ( a9d0fbe4 ). 0.1.10-draft (2026-04-22) — Init new site ( f33d319b ). 0.1.9-draft (2026-04-20) — Text nodes support ( 4d45eeb7 ). 0.1.8-draft (2026-04-20) — Better project-level scoping ( 0cba233c ). 0.1.7-draft (2026-04-18) — Dedicated combo/picker component for style preview ( d8d07921 ). 0.1.6-draft (2026-04-18) — Fix the test path handling on windows ( 26ea0d70 ). 0.1.5-draft (2026-04-17) — Update studio specs ( d0e5475a ). 0.1.4-draft (2026-04-17) — Reorganize code tree ( d5ee04c4 ). 0.1.3-draft (2026-04-16) — Landing site + working exports + release-it + linting ( a8409b5f ). 0.1.2-draft (2026-04-15) — Rebrand to Jx / Jx Platform ( abc63f2d ). 0.1.1-draft (2026-04-10) — Finalize vision for site architecture ( da594993 ). 0.1.0-draft (2026-04-10) — Consolidate specs ( 80ca313f )."},{"id":"docs:extending/reference/studio-routes","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"","text":"Protocol route reference The 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. Session / project Route Method Path Summary Optional Degradation activate POST /__studio/activate Bind the backend to a project root no — project GET /__studio/project Root project metadata {name, root} no — projectInfo GET /__studio/project-info Probe a directory: {isSiteProject, projectConfig?, directories?} no — resolveSite GET /__studio/resolve-site Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?} no — sites GET /__studio/sites Enumerate site projects [{config, path}] (backs listProjects) yes The welcome screen's Projects catalogue stays hidden. findProject GET /__studio/find-project Locate a project directory by name outside the root yes openProject falls back to config-matching only. 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. createProject POST /__studio/create-project Scaffold a project at a caller-chosen destination → {root, config} no — starters GET /__studio/starters Starter templates (StarterInfo[]) yes The New Project picker offers only blank/templates. importSite POST /__studio/import-site Clone a live website into a project; streams NDJSON progress yes The New Project Import tab is unavailable. Filesystem Route Method Path Summary Optional Degradation files GET /__studio/files List a directory (DirEntry[]); with ?glob= , search matching files project-wide no — fileRead GET /__studio/file Read a file's text content no — fileWrite PUT /__studio/file Write a file's text content no — fileDelete DELETE /__studio/file Delete a file no — fileUpload POST /__studio/file/upload Upload binary content to a path no — fileRename POST /__studio/file/rename Rename/move (+ refactor report) no — locate POST /__studio/locate Find a file by name → {path | null} no — 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 Documents / components / formats Route Method Path Summary Optional Degradation components GET /__studio/components Discover components (ComponentMeta[]) no — cem GET /__studio/cem Custom-elements manifest of an npm dependency yes Dependency components lose prop/slot metadata. 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. 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). format POST /__studio/format Dispatch a format capability {format, action: parse|serialize, source?|doc?} yes Non-JSON documents cannot be parsed/serialized (backs formatAction). pluginSchema GET /__studio/plugin-schema Extract a $studio schema from a class source yes Plugin property panels fall back to generic JSON editing. codeFormat POST /__studio/code/format Format posted source {code, path?} → {code, errors} yes Code editors skip format-on-open/save (codeService returns null). codeMinify POST /__studio/code/minify Minify posted source {code} → {code} yes Compiled-output minification is skipped. codeLint POST /__studio/code/lint Lint posted source {code, path?} → {diagnostics} yes Code editors show no lint markers. Packages Route Method Path Summary Optional Degradation packages GET /__studio/packages List dependencies (PackageInfo[]) no — packagesAdd POST /__studio/packages/add Add a dependency no — packagesRemove POST /__studio/packages/remove Remove a dependency no — packagesInstall POST /__studio/packages/install Run the package manager install yes Install/reinstall affordances are hidden; manifest-only edits still work. packagesNeedsInstall GET /__studio/packages/needs-install Whether node_modules is stale yes The install-on-open prompt never shows. packagesOutdated GET /__studio/packages/outdated Dependencies with newer versions (OutdatedInfo[]) yes The update affordances are hidden. packagesSetVersions POST /__studio/packages/set-versions Rewrite dependency ranges and install yes Bulk version updates are hidden. Git Route Method Path Summary Optional Degradation gitStatus GET /__studio/git/status Working-tree status (GitStatusResult) no — gitBranches GET /__studio/git/branches Branch list (GitBranchesResult) no — gitLog GET /__studio/git/log Recent commits (GitLogEntry[]) no — gitStage POST /__studio/git/stage Stage files no — gitUnstage POST /__studio/git/unstage Unstage files no — gitCommit POST /__studio/git/commit Commit staged (else all dirty) files no — gitPush POST /__studio/git/push Push (cloud: sync check — commits land on push) no — gitPull POST /__studio/git/pull Pull/fast-forward; 409 {conflicts} on overlap no — gitFetch POST /__studio/git/fetch Refresh remote tracking state no — gitCheckout POST /__studio/git/checkout Switch branches no — gitCreateBranch POST /__studio/git/create-branch Create a branch no — gitDiff GET /__studio/git/diff Unified diff of dirty files (or one path) no — gitShow GET /__studio/git/show File content at a ref no — gitDiscard POST /__studio/git/discard Discard working changes no — gitInit POST /__studio/git/init Initialize a repository no — gitAddRemote POST /__studio/git/add-remote Add a remote no — gitClone POST /__studio/git/clone Clone a repository yes The welcome screen hides Clone Git Repository. 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. Data surface (connector domain owner console) Route Method Path Summary Optional Degradation 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. dataConnectionTest POST /__studio/data/connections/test Probe a connection {connection} → DataConnectionTestResult (backs dataConnectionTest) yes The Test Connection action is hidden. 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. dataRows GET /__studio/data/rows Page a table (DataRowsQuery params) → DataRowsResult {rows, total, columns} (backs dataRows) yes The data grid is unavailable. dataInsertRow POST /__studio/data/rows Insert a row {table, connection?, values} → {row} (backs dataInsertRow) yes The data grid hides its add-row footer. 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. dataDeleteRow DELETE /__studio/data/rows Delete a row (?table=&pk=&connection=) → {ok} (backs dataDeleteRow) yes The data grid hides row deletion. Secrets (names only) Route Method Path Summary Optional Degradation secretsList GET /__studio/secrets Configured secret env-var NAMES, never values (SecretsListResponse; backs listSecrets) yes Secret fields cannot show set/unset state. 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. AI proxy Route Method Path Summary Optional Degradation aiChat POST /__studio/ai/chat StreamEvent SSE chat proxy {messages, tools, systemPrompt, model} no — aiModels GET /__studio/ai/models Model catalogue (AiModelsResponse) no — Cloudflare publish surface Route Method Path Summary Optional Degradation 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."},{"id":"docs:extending/reference/studio-routes#protocol-route-reference","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#protocol-route-reference","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Protocol route reference","text":"The 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."},{"id":"docs:extending/reference/studio-routes#session-project","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#session-project","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Session / project","text":"Route Method Path Summary Optional Degradation activate POST /__studio/activate Bind the backend to a project root no — project GET /__studio/project Root project metadata {name, root} no — projectInfo GET /__studio/project-info Probe a directory: {isSiteProject, projectConfig?, directories?} no — resolveSite GET /__studio/resolve-site Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?} no — sites GET /__studio/sites Enumerate site projects [{config, path}] (backs listProjects) yes The welcome screen's Projects catalogue stays hidden. findProject GET /__studio/find-project Locate a project directory by name outside the root yes openProject falls back to config-matching only. 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. createProject POST /__studio/create-project Scaffold a project at a caller-chosen destination → {root, config} no — starters GET /__studio/starters Starter templates (StarterInfo[]) yes The New Project picker offers only blank/templates. importSite POST /__studio/import-site Clone a live website into a project; streams NDJSON progress yes The New Project Import tab is unavailable."},{"id":"docs:extending/reference/studio-routes#filesystem","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#filesystem","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Filesystem","text":"Route Method Path Summary Optional Degradation files GET /__studio/files List a directory (DirEntry[]); with ?glob= , search matching files project-wide no — fileRead GET /__studio/file Read a file's text content no — fileWrite PUT /__studio/file Write a file's text content no — fileDelete DELETE /__studio/file Delete a file no — fileUpload POST /__studio/file/upload Upload binary content to a path no — fileRename POST /__studio/file/rename Rename/move (+ refactor report) no — locate POST /__studio/locate Find a file by name → {path | null} no — 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"},{"id":"docs:extending/reference/studio-routes#documents-components-formats","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#documents-components-formats","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Documents / components / formats","text":"Route Method Path Summary Optional Degradation components GET /__studio/components Discover components (ComponentMeta[]) no — cem GET /__studio/cem Custom-elements manifest of an npm dependency yes Dependency components lose prop/slot metadata. 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. 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). format POST /__studio/format Dispatch a format capability {format, action: parse|serialize, source?|doc?} yes Non-JSON documents cannot be parsed/serialized (backs formatAction). pluginSchema GET /__studio/plugin-schema Extract a $studio schema from a class source yes Plugin property panels fall back to generic JSON editing. codeFormat POST /__studio/code/format Format posted source {code, path?} → {code, errors} yes Code editors skip format-on-open/save (codeService returns null). codeMinify POST /__studio/code/minify Minify posted source {code} → {code} yes Compiled-output minification is skipped. codeLint POST /__studio/code/lint Lint posted source {code, path?} → {diagnostics} yes Code editors show no lint markers."},{"id":"docs:extending/reference/studio-routes#packages","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#packages","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Packages","text":"Route Method Path Summary Optional Degradation packages GET /__studio/packages List dependencies (PackageInfo[]) no — packagesAdd POST /__studio/packages/add Add a dependency no — packagesRemove POST /__studio/packages/remove Remove a dependency no — packagesInstall POST /__studio/packages/install Run the package manager install yes Install/reinstall affordances are hidden; manifest-only edits still work. packagesNeedsInstall GET /__studio/packages/needs-install Whether node_modules is stale yes The install-on-open prompt never shows. packagesOutdated GET /__studio/packages/outdated Dependencies with newer versions (OutdatedInfo[]) yes The update affordances are hidden. packagesSetVersions POST /__studio/packages/set-versions Rewrite dependency ranges and install yes Bulk version updates are hidden."},{"id":"docs:extending/reference/studio-routes#git","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#git","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Git","text":"Route Method Path Summary Optional Degradation gitStatus GET /__studio/git/status Working-tree status (GitStatusResult) no — gitBranches GET /__studio/git/branches Branch list (GitBranchesResult) no — gitLog GET /__studio/git/log Recent commits (GitLogEntry[]) no — gitStage POST /__studio/git/stage Stage files no — gitUnstage POST /__studio/git/unstage Unstage files no — gitCommit POST /__studio/git/commit Commit staged (else all dirty) files no — gitPush POST /__studio/git/push Push (cloud: sync check — commits land on push) no — gitPull POST /__studio/git/pull Pull/fast-forward; 409 {conflicts} on overlap no — gitFetch POST /__studio/git/fetch Refresh remote tracking state no — gitCheckout POST /__studio/git/checkout Switch branches no — gitCreateBranch POST /__studio/git/create-branch Create a branch no — gitDiff GET /__studio/git/diff Unified diff of dirty files (or one path) no — gitShow GET /__studio/git/show File content at a ref no — gitDiscard POST /__studio/git/discard Discard working changes no — gitInit POST /__studio/git/init Initialize a repository no — gitAddRemote POST /__studio/git/add-remote Add a remote no — gitClone POST /__studio/git/clone Clone a repository yes The welcome screen hides Clone Git Repository. 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."},{"id":"docs:extending/reference/studio-routes#data-surface-connector-domain-owner-console","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#data-surface-connector-domain-owner-console","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Data surface (connector domain owner console)","text":"Route Method Path Summary Optional Degradation 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. dataConnectionTest POST /__studio/data/connections/test Probe a connection {connection} → DataConnectionTestResult (backs dataConnectionTest) yes The Test Connection action is hidden. 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. dataRows GET /__studio/data/rows Page a table (DataRowsQuery params) → DataRowsResult {rows, total, columns} (backs dataRows) yes The data grid is unavailable. dataInsertRow POST /__studio/data/rows Insert a row {table, connection?, values} → {row} (backs dataInsertRow) yes The data grid hides its add-row footer. 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. dataDeleteRow DELETE /__studio/data/rows Delete a row (?table=&pk=&connection=) → {ok} (backs dataDeleteRow) yes The data grid hides row deletion."},{"id":"docs:extending/reference/studio-routes#secrets-names-only","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#secrets-names-only","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Secrets (names only)","text":"Route Method Path Summary Optional Degradation secretsList GET /__studio/secrets Configured secret env-var NAMES, never values (SecretsListResponse; backs listSecrets) yes Secret fields cannot show set/unset state. 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."},{"id":"docs:extending/reference/studio-routes#ai-proxy","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#ai-proxy","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"AI proxy","text":"Route Method Path Summary Optional Degradation aiChat POST /__studio/ai/chat StreamEvent SSE chat proxy {messages, tools, systemPrompt, model} no — aiModels GET /__studio/ai/models Model catalogue (AiModelsResponse) no —"},{"id":"docs:extending/reference/studio-routes#cloudflare-publish-surface","collection":"docs","slug":"extending/reference/studio-routes","url":"/docs/extending/reference/studio-routes/#cloudflare-publish-surface","title":"Protocol route reference","description":"Every route in the Studio Backend Protocol — method, path, contract summary, and how Studio degrades when an optional route is absent.","heading":"Cloudflare publish surface","text":"Route Method Path Summary Optional Degradation 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."},{"id":"docs:framework/agents/authoring-rules","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/","title":"Authoring rules for 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.","heading":"","text":"Authoring rules for agents An 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. Everything 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 , which you can copy into your own project's .claude/commands/ . Whatever 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. The three schemas Every Jx file validates against one of three JSON Schema 2020-12 documents, published at stable URLs: File Schema Components, pages, layouts ( *.json ) https://jxsuite.com/schema/v1 project.json https://jxsuite.com/schema/project/v1 Class definitions ( *.class.json ) https://jxsuite.com/schema/class/v1 Inside 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 and 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. Project structure my-site/ project.json # Required. Site config, global styles, collections, build settings pages/ # File-based routing. Each file = a route. [slug].json = dynamic layouts/ # Shared page shells. Use { \"tagName\": \"slot\" } for content insertion components/ # Reusable custom elements content/ # Markdown/JSON/CSV content collections public/ # Static assets copied verbatim to dist/ Files and folders prefixed with _ inside pages/ are excluded from routing, so a page's private components can sit beside it. The document A Jx document is a JSON object describing a reactive web component: { \"$id\" : \"TaskList\" , \"tagName\" : \"task-list\" , \"$defs\" : {}, \"state\" : {}, \"style\" : {}, \"children\" : [] } Only tagName is required. A tagName containing a hyphen makes the document a custom element. State — five shapes, detected by structure 1. Naked value — a scalar, array, or plain object with no reserved keys: \"count\" : 0 , \"items\" : [], \"user\" : { \"name\" : \"\" , \"email\" : \"\" } 2. Typed value — has default , optionally type : \"status\" : { \"type\" : { \"type\" : \"string\" , \"enum\" : [ \"idle\" , \"loading\" ] }, \"default\" : \"idle\" } 3. Computed — a string containing ${} : \"fullName\" : \"${state.firstName} ${state.lastName}\" , \"itemCount\" : \"${state.items.length} items\" 4. Function — $prototype: \"Function\" , with an inline body or an external .js sidecar: \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" }, \"handleInput\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"event\" ], \"body\" : \"state.value = event.target.value\" }, \"validate\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./validators.js\" , \"$export\" : \"validateEmail\" } 5. Data source — $prototype: <ClassName> : \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users\" , \"method\" : \"GET\" }, \"posts\" : { \"$prototype\" : \"ContentCollection\" , \"contentType\" : \"blog\" , \"sort\" : { \"field\" : \"pubDate\" , \"order\" : \"desc\" } } See State and Data prototypes . $ref bindings Pattern Example Meaning State { \"$ref\": \"#/state/count\" } Reactive binding to state $defs { \"$ref\": \"#/$defs/TodoItem\" } Type definition reference Parent { \"$ref\": \"parent#/theme\" } A value passed in via $props Map item { \"$ref\": \"$map/item\" } Current item in an iteration Map index { \"$ref\": \"$map/index\" } Current zero-based index External { \"$ref\": \"./card.json\" } Another Jx document Window { \"$ref\": \"window#/Math/max\" } A path resolved off the global object Use ${} templates for inline one-off bindings, $ref objects for named or reused signals. See References . Children Static — an array of element objects and text strings, mixed freely: \"children\" : [ { \"tagName\" : \"h1\" , \"textContent\" : \"Hello\" }, { \"tagName\" : \"p\" , \"children\" : [ \"Welcome to \" , { \"tagName\" : \"strong\" , \"textContent\" : \"Jx\" }] } ] Dynamic list — $prototype: \"Array\" : \"children\" : { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todos\" }, \"map\" : { \"tagName\" : \"li\" , \"className\" : \"${$map.item.done ? 'completed' : ''}\" , \"textContent\" : { \"$ref\" : \"$map/item/text\" } } } See Lists and iteration . $switch and cases { \"$switch\" : { \"$ref\" : \"#/state/currentView\" }, \"cases\" : { \"home\" : { \"$ref\" : \"./views/home.json\" }, \"settings\" : { \"$ref\" : \"./views/settings.json\" } } } See Dynamic switching . Style An 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): \"style\" : { \"display\" : \"flex\" , \"gap\" : \"1rem\" , \"padding\" : \"clamp(1rem, 3vw, 2rem)\" , \":hover\" : { \"backgroundColor\" : \"#f0f0f0\" }, \"&.active\" : { \"borderColor\" : \"blue\" }, \"@--md\" : { \"flexDirection\" : \"row\" }, \"@(prefers-reduced-motion: reduce)\" : { \"transition\" : \"none\" } } See Styling . Properties and attributes IDL properties — href , src , textContent , className , hidden , disabled , value , checked — go directly on the element object. Everything else ( aria-* , data-* , role , slot ) goes in attributes : { \"tagName\" : \"a\" , \"href\" : \"/about\" , \"textContent\" : \"About\" , \"attributes\" : { \"aria-label\" : \"About page\" , \"data-section\" : \"nav\" } } Event handlers Reference a function from state: { \"tagName\" : \"button\" , \"textContent\" : \"Add\" , \"onclick\" : { \"$ref\" : \"#/state/handleAdd\" } } Components and props $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: { \"$elements\" : [ { \"$ref\" : \"./components/user-card.json\" }, \"@shoelace-style/shoelace/components/button/button.js\" ], \"children\" : [ { \"tagName\" : \"user-card\" , \"$props\" : { \"name\" : \"Ada\" , \"count\" : { \"$ref\" : \"#/state/count\" } } } ] } 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 . Pages Pages are documents with a few extra top-level fields: { \"title\" : \"About us\" , \"$layout\" : \"./layouts/base.json\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"About our team\" } } ], \"tagName\" : \"main\" , \"children\" : [] } $layout — a path resolved from the project root , or false for no layout. Omit it to use defaults.layout from project.json . $head — merges with the layout's and the project's entries; the page wins on conflicts. $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. tagName is optional on a page that uses a layout. Dynamic route ( pages/blog/[slug].json ): { \"$paths\" : { \"contentType\" : \"blog\" , \"param\" : \"slug\" }, \"state\" : { \"post\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"blog\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } } } See Routing and Content collections . Layouts Ordinary documents with <slot> elements marking where page content lands. Name a slot to define more than one region: { \"tagName\" : \"div\" , \"children\" : [ { \"tagName\" : \"header\" }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] }, { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"footer\" } ] } See Layouts . project.json { \"$schema\" : \"./project.schema.json\" , \"name\" : \"My Site\" , \"url\" : \"https://example.com\" , \"extensions\" : [ \"@jxsuite/parser\" ], \"defaults\" : { \"layout\" : \"./layouts/base.json\" , \"lang\" : \"en\" }, \"$head\" : [{ \"tagName\" : \"link\" , \"attributes\" : { \"rel\" : \"icon\" , \"href\" : \"/favicon.svg\" } }], \"imports\" : { \"MarkdownCollection\" : \"@jxsuite/parser/MarkdownCollection.class.json\" }, \"$media\" : { \"--sm\" : \"(min-width: 640px)\" , \"--md\" : \"(min-width: 768px)\" }, \"style\" : { \"fontFamily\" : \"system-ui, sans-serif\" , \"margin\" : \"0\" }, \"content\" : { \"blog\" : { \"source\" : \"./content/blog/\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" } }, \"required\" : [ \"title\" ] } } }, \"build\" : { \"outDir\" : \"./dist\" , \"trailingSlash\" : \"always\" } } 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 and First-party extensions . .class.json { \"$prototype\" : \"Class\" , \"title\" : \"MyParser\" , \"$implementation\" : \"./my-parser.js\" , \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } } }, \"constructor\" : { \"role\" : \"constructor\" , \"$prototype\" : \"Function\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"identifier\" : \"resolve\" , \"returnType\" : { \"type\" : \"object\" } } } } } See Custom classes . The server tier A brochure site is not the ceiling. Three additions turn a Jx project into an application, and an agent should know they exist: 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 . 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 . Accounts. Add @jxsuite/auth and an auth section for sign-in, sessions, roles, and per-table permissions. See Auth and secrets . Two 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 . Hard rules style is always an object of camelCase properties, never a CSS string. Use \"textContent\" for leaf elements; use \"children\": [\"text\", …] for mixed content. IDL properties go on the element; everything else goes in attributes . Prefer CSS clamp() over media queries for simple responsive values. A component's tagName must contain a hyphen (Web Components requires it). $defs holds JSON Schema type definitions only — no functions, no defaults, no runtime artifacts. Template strings ( ${} ) are pure expressions — no statements, no assignments. Function body strings are raw JavaScript where state is in scope; a sidecar export takes state as its first parameter. $head entries are { tagName, attributes } objects, never HTML strings. Layouts inject content with { \"tagName\": \"slot\" } — there is no $slot or $content . All state entries are reactive by default; no signal: true flag exists. timing ( \"compiler\" , \"server\" , \"client\" ) decides where a data source resolves. \"client\" is the default. Check your work Never report a task complete on the strength of the output looking right. Run the loop: jx schema # once per project, and after any change to project.json's \"extensions\" jx validate # after every edit — exits 1 with the failing instance paths jx build # only once validate is clean jx validate prints one block per failing file: pages/about.json: - /children/0: must NOT have additional properties Each 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 . Related Working with agents — where this briefing fits in the wider workflow Machine-readable docs — the schema and documentation URLs an agent can fetch Documents — the long-form version of the document model Site architecture — the project layout these rules describe"},{"id":"docs:framework/agents/authoring-rules#authoring-rules-for-agents","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#authoring-rules-for-agents","title":"Authoring rules for 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.","heading":"Authoring rules for agents","text":"An 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. Everything 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 , which you can copy into your own project's .claude/commands/ . Whatever 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."},{"id":"docs:framework/agents/authoring-rules#the-three-schemas","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#the-three-schemas","title":"Authoring rules for 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.","heading":"The three schemas","text":"Every Jx file validates against one of three JSON Schema 2020-12 documents, published at stable URLs: File Schema Components, pages, layouts ( *.json ) https://jxsuite.com/schema/v1 project.json https://jxsuite.com/schema/project/v1 Class definitions ( *.class.json ) https://jxsuite.com/schema/class/v1 Inside 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 and 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."},{"id":"docs:framework/agents/authoring-rules#project-structure","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#project-structure","title":"Authoring rules for 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.","heading":"Project structure","text":"my-site/ project.json # Required. Site config, global styles, collections, build settings pages/ # File-based routing. Each file = a route. [slug].json = dynamic layouts/ # Shared page shells. Use { \"tagName\": \"slot\" } for content insertion components/ # Reusable custom elements content/ # Markdown/JSON/CSV content collections public/ # Static assets copied verbatim to dist/ Files and folders prefixed with _ inside pages/ are excluded from routing, so a page's private components can sit beside it."},{"id":"docs:framework/agents/authoring-rules#the-document","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#the-document","title":"Authoring rules for 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.","heading":"The document","text":"A Jx document is a JSON object describing a reactive web component: { \"$id\" : \"TaskList\" , \"tagName\" : \"task-list\" , \"$defs\" : {}, \"state\" : {}, \"style\" : {}, \"children\" : [] } Only tagName is required. A tagName containing a hyphen makes the document a custom element."},{"id":"docs:framework/agents/authoring-rules#state-five-shapes-detected-by-structure","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#state-five-shapes-detected-by-structure","title":"Authoring rules for 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.","heading":"State — five shapes, detected by structure","text":"1. Naked value — a scalar, array, or plain object with no reserved keys: \"count\" : 0 , \"items\" : [], \"user\" : { \"name\" : \"\" , \"email\" : \"\" } 2. Typed value — has default , optionally type : \"status\" : { \"type\" : { \"type\" : \"string\" , \"enum\" : [ \"idle\" , \"loading\" ] }, \"default\" : \"idle\" } 3. Computed — a string containing ${} : \"fullName\" : \"${state.firstName} ${state.lastName}\" , \"itemCount\" : \"${state.items.length} items\" 4. Function — $prototype: \"Function\" , with an inline body or an external .js sidecar: \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" }, \"handleInput\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"event\" ], \"body\" : \"state.value = event.target.value\" }, \"validate\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./validators.js\" , \"$export\" : \"validateEmail\" } 5. Data source — $prototype: <ClassName> : \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users\" , \"method\" : \"GET\" }, \"posts\" : { \"$prototype\" : \"ContentCollection\" , \"contentType\" : \"blog\" , \"sort\" : { \"field\" : \"pubDate\" , \"order\" : \"desc\" } } See State and Data prototypes ."},{"id":"docs:framework/agents/authoring-rules#ref-bindings","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#ref-bindings","title":"Authoring rules for 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.","heading":"$ref bindings","text":"Pattern Example Meaning State { \"$ref\": \"#/state/count\" } Reactive binding to state $defs { \"$ref\": \"#/$defs/TodoItem\" } Type definition reference Parent { \"$ref\": \"parent#/theme\" } A value passed in via $props Map item { \"$ref\": \"$map/item\" } Current item in an iteration Map index { \"$ref\": \"$map/index\" } Current zero-based index External { \"$ref\": \"./card.json\" } Another Jx document Window { \"$ref\": \"window#/Math/max\" } A path resolved off the global object Use ${} templates for inline one-off bindings, $ref objects for named or reused signals. See References ."},{"id":"docs:framework/agents/authoring-rules#children","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#children","title":"Authoring rules for 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.","heading":"Children","text":"Static — an array of element objects and text strings, mixed freely: \"children\" : [ { \"tagName\" : \"h1\" , \"textContent\" : \"Hello\" }, { \"tagName\" : \"p\" , \"children\" : [ \"Welcome to \" , { \"tagName\" : \"strong\" , \"textContent\" : \"Jx\" }] } ] Dynamic list — $prototype: \"Array\" : \"children\" : { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todos\" }, \"map\" : { \"tagName\" : \"li\" , \"className\" : \"${$map.item.done ? 'completed' : ''}\" , \"textContent\" : { \"$ref\" : \"$map/item/text\" } } } See Lists and iteration ."},{"id":"docs:framework/agents/authoring-rules#switch-and-cases","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#switch-and-cases","title":"Authoring rules for 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.","heading":"$switch and cases","text":"{ \"$switch\" : { \"$ref\" : \"#/state/currentView\" }, \"cases\" : { \"home\" : { \"$ref\" : \"./views/home.json\" }, \"settings\" : { \"$ref\" : \"./views/settings.json\" } } } See Dynamic switching ."},{"id":"docs:framework/agents/authoring-rules#style","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#style","title":"Authoring rules for 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.","heading":"Style","text":"An 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): \"style\" : { \"display\" : \"flex\" , \"gap\" : \"1rem\" , \"padding\" : \"clamp(1rem, 3vw, 2rem)\" , \":hover\" : { \"backgroundColor\" : \"#f0f0f0\" }, \"&.active\" : { \"borderColor\" : \"blue\" }, \"@--md\" : { \"flexDirection\" : \"row\" }, \"@(prefers-reduced-motion: reduce)\" : { \"transition\" : \"none\" } } See Styling ."},{"id":"docs:framework/agents/authoring-rules#properties-and-attributes","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#properties-and-attributes","title":"Authoring rules for 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.","heading":"Properties and attributes","text":"IDL properties — href , src , textContent , className , hidden , disabled , value , checked — go directly on the element object. Everything else ( aria-* , data-* , role , slot ) goes in attributes : { \"tagName\" : \"a\" , \"href\" : \"/about\" , \"textContent\" : \"About\" , \"attributes\" : { \"aria-label\" : \"About page\" , \"data-section\" : \"nav\" } }"},{"id":"docs:framework/agents/authoring-rules#event-handlers","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#event-handlers","title":"Authoring rules for 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.","heading":"Event handlers","text":"Reference a function from state: { \"tagName\" : \"button\" , \"textContent\" : \"Add\" , \"onclick\" : { \"$ref\" : \"#/state/handleAdd\" } }"},{"id":"docs:framework/agents/authoring-rules#components-and-props","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#components-and-props","title":"Authoring rules for 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.","heading":"Components and props","text":"$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: { \"$elements\" : [ { \"$ref\" : \"./components/user-card.json\" }, \"@shoelace-style/shoelace/components/button/button.js\" ], \"children\" : [ { \"tagName\" : \"user-card\" , \"$props\" : { \"name\" : \"Ada\" , \"count\" : { \"$ref\" : \"#/state/count\" } } } ] } 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 ."},{"id":"docs:framework/agents/authoring-rules#pages","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#pages","title":"Authoring rules for 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.","heading":"Pages","text":"Pages are documents with a few extra top-level fields: { \"title\" : \"About us\" , \"$layout\" : \"./layouts/base.json\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"About our team\" } } ], \"tagName\" : \"main\" , \"children\" : [] } $layout — a path resolved from the project root , or false for no layout. Omit it to use defaults.layout from project.json . $head — merges with the layout's and the project's entries; the page wins on conflicts. $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. tagName is optional on a page that uses a layout. Dynamic route ( pages/blog/[slug].json ): { \"$paths\" : { \"contentType\" : \"blog\" , \"param\" : \"slug\" }, \"state\" : { \"post\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"blog\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } } } See Routing and Content collections ."},{"id":"docs:framework/agents/authoring-rules#layouts","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#layouts","title":"Authoring rules for 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.","heading":"Layouts","text":"Ordinary documents with <slot> elements marking where page content lands. Name a slot to define more than one region: { \"tagName\" : \"div\" , \"children\" : [ { \"tagName\" : \"header\" }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] }, { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"footer\" } ] } See Layouts ."},{"id":"docs:framework/agents/authoring-rules#projectjson","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#projectjson","title":"Authoring rules for 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.","heading":"project.json","text":"{ \"$schema\" : \"./project.schema.json\" , \"name\" : \"My Site\" , \"url\" : \"https://example.com\" , \"extensions\" : [ \"@jxsuite/parser\" ], \"defaults\" : { \"layout\" : \"./layouts/base.json\" , \"lang\" : \"en\" }, \"$head\" : [{ \"tagName\" : \"link\" , \"attributes\" : { \"rel\" : \"icon\" , \"href\" : \"/favicon.svg\" } }], \"imports\" : { \"MarkdownCollection\" : \"@jxsuite/parser/MarkdownCollection.class.json\" }, \"$media\" : { \"--sm\" : \"(min-width: 640px)\" , \"--md\" : \"(min-width: 768px)\" }, \"style\" : { \"fontFamily\" : \"system-ui, sans-serif\" , \"margin\" : \"0\" }, \"content\" : { \"blog\" : { \"source\" : \"./content/blog/\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" } }, \"required\" : [ \"title\" ] } } }, \"build\" : { \"outDir\" : \"./dist\" , \"trailingSlash\" : \"always\" } } 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 and First-party extensions ."},{"id":"docs:framework/agents/authoring-rules#classjson","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#classjson","title":"Authoring rules for 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.","heading":".class.json","text":"{ \"$prototype\" : \"Class\" , \"title\" : \"MyParser\" , \"$implementation\" : \"./my-parser.js\" , \"$defs\" : { \"parameters\" : { \"src\" : { \"identifier\" : \"src\" , \"type\" : { \"type\" : \"string\" } } }, \"constructor\" : { \"role\" : \"constructor\" , \"$prototype\" : \"Function\" , \"parameters\" : [{ \"$ref\" : \"#/$defs/parameters/src\" }] }, \"methods\" : { \"resolve\" : { \"role\" : \"method\" , \"identifier\" : \"resolve\" , \"returnType\" : { \"type\" : \"object\" } } } } } See Custom classes ."},{"id":"docs:framework/agents/authoring-rules#the-server-tier","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#the-server-tier","title":"Authoring rules for 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.","heading":"The server tier","text":"A brochure site is not the ceiling. Three additions turn a Jx project into an application, and an agent should know they exist: 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 . 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 . Accounts. Add @jxsuite/auth and an auth section for sign-in, sessions, roles, and per-table permissions. See Auth and secrets . Two 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 ."},{"id":"docs:framework/agents/authoring-rules#hard-rules","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#hard-rules","title":"Authoring rules for 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.","heading":"Hard rules","text":"style is always an object of camelCase properties, never a CSS string. Use \"textContent\" for leaf elements; use \"children\": [\"text\", …] for mixed content. IDL properties go on the element; everything else goes in attributes . Prefer CSS clamp() over media queries for simple responsive values. A component's tagName must contain a hyphen (Web Components requires it). $defs holds JSON Schema type definitions only — no functions, no defaults, no runtime artifacts. Template strings ( ${} ) are pure expressions — no statements, no assignments. Function body strings are raw JavaScript where state is in scope; a sidecar export takes state as its first parameter. $head entries are { tagName, attributes } objects, never HTML strings. Layouts inject content with { \"tagName\": \"slot\" } — there is no $slot or $content . All state entries are reactive by default; no signal: true flag exists. timing ( \"compiler\" , \"server\" , \"client\" ) decides where a data source resolves. \"client\" is the default."},{"id":"docs:framework/agents/authoring-rules#check-your-work","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#check-your-work","title":"Authoring rules for 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.","heading":"Check your work","text":"Never report a task complete on the strength of the output looking right. Run the loop: jx schema # once per project, and after any change to project.json's \"extensions\" jx validate # after every edit — exits 1 with the failing instance paths jx build # only once validate is clean jx validate prints one block per failing file: pages/about.json: - /children/0: must NOT have additional properties Each 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 ."},{"id":"docs:framework/agents/authoring-rules#related","collection":"docs","slug":"framework/agents/authoring-rules","url":"/docs/framework/agents/authoring-rules/#related","title":"Authoring rules for 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.","heading":"Related","text":"Working with agents — where this briefing fits in the wider workflow Machine-readable docs — the schema and documentation URLs an agent can fetch Documents — the long-form version of the document model Site architecture — the project layout these rules describe"},{"id":"docs:framework/agents/machine-readable","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/","title":"Machine-readable docs","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.","heading":"","text":"Machine-readable docs Everything 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. The documentation corpus llms.txt https://jxsuite.com/llms.txt A plain-text index in the llms.txt convention : a one-paragraph summary of the project, then one ## heading per documentation section, then one line per page in sidebar order: # Jx Suite > 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. … ## Start here - [Start here](https://jxsuite.com/docs/start/): What Jx Suite is, how to install Jx Studio, and where to go first … - [Install Jx Studio](https://jxsuite.com/docs/start/install/): Download the Jx Studio desktop app … - [Your first project](https://jxsuite.com/docs/start/first-project/): … Each section's landing page comes first, then its children, exactly as the sidebar orders them. It 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. full-docs.json https://jxsuite.com/docs/full-docs.json The same corpus in the same order, with the prose included. A JSON array of objects, one per page: Field What it holds slug The docs path, e.g. framework/agents/machine-readable url The canonical page URL title The page's frontmatter title description The page's frontmatter description section The top-level section label, e.g. Framework markdown The full page body as Markdown, frontmatter stripped This 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. search-index.json https://jxsuite.com/search-index.json The index that powers the site's own search box, emitted by the search extension 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 . Reach 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. 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 . The schemas The 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: URL Validates https://jxsuite.com/schema/v1 Components, pages, and layouts https://jxsuite.com/schema/project/v1 project.json https://jxsuite.com/schema/class/v1 *.class.json class definitions https://jxsuite.com/schema/document/paths/v2 A page's $paths source — referenced by schema/v1 , and overridden per project Any 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. For 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 . Related Working with agents — the workflow these endpoints support Authoring rules for agents — the briefing to hand an agent alongside them CLI commands — jx schema and jx validate Site search — how the search index is configured and emitted Contributing to these docs — where the corpus comes from"},{"id":"docs:framework/agents/machine-readable#machine-readable-docs","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#machine-readable-docs","title":"Machine-readable docs","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.","heading":"Machine-readable docs","text":"Everything 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."},{"id":"docs:framework/agents/machine-readable#the-documentation-corpus","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#the-documentation-corpus","title":"Machine-readable docs","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.","heading":"The documentation corpus","text":""},{"id":"docs:framework/agents/machine-readable#llmstxt","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#llmstxt","title":"Machine-readable docs","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.","heading":"llms.txt","text":"https://jxsuite.com/llms.txt A plain-text index in the llms.txt convention : a one-paragraph summary of the project, then one ## heading per documentation section, then one line per page in sidebar order: # Jx Suite > 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. … ## Start here - [Start here](https://jxsuite.com/docs/start/): What Jx Suite is, how to install Jx Studio, and where to go first … - [Install Jx Studio](https://jxsuite.com/docs/start/install/): Download the Jx Studio desktop app … - [Your first project](https://jxsuite.com/docs/start/first-project/): … Each section's landing page comes first, then its children, exactly as the sidebar orders them. It 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."},{"id":"docs:framework/agents/machine-readable#full-docsjson","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#full-docsjson","title":"Machine-readable docs","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.","heading":"full-docs.json","text":"https://jxsuite.com/docs/full-docs.json The same corpus in the same order, with the prose included. A JSON array of objects, one per page: Field What it holds slug The docs path, e.g. framework/agents/machine-readable url The canonical page URL title The page's frontmatter title description The page's frontmatter description section The top-level section label, e.g. Framework markdown The full page body as Markdown, frontmatter stripped This 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."},{"id":"docs:framework/agents/machine-readable#search-indexjson","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#search-indexjson","title":"Machine-readable docs","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.","heading":"search-index.json","text":"https://jxsuite.com/search-index.json The index that powers the site's own search box, emitted by the search extension 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 . Reach 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. 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 ."},{"id":"docs:framework/agents/machine-readable#the-schemas","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#the-schemas","title":"Machine-readable docs","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.","heading":"The schemas","text":"The 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: URL Validates https://jxsuite.com/schema/v1 Components, pages, and layouts https://jxsuite.com/schema/project/v1 project.json https://jxsuite.com/schema/class/v1 *.class.json class definitions https://jxsuite.com/schema/document/paths/v2 A page's $paths source — referenced by schema/v1 , and overridden per project Any 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. For 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 ."},{"id":"docs:framework/agents/machine-readable#related","collection":"docs","slug":"framework/agents/machine-readable","url":"/docs/framework/agents/machine-readable/#related","title":"Machine-readable docs","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.","heading":"Related","text":"Working with agents — the workflow these endpoints support Authoring rules for agents — the briefing to hand an agent alongside them CLI commands — jx schema and jx validate Site search — how the search index is configured and emitted Contributing to these docs — where the corpus comes from"},{"id":"docs:framework/build/cli","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"","text":"CLI commands Jx 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. bun create @jxsuite Scaffold a new Jx project. bun create @jxsuite <directory> [--template <id>] Flag What it does --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 . The 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 . A blank scaffold produces: 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 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) layouts/base.json , pages/index.md , empty components/ , content/ , and public/ directories, and a .gitignore wrangler.jsonc for the Cloudflare adapters; the mobile-app template overlays an app-shell layout and home page Choosing 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. jx build Build the site to the configured outDir (default dist/ ). See How compilation works for what happens inside. jx build [root] [--verbose] [--no-clean] Flag What it does --verbose Print detailed build progress. --no-clean Don't remove the output directory first. Prints a summary ( Done: 12 routes → 34 files ) on success; lists route errors and exits 1 if any page failed to compile. jx dev Start 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 and @jxsuite/server in the project's devDependencies — both part of every scaffolded project; jx dev prints an install hint when either is missing. jx dev [root] [--port <n>] Flag What it does --port <n> Port to listen on (default 3000 ). See The dev server for what runs underneath. jx preview Serve an already-built dist/ directory — a dependency-free static server for checking the production build locally. Run jx build first. jx preview [root] [--port <n>] Flag What it does --port <n> Port to listen on (default 4173 ). Directory URLs map to their index.html (with or without a trailing slash) and 404.html is served for missing routes when present. jx schema Generate 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. jx schema [root] Re-run it whenever you change the extensions list so editors and jx validate see the current schema. jx validate Validate the whole project tree (run jx schema first). No flags. jx validate [root] Checks, 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. Prints Project is valid (N files checked) on success; otherwise lists each file's violations with their instance paths and exits 1 . The 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. jx db push Additively 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. jx db push [root] [--dry-run] [--connection <name>] Flag What it does --dry-run Print the SQL statements without executing them. --connection <name> Restrict the push to one connections entry. The 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. The 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. 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. What the scaffolded scripts run Script Runs bun run build jx build bun run dev jx dev bun run preview jx preview bun run deploy (CF only) wrangler deploy / wrangler pages deploy dist Related How compilation works — what jx build produces and why The dev server — local development without a build Site architecture — the project layout these commands operate on Data tables — the tables and push plan behind jx db push"},{"id":"docs:framework/build/cli#cli-commands","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#cli-commands","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"CLI commands","text":"Jx 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."},{"id":"docs:framework/build/cli#bun-create-jxsuite","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#bun-create-jxsuite","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"bun create @jxsuite","text":"Scaffold a new Jx project. bun create @jxsuite <directory> [--template <id>] Flag What it does --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 . The 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 . A blank scaffold produces: 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 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) layouts/base.json , pages/index.md , empty components/ , content/ , and public/ directories, and a .gitignore wrangler.jsonc for the Cloudflare adapters; the mobile-app template overlays an app-shell layout and home page Choosing 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."},{"id":"docs:framework/build/cli#jx-build","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-build","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx build","text":"Build the site to the configured outDir (default dist/ ). See How compilation works for what happens inside. jx build [root] [--verbose] [--no-clean] Flag What it does --verbose Print detailed build progress. --no-clean Don't remove the output directory first. Prints a summary ( Done: 12 routes → 34 files ) on success; lists route errors and exits 1 if any page failed to compile."},{"id":"docs:framework/build/cli#jx-dev","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-dev","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx dev","text":"Start 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 and @jxsuite/server in the project's devDependencies — both part of every scaffolded project; jx dev prints an install hint when either is missing. jx dev [root] [--port <n>] Flag What it does --port <n> Port to listen on (default 3000 ). See The dev server for what runs underneath."},{"id":"docs:framework/build/cli#jx-preview","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-preview","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx preview","text":"Serve an already-built dist/ directory — a dependency-free static server for checking the production build locally. Run jx build first. jx preview [root] [--port <n>] Flag What it does --port <n> Port to listen on (default 4173 ). Directory URLs map to their index.html (with or without a trailing slash) and 404.html is served for missing routes when present."},{"id":"docs:framework/build/cli#jx-schema","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-schema","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx schema","text":"Generate 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. jx schema [root] Re-run it whenever you change the extensions list so editors and jx validate see the current schema."},{"id":"docs:framework/build/cli#jx-validate","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-validate","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx validate","text":"Validate the whole project tree (run jx schema first). No flags. jx validate [root] Checks, 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. Prints Project is valid (N files checked) on success; otherwise lists each file's violations with their instance paths and exits 1 . The 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."},{"id":"docs:framework/build/cli#jx-db-push","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#jx-db-push","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"jx db push","text":"Additively 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. jx db push [root] [--dry-run] [--connection <name>] Flag What it does --dry-run Print the SQL statements without executing them. --connection <name> Restrict the push to one connections entry. The 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. The 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. 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."},{"id":"docs:framework/build/cli#what-the-scaffolded-scripts-run","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#what-the-scaffolded-scripts-run","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"What the scaffolded scripts run","text":"Script Runs bun run build jx build bun run dev jx dev bun run preview jx preview bun run deploy (CF only) wrangler deploy / wrangler pages deploy dist"},{"id":"docs:framework/build/cli#related","collection":"docs","slug":"framework/build/cli","url":"/docs/framework/build/cli/#related","title":"CLI commands","description":"Reference for bun create @jxsuite and the jx CLI — build, schema, validate, and db push — with every flag as implemented.","heading":"Related","text":"How compilation works — what jx build produces and why The dev server — local development without a build Site architecture — the project layout these commands operate on Data tables — the tables and push plan behind jx db push"},{"id":"docs:framework/build/dev-server","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"","text":"The dev server During 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. Starting it The whole server is one call, createDevServer : // server.js import { createDevServer } from \"@jxsuite/server\" ; await createDevServer ({ root: import . meta .dir, port: 3000 , }); Run it with bun run server.js and open http://localhost:3000/ . The full options surface: Option Default What it does root (required) Project root to serve; every file operation is contained to it. port 3000 Listen port. builds [] Bun.build entries ( entrypoints , outdir , optional match , label ) bundled at startup and selectively rebuilt when a changed file matches. watch true File watching + live reload. Pass false to disable, or an options object for the watcher. studio true Mounts the /__studio/* API that Jx Studio talks to. middleware — (req, url) => Response | null — your own routes, checked before static file serving. For a site project, jx dev 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. Live reload The 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> : < script > new EventSource ( \"/__reload\" ). onmessage = () => location. reload (); </ script > Save 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. How Studio is served Studio 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. Studio'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. Running server-side code: the two proxies Production 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 . Module resolution ( POST /__jx_resolve__ ) When 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. Server functions ( POST /__jx_server__ ) Functions marked timing: \"server\" never ship to the browser. In development the runtime posts the call instead: { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"arguments\" : { \"userId\" : 42 } } The 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 . Extensions 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. Static files and npm packages Anything 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. Related CLI commands — what bun create @jxsuite scaffolds and what jx can run How compilation works — what replaces the proxies in production Site architecture — the directory layout the server serves"},{"id":"docs:framework/build/dev-server#the-dev-server","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#the-dev-server","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"The dev server","text":"During 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."},{"id":"docs:framework/build/dev-server#starting-it","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#starting-it","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Starting it","text":"The whole server is one call, createDevServer : // server.js import { createDevServer } from \"@jxsuite/server\" ; await createDevServer ({ root: import . meta .dir, port: 3000 , }); Run it with bun run server.js and open http://localhost:3000/ . The full options surface: Option Default What it does root (required) Project root to serve; every file operation is contained to it. port 3000 Listen port. builds [] Bun.build entries ( entrypoints , outdir , optional match , label ) bundled at startup and selectively rebuilt when a changed file matches. watch true File watching + live reload. Pass false to disable, or an options object for the watcher. studio true Mounts the /__studio/* API that Jx Studio talks to. middleware — (req, url) => Response | null — your own routes, checked before static file serving. For a site project, jx dev 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."},{"id":"docs:framework/build/dev-server#live-reload","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#live-reload","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Live reload","text":"The 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> : < script > new EventSource ( \"/__reload\" ). onmessage = () => location. reload (); </ script > Save 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."},{"id":"docs:framework/build/dev-server#how-studio-is-served","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#how-studio-is-served","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"How Studio is served","text":"Studio 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. Studio'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."},{"id":"docs:framework/build/dev-server#running-server-side-code-the-two-proxies","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#running-server-side-code-the-two-proxies","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Running server-side code: the two proxies","text":"Production 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 ."},{"id":"docs:framework/build/dev-server#module-resolution-post-__jx_resolve__","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#module-resolution-post-__jx_resolve__","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Module resolution ( POST /__jx_resolve__ )","text":"When 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."},{"id":"docs:framework/build/dev-server#server-functions-post-__jx_server__","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#server-functions-post-__jx_server__","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Server functions ( POST /__jx_server__ )","text":"Functions marked timing: \"server\" never ship to the browser. In development the runtime posts the call instead: { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"arguments\" : { \"userId\" : 42 } } The 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 . Extensions 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."},{"id":"docs:framework/build/dev-server#static-files-and-npm-packages","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#static-files-and-npm-packages","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Static files and npm packages","text":"Anything 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."},{"id":"docs:framework/build/dev-server#related","collection":"docs","slug":"framework/build/dev-server","url":"/docs/framework/build/dev-server/#related","title":"The dev server","description":"The @jxsuite/server development server: live reload over SSE, the Studio API, and the proxies that run server-side code while you develop.","heading":"Related","text":"CLI commands — what bun create @jxsuite scaffolds and what jx can run How compilation works — what replaces the proxies in production Site architecture — the directory layout the server serves"},{"id":"docs:framework/concepts/color-schemes","collection":"docs","slug":"framework/concepts/color-schemes","url":"/docs/framework/concepts/color-schemes/","title":"Color schemes","description":"Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.","heading":"","text":"Color schemes A 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 — no separate theme system. Declaring scheme support Add a scheme query to $media in project.json — an entry whose value is exactly a prefers-color-scheme query: { \"$media\" : { \"--md\" : \"(min-width: 768px)\" , \"--dark\" : \"(prefers-color-scheme: dark)\" } } Then 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. { \"style\" : { \"--color-bg\" : \"#ffffff\" , \"--color-text\" : \"#18181b\" , \"@--dark\" : { \"--color-bg\" : \"#0a0a0a\" , \"--color-text\" : \"#fafafa\" } } } @--dark blocks work in component and element styles too, exactly like responsive breakpoints: { \"tagName\" : \"div\" , \"style\" : { \"boxShadow\" : \"0 2px 8px rgba(0,0,0,0.12)\" , \"@--dark\" : { \"boxShadow\" : \"0 2px 8px rgba(0,0,0,0.5)\" } } } What declaring a scheme query does Declaring a scheme query opts the site into the forced-scheme contract: 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. color-scheme: light dark is declared on :root , with forced-mode overrides — native form controls and scrollbars follow along. 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. The visitor override contract Two constants make up the contract: Constant Where Meaning data-color-scheme attribute on <html> \"light\" or \"dark\" forces that scheme; absent = auto jx-color-scheme localStorage key persisted forced scheme; absent = auto A switcher only needs to keep the two in sync. Cycling auto → light → dark: const next = { auto: \"light\" , light: \"dark\" , dark: \"auto\" }[current]; if (next === \"auto\" ) { localStorage. removeItem ( \"jx-color-scheme\" ); document.documentElement. removeAttribute ( \"data-color-scheme\" ); } else { localStorage. setItem ( \"jx-color-scheme\" , next); document.documentElement. setAttribute ( \"data-color-scheme\" , next); } The pre-paint script (injected automatically) reads the storage key on the next load, so the choice sticks across pages and visits. Only 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. Studio 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."},{"id":"docs:framework/concepts/color-schemes#color-schemes","collection":"docs","slug":"framework/concepts/color-schemes","url":"/docs/framework/concepts/color-schemes/#color-schemes","title":"Color schemes","description":"Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.","heading":"Color schemes","text":"A 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 — no separate theme system."},{"id":"docs:framework/concepts/color-schemes#declaring-scheme-support","collection":"docs","slug":"framework/concepts/color-schemes","url":"/docs/framework/concepts/color-schemes/#declaring-scheme-support","title":"Color schemes","description":"Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.","heading":"Declaring scheme support","text":"Add a scheme query to $media in project.json — an entry whose value is exactly a prefers-color-scheme query: { \"$media\" : { \"--md\" : \"(min-width: 768px)\" , \"--dark\" : \"(prefers-color-scheme: dark)\" } } Then 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. { \"style\" : { \"--color-bg\" : \"#ffffff\" , \"--color-text\" : \"#18181b\" , \"@--dark\" : { \"--color-bg\" : \"#0a0a0a\" , \"--color-text\" : \"#fafafa\" } } } @--dark blocks work in component and element styles too, exactly like responsive breakpoints: { \"tagName\" : \"div\" , \"style\" : { \"boxShadow\" : \"0 2px 8px rgba(0,0,0,0.12)\" , \"@--dark\" : { \"boxShadow\" : \"0 2px 8px rgba(0,0,0,0.5)\" } } }"},{"id":"docs:framework/concepts/color-schemes#what-declaring-a-scheme-query-does","collection":"docs","slug":"framework/concepts/color-schemes","url":"/docs/framework/concepts/color-schemes/#what-declaring-a-scheme-query-does","title":"Color schemes","description":"Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.","heading":"What declaring a scheme query does","text":"Declaring a scheme query opts the site into the forced-scheme contract: 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. color-scheme: light dark is declared on :root , with forced-mode overrides — native form controls and scrollbars follow along. 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."},{"id":"docs:framework/concepts/color-schemes#the-visitor-override-contract","collection":"docs","slug":"framework/concepts/color-schemes","url":"/docs/framework/concepts/color-schemes/#the-visitor-override-contract","title":"Color schemes","description":"Declare light and dark variants of a design and let visitors force either scheme with the auto/light/dark contract.","heading":"The visitor override contract","text":"Two constants make up the contract: Constant Where Meaning data-color-scheme attribute on <html> \"light\" or \"dark\" forces that scheme; absent = auto jx-color-scheme localStorage key persisted forced scheme; absent = auto A switcher only needs to keep the two in sync. Cycling auto → light → dark: const next = { auto: \"light\" , light: \"dark\" , dark: \"auto\" }[current]; if (next === \"auto\" ) { localStorage. removeItem ( \"jx-color-scheme\" ); document.documentElement. removeAttribute ( \"data-color-scheme\" ); } else { localStorage. setItem ( \"jx-color-scheme\" , next); document.documentElement. setAttribute ( \"data-color-scheme\" , next); } The pre-paint script (injected automatically) reads the storage key on the next load, so the choice sticks across pages and visits. Only 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. Studio 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."},{"id":"docs:framework/concepts/components","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"","text":"Components 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. A Jx component is a single .json file. All state, computed values, and functions are declared in state . Simple components need no sidecar file. Self-Describing Components { \"$id\": \"Counter\", \"state\": { \"count\": 0, \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" } }, \"tagName\": \"my-counter\", \"children\": [ { \"tagName\": \"span\", \"textContent\": \"${state.count}\" }, { \"tagName\": \"button\", \"textContent\": \"+\", \"onclick\": { \"$ref\": \"#/state/increment\" } } ] } State Shapes Every entry in state falls into one of four shapes, determinable by inspection: Shape 1 — Naked Value A JSON scalar, array, or plain object with no reserved keys: { \"state\": { \"count\": 0, \"name\": \"World\", \"tags\": [] } } Shape 2 — Typed Value An object with a default property and optional type : { \"state\": { \"count\": { \"type\": { \"$ref\": \"#/$defs/Count\" }, \"default\": 0, \"description\": \"Current counter value\" } } } Shape 3 — Computed (Template String) A string containing ${} syntax: { \"state\": { \"fullName\": \"${state.firstName} ${state.lastName}\", \"isEmpty\": \"${state.items.length === 0}\" } } Shape 4 — Prototype ( $prototype ) An object with $prototype for functions and data sources: { \"state\": { \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" }, \"userData\": { \"$prototype\": \"Request\", \"url\": \"/api/users/\", \"method\": \"GET\" } } } External Sidecars When functions grow complex, extract them to a .js file: { \"state\": { \"increment\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" }, \"decrement\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" } } } export function increment(state) { state.count++; } export function decrement(state) { state.count = Math.max(0, state.count - 1); } The first parameter is always state — the component's reactive scope. this is never used. Custom Elements A component whose tagName contains a hyphen is a custom element: { \"tagName\": \"user-card\", \"state\": { \"username\": \"Guest\", \"status\": \"offline\" }, \"children\": [{ \"tagName\": \"h3\", \"textContent\": \"${state.username}\" }] } Custom elements render to the light DOM (no Shadow DOM). Style scoping uses data-jx attributes. Props and Encapsulation Props 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: { \"$elements\": { \"my-card\": { \"$ref\": \"./card.json\" } }, \"children\": [ { \"tagName\": \"my-card\", \"$props\": { \"title\": \"Static string\", \"count\": { \"$ref\": \"#/state/count\" } } } ] } Signal scope is bounded at the component level. No implicit scope leaking."},{"id":"docs:framework/concepts/components#components","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#components","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Components","text":"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. A Jx component is a single .json file. All state, computed values, and functions are declared in state . Simple components need no sidecar file."},{"id":"docs:framework/concepts/components#self-describing-components","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#self-describing-components","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Self-Describing Components","text":"{ \"$id\": \"Counter\", \"state\": { \"count\": 0, \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" } }, \"tagName\": \"my-counter\", \"children\": [ { \"tagName\": \"span\", \"textContent\": \"${state.count}\" }, { \"tagName\": \"button\", \"textContent\": \"+\", \"onclick\": { \"$ref\": \"#/state/increment\" } } ] }"},{"id":"docs:framework/concepts/components#state-shapes","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#state-shapes","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"State Shapes","text":"Every entry in state falls into one of four shapes, determinable by inspection:"},{"id":"docs:framework/concepts/components#shape-1-naked-value","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#shape-1-naked-value","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Shape 1 — Naked Value","text":"A JSON scalar, array, or plain object with no reserved keys: { \"state\": { \"count\": 0, \"name\": \"World\", \"tags\": [] } }"},{"id":"docs:framework/concepts/components#shape-2-typed-value","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#shape-2-typed-value","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Shape 2 — Typed Value","text":"An object with a default property and optional type : { \"state\": { \"count\": { \"type\": { \"$ref\": \"#/$defs/Count\" }, \"default\": 0, \"description\": \"Current counter value\" } } }"},{"id":"docs:framework/concepts/components#shape-3-computed-template-string","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#shape-3-computed-template-string","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Shape 3 — Computed (Template String)","text":"A string containing ${} syntax: { \"state\": { \"fullName\": \"${state.firstName} ${state.lastName}\", \"isEmpty\": \"${state.items.length === 0}\" } }"},{"id":"docs:framework/concepts/components#shape-4-prototype-prototype","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#shape-4-prototype-prototype","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Shape 4 — Prototype ( $prototype )","text":"An object with $prototype for functions and data sources: { \"state\": { \"increment\": { \"$prototype\": \"Function\", \"body\": \"state.count++\" }, \"userData\": { \"$prototype\": \"Request\", \"url\": \"/api/users/\", \"method\": \"GET\" } } }"},{"id":"docs:framework/concepts/components#external-sidecars","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#external-sidecars","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"External Sidecars","text":"When functions grow complex, extract them to a .js file: { \"state\": { \"increment\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" }, \"decrement\": { \"$prototype\": \"Function\", \"$src\": \"./counter.js\" } } } export function increment(state) { state.count++; } export function decrement(state) { state.count = Math.max(0, state.count - 1); } The first parameter is always state — the component's reactive scope. this is never used."},{"id":"docs:framework/concepts/components#custom-elements","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#custom-elements","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Custom Elements","text":"A component whose tagName contains a hyphen is a custom element: { \"tagName\": \"user-card\", \"state\": { \"username\": \"Guest\", \"status\": \"offline\" }, \"children\": [{ \"tagName\": \"h3\", \"textContent\": \"${state.username}\" }] } Custom elements render to the light DOM (no Shadow DOM). Style scoping uses data-jx attributes."},{"id":"docs:framework/concepts/components#props-and-encapsulation","collection":"docs","slug":"framework/concepts/components","url":"/docs/framework/concepts/components/#props-and-encapsulation","title":"Components","description":"How Jx components work: self-describing JSON, state management, external sidecars, and custom elements.","heading":"Props and Encapsulation","text":"Props 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: { \"$elements\": { \"my-card\": { \"$ref\": \"./card.json\" } }, \"children\": [ { \"tagName\": \"my-card\", \"$props\": { \"title\": \"Static string\", \"count\": { \"$ref\": \"#/state/count\" } } } ] } Signal scope is bounded at the component level. No implicit scope leaking."},{"id":"docs:framework/concepts/data-prototypes","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"","text":"Data prototypes Studio writes this format for you. The source editors in the State panel ( Data sources ) generate these entries field by field — this page documents the underlying model. A $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. The smallest complete data prototype — an HTTP request whose URL tracks another state value: { \"state\" : { \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users/\" , \"urlParams\" : { \"$ref\" : \"#/state/userId\" }, \"method\" : \"GET\" } } } Built-in Web-API prototypes These resolve automatically, with no imports or $src , and each maps to a genuine Web API: Request — HTTP fetch with reactive URL parameters, debouncing, and manual mode. LocalStorage — a persistent key-value entry that survives closing the browser. SessionStorage — the same, scoped to the visit. Cookie — a cookie with maxAge , path , domain , secure , and sameSite . IndexedDB — a browser database store with indexes and CRUD helpers. FormData — form fields assembled for submission. URLSearchParams — a computed query string. Set / Map — reactive wrappers over the corresponding collections. Blob — binary data from parts and a MIME type. Array — a reactive array driving a mapped list . Field-level documentation for each source lives in Data sources . A storage entry is as small as: { \"theme\" : { \"$prototype\" : \"LocalStorage\" , \"key\" : \"theme\" , \"default\" : \"light\" } } Content prototypes Content loaders are built in too, and resolve at build time ( timing: \"compiler\" — see Timing ): MarkdownFile — parses one .md file into frontmatter and a content tree. MarkdownCollection — globs and parses many .md files. ContentCollection — a schema-validated, multi-format content source. ContentEntry — a single entry within a collection. { \"posts\" : { \"$prototype\" : \"MarkdownCollection\" , \"src\" : \"./content/posts/*.md\" , \"timing\" : \"compiler\" } } These names map internally to .class.json implementations shipped with Jx — no configuration needed. External classes Any 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 : { \"forecast\" : { \"$prototype\" : \"WeatherForecast\" , \"$src\" : \"./lib/WeatherForecast.class.json\" , \"location\" : \"Lancaster, PA\" , \"days\" : 5 } } The 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. Import maps To avoid repeating $src on every entry, declare a top-level imports map from prototype names to .class.json paths: { \"imports\" : { \"WeatherForecast\" : \"@acme/weather/WeatherForecast.class.json\" , \"GeoLocation\" : \"./lib/GeoLocation.class.json\" }, \"state\" : { \"forecast\" : { \"$prototype\" : \"WeatherForecast\" , \"location\" : \"Lancaster, PA\" }, \"coords\" : { \"$prototype\" : \"GeoLocation\" , \"address\" : \"123 Main St\" } } } 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. How it works The 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 . Rules A non-Function $src must point to a .class.json file — direct .js sources are for $prototype: \"Function\" only (see Functions and sidecars ). Reserved keys ( $prototype , $src , $export , timing , default , description ) are never passed to the constructor. Import-map values must end in .class.json ; anything else is warned about and skipped. An entry's own $src always beats the import map. Built-in prototypes need no imports entry and are unaffected by the map unless explicitly overridden. Related Timing — client, server, and compiler resolution State — the grammar prototypes live inside Lists — iterating over a source's resolved array Reactivity — how resolved values propagate Data sources in Studio — every field, source by source Data explorer in Studio — inspecting resolved values live"},{"id":"docs:framework/concepts/data-prototypes#data-prototypes","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#data-prototypes","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Data prototypes","text":"Studio writes this format for you. The source editors in the State panel ( Data sources ) generate these entries field by field — this page documents the underlying model. A $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. The smallest complete data prototype — an HTTP request whose URL tracks another state value: { \"state\" : { \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users/\" , \"urlParams\" : { \"$ref\" : \"#/state/userId\" }, \"method\" : \"GET\" } } }"},{"id":"docs:framework/concepts/data-prototypes#built-in-web-api-prototypes","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#built-in-web-api-prototypes","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Built-in Web-API prototypes","text":"These resolve automatically, with no imports or $src , and each maps to a genuine Web API: Request — HTTP fetch with reactive URL parameters, debouncing, and manual mode. LocalStorage — a persistent key-value entry that survives closing the browser. SessionStorage — the same, scoped to the visit. Cookie — a cookie with maxAge , path , domain , secure , and sameSite . IndexedDB — a browser database store with indexes and CRUD helpers. FormData — form fields assembled for submission. URLSearchParams — a computed query string. Set / Map — reactive wrappers over the corresponding collections. Blob — binary data from parts and a MIME type. Array — a reactive array driving a mapped list . Field-level documentation for each source lives in Data sources . A storage entry is as small as: { \"theme\" : { \"$prototype\" : \"LocalStorage\" , \"key\" : \"theme\" , \"default\" : \"light\" } }"},{"id":"docs:framework/concepts/data-prototypes#content-prototypes","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#content-prototypes","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Content prototypes","text":"Content loaders are built in too, and resolve at build time ( timing: \"compiler\" — see Timing ): MarkdownFile — parses one .md file into frontmatter and a content tree. MarkdownCollection — globs and parses many .md files. ContentCollection — a schema-validated, multi-format content source. ContentEntry — a single entry within a collection. { \"posts\" : { \"$prototype\" : \"MarkdownCollection\" , \"src\" : \"./content/posts/*.md\" , \"timing\" : \"compiler\" } } These names map internally to .class.json implementations shipped with Jx — no configuration needed."},{"id":"docs:framework/concepts/data-prototypes#external-classes","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#external-classes","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"External classes","text":"Any 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 : { \"forecast\" : { \"$prototype\" : \"WeatherForecast\" , \"$src\" : \"./lib/WeatherForecast.class.json\" , \"location\" : \"Lancaster, PA\" , \"days\" : 5 } } The 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."},{"id":"docs:framework/concepts/data-prototypes#import-maps","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#import-maps","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Import maps","text":"To avoid repeating $src on every entry, declare a top-level imports map from prototype names to .class.json paths: { \"imports\" : { \"WeatherForecast\" : \"@acme/weather/WeatherForecast.class.json\" , \"GeoLocation\" : \"./lib/GeoLocation.class.json\" }, \"state\" : { \"forecast\" : { \"$prototype\" : \"WeatherForecast\" , \"location\" : \"Lancaster, PA\" }, \"coords\" : { \"$prototype\" : \"GeoLocation\" , \"address\" : \"123 Main St\" } } } 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."},{"id":"docs:framework/concepts/data-prototypes#how-it-works","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#how-it-works","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"How it works","text":"The 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 ."},{"id":"docs:framework/concepts/data-prototypes#rules","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#rules","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Rules","text":"A non-Function $src must point to a .class.json file — direct .js sources are for $prototype: \"Function\" only (see Functions and sidecars ). Reserved keys ( $prototype , $src , $export , timing , default , description ) are never passed to the constructor. Import-map values must end in .class.json ; anything else is warned about and skipped. An entry's own $src always beats the import map. Built-in prototypes need no imports entry and are unaffected by the map unless explicitly overridden."},{"id":"docs:framework/concepts/data-prototypes#related","collection":"docs","slug":"framework/concepts/data-prototypes","url":"/docs/framework/concepts/data-prototypes/#related","title":"Data prototypes","description":"The $prototype model in Jx: built-in Web-API data sources, content collections, external classes with .class.json, and import maps.","heading":"Related","text":"Timing — client, server, and compiler resolution State — the grammar prototypes live inside Lists — iterating over a source's resolved array Reactivity — how resolved values propagate Data sources in Studio — every field, source by source Data explorer in Studio — inspecting resolved values live"},{"id":"docs:framework/concepts/documents","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"","text":"Documents Studio writes this format for you. Every page and component you build in Studio is saved as one of these files — open Code mode to see the raw source of whatever document you have open. A 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. The smallest complete document: { \"$schema\" : \"https://jxsuite.com/schema/v1\" , \"$id\" : \"Hello\" , \"state\" : { \"name\" : \"World\" }, \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Hello, ${state.name}!\" }] } Root fields Every document is a JSON object with up to six top-level fields: Field Required Description $schema Recommended Schema the file is checked against — the dialect URI, or the project's generated bundle (below) $id Recommended Component identifier, used by tooling $defs Optional Pure JSON Schema type definitions — tooling only, never rendered state Optional Reactive state: values, computed entries, functions, and data sources — see State tagName Required HTML tag name for the root element children Optional Array of child elements , text nodes, and repeaters , mixed freely Only tagName is required. A document can also carry $media (named breakpoints, see Styling ) and $elements (custom-element dependencies, see Components ). 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 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 . The 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. $defs — type definitions $defs holds pure JSON Schema 2020-12 type definitions. They exist for tooling — validation, autocomplete, Studio's input controls — and produce nothing at runtime: { \"$defs\" : { \"Count\" : { \"type\" : \"integer\" , \"minimum\" : 0 , \"maximum\" : 100 }, \"Status\" : { \"type\" : \"string\" , \"enum\" : [ \"idle\" , \"loading\" , \"success\" , \"error\" ] } } } State entries reference these types with $ref ( { \"$ref\": \"#/$defs/Count\" } ). $defs entries never contain default , $prototype , or template strings — runtime values belong in state . Component or page fragment What a document is follows from its root tagName and where the file lives: A root tagName containing a hyphen ( \"my-counter\" , \"user-card\" ) defines a custom element — a reusable component that other documents instantiate by reference. 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 ) or inlined into another document via $ref (see References ). Either kind may declare state ; the difference is that a custom element gets its own isolated scope and a $props boundary (see Props and scope ). Why plain JSON Jx documents are valid JSON — not JavaScript object literals, not JSX, not a template DSL. That distinction is load-bearing: JSON serializes and deserializes without executing code. There is no this ambiguity — self-references are explicit $ref pointers. Visual builders, IDEs, validators, and bundlers understand it natively. JSON Schema tooling (validation, autocomplete, LSP) applies directly. The format also follows the rule of least power : prefer a $ref binding over a template expression, a template expression over a declarative expression , and a handler function only when logic cannot be expressed otherwise. How it works Jx 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 compiles the JSON away — emitting plain JavaScript classes, static HTML, and extracted CSS. Rules A document must be valid JSON — no comments, no trailing commas, no inline code (behavior lives in state entries as strings or structured data). tagName is the only required root field; $schema and $id are recommended. $defs is tooling-only: no signals, no functions, no runtime artifacts. Jx reserves the keywords $prototype , $props , $switch , $map , $src , $export , timing , default , body , arguments , and name on top of the standard JSON Schema vocabulary. One document per file; a document nests other documents only by $ref , never by pasting their JSON inline. Related Components — custom elements and the component model Elements — the objects inside children State — the four shapes of a state entry Routing — how documents in pages/ become URLs Code editing — the Studio surface for raw document source"},{"id":"docs:framework/concepts/documents#documents","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#documents","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Documents","text":"Studio writes this format for you. Every page and component you build in Studio is saved as one of these files — open Code mode to see the raw source of whatever document you have open. A 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. The smallest complete document: { \"$schema\" : \"https://jxsuite.com/schema/v1\" , \"$id\" : \"Hello\" , \"state\" : { \"name\" : \"World\" }, \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Hello, ${state.name}!\" }] }"},{"id":"docs:framework/concepts/documents#root-fields","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#root-fields","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Root fields","text":"Every document is a JSON object with up to six top-level fields: Field Required Description $schema Recommended Schema the file is checked against — the dialect URI, or the project's generated bundle (below) $id Recommended Component identifier, used by tooling $defs Optional Pure JSON Schema type definitions — tooling only, never rendered state Optional Reactive state: values, computed entries, functions, and data sources — see State tagName Required HTML tag name for the root element children Optional Array of child elements , text nodes, and repeaters , mixed freely Only tagName is required. A document can also carry $media (named breakpoints, see Styling ) and $elements (custom-element dependencies, see Components ). 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 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 . The 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."},{"id":"docs:framework/concepts/documents#defs-type-definitions","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#defs-type-definitions","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"$defs — type definitions","text":"$defs holds pure JSON Schema 2020-12 type definitions. They exist for tooling — validation, autocomplete, Studio's input controls — and produce nothing at runtime: { \"$defs\" : { \"Count\" : { \"type\" : \"integer\" , \"minimum\" : 0 , \"maximum\" : 100 }, \"Status\" : { \"type\" : \"string\" , \"enum\" : [ \"idle\" , \"loading\" , \"success\" , \"error\" ] } } } State entries reference these types with $ref ( { \"$ref\": \"#/$defs/Count\" } ). $defs entries never contain default , $prototype , or template strings — runtime values belong in state ."},{"id":"docs:framework/concepts/documents#component-or-page-fragment","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#component-or-page-fragment","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Component or page fragment","text":"What a document is follows from its root tagName and where the file lives: A root tagName containing a hyphen ( \"my-counter\" , \"user-card\" ) defines a custom element — a reusable component that other documents instantiate by reference. 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 ) or inlined into another document via $ref (see References ). Either kind may declare state ; the difference is that a custom element gets its own isolated scope and a $props boundary (see Props and scope )."},{"id":"docs:framework/concepts/documents#why-plain-json","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#why-plain-json","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Why plain JSON","text":"Jx documents are valid JSON — not JavaScript object literals, not JSX, not a template DSL. That distinction is load-bearing: JSON serializes and deserializes without executing code. There is no this ambiguity — self-references are explicit $ref pointers. Visual builders, IDEs, validators, and bundlers understand it natively. JSON Schema tooling (validation, autocomplete, LSP) applies directly. The format also follows the rule of least power : prefer a $ref binding over a template expression, a template expression over a declarative expression , and a handler function only when logic cannot be expressed otherwise."},{"id":"docs:framework/concepts/documents#how-it-works","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#how-it-works","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"How it works","text":"Jx 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 compiles the JSON away — emitting plain JavaScript classes, static HTML, and extracted CSS."},{"id":"docs:framework/concepts/documents#rules","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#rules","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Rules","text":"A document must be valid JSON — no comments, no trailing commas, no inline code (behavior lives in state entries as strings or structured data). tagName is the only required root field; $schema and $id are recommended. $defs is tooling-only: no signals, no functions, no runtime artifacts. Jx reserves the keywords $prototype , $props , $switch , $map , $src , $export , timing , default , body , arguments , and name on top of the standard JSON Schema vocabulary. One document per file; a document nests other documents only by $ref , never by pasting their JSON inline."},{"id":"docs:framework/concepts/documents#related","collection":"docs","slug":"framework/concepts/documents","url":"/docs/framework/concepts/documents/#related","title":"Documents","description":"The root structure of a Jx file — $schema, $id, $defs, state, tagName, children — and why the format is plain JSON.","heading":"Related","text":"Components — custom elements and the component model Elements — the objects inside children State — the four shapes of a state entry Routing — how documents in pages/ become URLs Code editing — the Studio surface for raw document source"},{"id":"docs:framework/concepts/elements","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"","text":"Elements Studio writes this format for you. Every element you place on the canvas and edit in the Properties panel is stored as one of these objects — this page documents what lands in the file. An 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. { \"tagName\" : \"div\" , \"id\" : \"my-element\" , \"className\" : \"container active\" , \"hidden\" : false , \"tabIndex\" : 0 , \"textContent\" : \"Hello World\" } DOM properties Property 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 ), or be an object with $ref bound to state (see References ): { \"tagName\" : \"div\" , \"textContent\" : \"${state.count} items remaining\" , \"hidden\" : \"${state.items.length === 0}\" } Two properties are protected : id and tagName identify the element and may not be set via $ref bindings. Custom attributes Anything that is not a standard DOM property — data-* , aria-* , slot — goes in the attributes object, written exactly as it appears in HTML: { \"tagName\" : \"div\" , \"attributes\" : { \"data-component\" : \"my-widget\" , \"aria-label\" : \"Interactive counter\" , \"slot\" : \"header\" } } Attribute values may also be reactive templates ( \"aria-label\": \"${state.count} unread messages\" ). Children children is an array of element definitions, rendered in order: { \"tagName\" : \"div\" , \"children\" : [ { \"tagName\" : \"h1\" , \"textContent\" : \"Title\" }, { \"tagName\" : \"p\" , \"textContent\" : \"Content\" } ] } A children array may freely mix element objects, bare text nodes, and repeaters ( $prototype: \"Array\" members) or switches . Text nodes Bare 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: { \"tagName\" : \"p\" , \"children\" : [ \"Hello \" , { \"tagName\" : \"strong\" , \"textContent\" : \"world\" }, \"!\" ] } That is the HTML <p>Hello <strong>world</strong>!</p> . Template strings in text nodes are reactive: { \"children\": [\"Welcome, ${state.name}!\"] } . Slots Custom 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 : { \"tagName\" : \"card-component\" , \"children\" : [ { \"tagName\" : \"header\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"header\" } }] }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } A slot's own children act as fallback content, kept when the instance provides nothing for it. Annotations Any element may carry $title and $description — developer-facing labels that never reach the DOM: { \"tagName\" : \"section\" , \"$title\" : \"Hero Section\" , \"$description\" : \"Primary landing area with headline and call-to-action\" , \"children\" : [] } Studio's Layers panel shows $title as the element's display name. How it works The 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 . Rules tagName is required on every element definition. id and tagName are protected — never settable via $ref . Standard DOM properties go at the top level; everything else ( data-* , aria-* , slot ) goes in attributes . Bare strings and numbers in children become text nodes; when all children are bare strings with no element siblings, prefer textContent instead. $title and $description are plain strings — not reactive, not $ref -resolvable, never applied to the DOM. Styling does not use DOM properties — the style object has its own grammar, covered in Styling . Related Documents — the file these objects live in Reactivity — ${} templates in any string property Styling — the style object, nesting, and breakpoints Lists and iteration — repeaters inside children Properties panel — the Studio surface that edits these objects"},{"id":"docs:framework/concepts/elements#elements","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#elements","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Elements","text":"Studio writes this format for you. Every element you place on the canvas and edit in the Properties panel is stored as one of these objects — this page documents what lands in the file. An 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. { \"tagName\" : \"div\" , \"id\" : \"my-element\" , \"className\" : \"container active\" , \"hidden\" : false , \"tabIndex\" : 0 , \"textContent\" : \"Hello World\" }"},{"id":"docs:framework/concepts/elements#dom-properties","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#dom-properties","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"DOM properties","text":"Property 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 ), or be an object with $ref bound to state (see References ): { \"tagName\" : \"div\" , \"textContent\" : \"${state.count} items remaining\" , \"hidden\" : \"${state.items.length === 0}\" } Two properties are protected : id and tagName identify the element and may not be set via $ref bindings."},{"id":"docs:framework/concepts/elements#custom-attributes","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#custom-attributes","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Custom attributes","text":"Anything that is not a standard DOM property — data-* , aria-* , slot — goes in the attributes object, written exactly as it appears in HTML: { \"tagName\" : \"div\" , \"attributes\" : { \"data-component\" : \"my-widget\" , \"aria-label\" : \"Interactive counter\" , \"slot\" : \"header\" } } Attribute values may also be reactive templates ( \"aria-label\": \"${state.count} unread messages\" )."},{"id":"docs:framework/concepts/elements#children","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#children","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Children","text":"children is an array of element definitions, rendered in order: { \"tagName\" : \"div\" , \"children\" : [ { \"tagName\" : \"h1\" , \"textContent\" : \"Title\" }, { \"tagName\" : \"p\" , \"textContent\" : \"Content\" } ] } A children array may freely mix element objects, bare text nodes, and repeaters ( $prototype: \"Array\" members) or switches ."},{"id":"docs:framework/concepts/elements#text-nodes","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#text-nodes","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Text nodes","text":"Bare 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: { \"tagName\" : \"p\" , \"children\" : [ \"Hello \" , { \"tagName\" : \"strong\" , \"textContent\" : \"world\" }, \"!\" ] } That is the HTML <p>Hello <strong>world</strong>!</p> . Template strings in text nodes are reactive: { \"children\": [\"Welcome, ${state.name}!\"] } ."},{"id":"docs:framework/concepts/elements#slots","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#slots","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Slots","text":"Custom 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 : { \"tagName\" : \"card-component\" , \"children\" : [ { \"tagName\" : \"header\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"header\" } }] }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } A slot's own children act as fallback content, kept when the instance provides nothing for it."},{"id":"docs:framework/concepts/elements#annotations","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#annotations","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Annotations","text":"Any element may carry $title and $description — developer-facing labels that never reach the DOM: { \"tagName\" : \"section\" , \"$title\" : \"Hero Section\" , \"$description\" : \"Primary landing area with headline and call-to-action\" , \"children\" : [] } Studio's Layers panel shows $title as the element's display name."},{"id":"docs:framework/concepts/elements#how-it-works","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#how-it-works","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"How it works","text":"The 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 ."},{"id":"docs:framework/concepts/elements#rules","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#rules","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Rules","text":"tagName is required on every element definition. id and tagName are protected — never settable via $ref . Standard DOM properties go at the top level; everything else ( data-* , aria-* , slot ) goes in attributes . Bare strings and numbers in children become text nodes; when all children are bare strings with no element siblings, prefer textContent instead. $title and $description are plain strings — not reactive, not $ref -resolvable, never applied to the DOM. Styling does not use DOM properties — the style object has its own grammar, covered in Styling ."},{"id":"docs:framework/concepts/elements#related","collection":"docs","slug":"framework/concepts/elements","url":"/docs/framework/concepts/elements/#related","title":"Elements","description":"How element objects map to the DOM: direct property names, the attributes object, children arrays, text nodes, and protected properties.","heading":"Related","text":"Documents — the file these objects live in Reactivity — ${} templates in any string property Styling — the style object, nesting, and breakpoints Lists and iteration — repeaters inside children Properties panel — the Studio surface that edits these objects"},{"id":"docs:framework/concepts/expressions","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"","text":"Expressions Studio writes this format for you. The fx mode on value fields and the formula editor ( Formulas and expressions ) 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. An $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. The smallest complete expression — a dark-mode toggle handler: { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/darkMode\" }, \"value\" : { \"operator\" : \"!\" , \"target\" : { \"$ref\" : \"#/state/darkMode\" } } } } Expressions sit on the middle rung of the escalation ladder. Prefer the least powerful form that does the job: Rung Power Use when $ref binding Lowest Reading a state value ${} template Low Single-use computed read $expression Mid Declarative computation or state mutation $prototype: \"Function\" Highest Logic not expressible as structure Expression nodes Every node has the same three fields: Field Required Description operator Yes A token from the blessed set (see below) target Yes The operand the operator acts on — a $ref , a literal, or a nested node value By operator arity The right-hand operand — a $ref , a literal, an array, or a nested node Nodes 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: { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/counter\" }, \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"#/state/counter\" }, \"value\" : 1 } } } Operands resolve exactly like any $ref — state paths are always explicit JSON Pointers ( #/state/counter ), never raw state.counter strings, and $map/ paths resolve through iteration context as elsewhere. Pure and mutating operators Every operator is one of two modes, and the mode is never declared — it follows from the operator: 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. 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. { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : { \"$ref\" : \"$map/item\" } } { \"operator\" : \"splice\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : [{ \"$ref\" : \"$map/index\" }, 1 ] } 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 — anything outside it is a compile-time error, and logic that needs more escalates to a function body . Aggregates 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 use: Reference Bound during aggregation { \"$ref\": \"$map/item\" } The current array element { \"$ref\": \"$map/index\" } The current zero-based index { \"$ref\": \"$reduce/acc\" } The accumulator ( reduce only) A cart total — acc + item.price * item.qty , seeded by initial : { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$map/item/price\" }, \"value\" : { \"$ref\" : \"$map/item/qty\" } } } } reduce requires initial ; map and filter must not declare it. Aggregates compose by nesting — a filter node can be the target of a reduce . Conditionals ?: 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 . { \"operator\" : \"?:\" , \"target\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"#/state/cart/length\" }, \"value\" : 10 }, \"value\" : \"Cart full\" , \"initial\" : \"Keep shopping\" } switch is value-keyed selection, mirroring element-level $switch : target is the discriminant, cases maps its string form to results, default is optional. { \"operator\" : \"switch\" , \"target\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"loading\" : \"Please wait…\" , \"error\" : { \"$ref\" : \"#/state/errorMessage\" } }, \"default\" : { \"$ref\" : \"#/state/data/title\" } } For fallbacks, prefer ?? (nullish coalescing) over || when 0 , \"\" , or false are legitimate values — it substitutes only for null and undefined . Pure method operators Genuine 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 ). { \"operator\" : \"toUpperCase\" , \"target\" : { \"$ref\" : \"#/state/name\" } } { \"operator\" : \"padStart\" , \"target\" : { \"$ref\" : \"#/state/code\" }, \"value\" : [ 6 , \"0\" ] } Evaluation is null-safe: a missing receiver yields undefined rather than throwing. The full method table is in the operator reference . Named formulas and call A 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: { \"lineTotal\" : { \"parameters\" : [ { \"name\" : \"price\" , \"type\" : { \"text\" : \"number\" } }, { \"name\" : \"qty\" , \"type\" : { \"text\" : \"number\" }, \"default\" : 1 } ], \"$expression\" : { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$args/price\" }, \"value\" : { \"$ref\" : \"$args/qty\" } } } } The call operator invokes it: target is the callee pointer, value the positional argument list, ordered by the declared parameters (omitted arguments take their default ): { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/lineTotal\" }, \"value\" : [{ \"$ref\" : \"$map/item/price\" }, { \"$ref\" : \"$map/item/qty\" }] } A 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 . { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [{ \"$ref\" : \"#/state/a\" }, { \"$ref\" : \"#/state/b\" }, 0 ] } Formulas 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 . Reading the event Handlers 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: { \"tagName\" : \"input\" , \"oninput\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/name\" }, \"value\" : { \"$ref\" : \"event#/target/value\" } } } } Where expressions appear An $expression is valid in two positions: 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. Inline as an event handler value on any element, in place of a $ref to a function. Prefer the named form when reused; the inline form for single-use handlers. How it works An 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 . References inside a node resolve in a fixed order: 1. $map/ — iteration context 2. $reduce/acc — fold accumulator (reduce only) 3. $args/ — named-formula parameters 4. event#/ — handler event context 5. #/state/ — current component scope 6. parent#/ — explicitly passed props 7. window#/ — global window properties 8. document#/ — global document properties In 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. Rules The operator set is closed . An unknown operator is a compile-time error; escalate to a Function body instead. A mutating node may only appear at the handler boundary — never nested as an operand. Nested nodes take no $expression wrapper; it appears only at the entry or handler boundary. State operands are explicit $ref pointers, never raw state.x strings. reduce requires initial ; map and filter must not declare it. ?: requires target , value , and initial . switch matches on the discriminant's string form, because JSON keys are strings. event#/ resolves only in handler position — referencing it from a non-handler entry is a compile-time error. call targets a named formula or a blessed global; call depth is capped at 64, and statically detectable call cycles are rejected. Related Operator reference — every blessed token Formula catalog — composite formulas in Studio's palette Statements — multi-step bodies built from expression nodes Functions and sidecars — the escape hatch to real JavaScript Reactivity — how computed values track their inputs Formulas and expressions in Studio — the visual editor for all of this"},{"id":"docs:framework/concepts/expressions#expressions","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#expressions","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Expressions","text":"Studio writes this format for you. The fx mode on value fields and the formula editor ( Formulas and expressions ) 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. An $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. The smallest complete expression — a dark-mode toggle handler: { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/darkMode\" }, \"value\" : { \"operator\" : \"!\" , \"target\" : { \"$ref\" : \"#/state/darkMode\" } } } } Expressions sit on the middle rung of the escalation ladder. Prefer the least powerful form that does the job: Rung Power Use when $ref binding Lowest Reading a state value ${} template Low Single-use computed read $expression Mid Declarative computation or state mutation $prototype: \"Function\" Highest Logic not expressible as structure"},{"id":"docs:framework/concepts/expressions#expression-nodes","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#expression-nodes","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Expression nodes","text":"Every node has the same three fields: Field Required Description operator Yes A token from the blessed set (see below) target Yes The operand the operator acts on — a $ref , a literal, or a nested node value By operator arity The right-hand operand — a $ref , a literal, an array, or a nested node Nodes 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: { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/counter\" }, \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"#/state/counter\" }, \"value\" : 1 } } } Operands resolve exactly like any $ref — state paths are always explicit JSON Pointers ( #/state/counter ), never raw state.counter strings, and $map/ paths resolve through iteration context as elsewhere."},{"id":"docs:framework/concepts/expressions#pure-and-mutating-operators","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#pure-and-mutating-operators","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Pure and mutating operators","text":"Every operator is one of two modes, and the mode is never declared — it follows from the operator: 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. 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. { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : { \"$ref\" : \"$map/item\" } } { \"operator\" : \"splice\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : [{ \"$ref\" : \"$map/index\" }, 1 ] } 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 — anything outside it is a compile-time error, and logic that needs more escalates to a function body ."},{"id":"docs:framework/concepts/expressions#aggregates","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#aggregates","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Aggregates","text":"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 use: Reference Bound during aggregation { \"$ref\": \"$map/item\" } The current array element { \"$ref\": \"$map/index\" } The current zero-based index { \"$ref\": \"$reduce/acc\" } The accumulator ( reduce only) A cart total — acc + item.price * item.qty , seeded by initial : { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$map/item/price\" }, \"value\" : { \"$ref\" : \"$map/item/qty\" } } } } reduce requires initial ; map and filter must not declare it. Aggregates compose by nesting — a filter node can be the target of a reduce ."},{"id":"docs:framework/concepts/expressions#conditionals","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#conditionals","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Conditionals","text":"?: 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 . { \"operator\" : \"?:\" , \"target\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"#/state/cart/length\" }, \"value\" : 10 }, \"value\" : \"Cart full\" , \"initial\" : \"Keep shopping\" } switch is value-keyed selection, mirroring element-level $switch : target is the discriminant, cases maps its string form to results, default is optional. { \"operator\" : \"switch\" , \"target\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"loading\" : \"Please wait…\" , \"error\" : { \"$ref\" : \"#/state/errorMessage\" } }, \"default\" : { \"$ref\" : \"#/state/data/title\" } } For fallbacks, prefer ?? (nullish coalescing) over || when 0 , \"\" , or false are legitimate values — it substitutes only for null and undefined ."},{"id":"docs:framework/concepts/expressions#pure-method-operators","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#pure-method-operators","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Pure method operators","text":"Genuine 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 ). { \"operator\" : \"toUpperCase\" , \"target\" : { \"$ref\" : \"#/state/name\" } } { \"operator\" : \"padStart\" , \"target\" : { \"$ref\" : \"#/state/code\" }, \"value\" : [ 6 , \"0\" ] } Evaluation is null-safe: a missing receiver yields undefined rather than throwing. The full method table is in the operator reference ."},{"id":"docs:framework/concepts/expressions#named-formulas-and-call","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#named-formulas-and-call","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Named formulas and call","text":"A 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: { \"lineTotal\" : { \"parameters\" : [ { \"name\" : \"price\" , \"type\" : { \"text\" : \"number\" } }, { \"name\" : \"qty\" , \"type\" : { \"text\" : \"number\" }, \"default\" : 1 } ], \"$expression\" : { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$args/price\" }, \"value\" : { \"$ref\" : \"$args/qty\" } } } } The call operator invokes it: target is the callee pointer, value the positional argument list, ordered by the declared parameters (omitted arguments take their default ): { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/lineTotal\" }, \"value\" : [{ \"$ref\" : \"$map/item/price\" }, { \"$ref\" : \"$map/item/qty\" }] } A 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 . { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [{ \"$ref\" : \"#/state/a\" }, { \"$ref\" : \"#/state/b\" }, 0 ] } Formulas 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 ."},{"id":"docs:framework/concepts/expressions#reading-the-event","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#reading-the-event","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Reading the event","text":"Handlers 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: { \"tagName\" : \"input\" , \"oninput\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/name\" }, \"value\" : { \"$ref\" : \"event#/target/value\" } } } }"},{"id":"docs:framework/concepts/expressions#where-expressions-appear","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#where-expressions-appear","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Where expressions appear","text":"An $expression is valid in two positions: 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. Inline as an event handler value on any element, in place of a $ref to a function. Prefer the named form when reused; the inline form for single-use handlers."},{"id":"docs:framework/concepts/expressions#how-it-works","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#how-it-works","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"How it works","text":"An 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 . References inside a node resolve in a fixed order: 1. $map/ — iteration context 2. $reduce/acc — fold accumulator (reduce only) 3. $args/ — named-formula parameters 4. event#/ — handler event context 5. #/state/ — current component scope 6. parent#/ — explicitly passed props 7. window#/ — global window properties 8. document#/ — global document properties In 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."},{"id":"docs:framework/concepts/expressions#rules","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#rules","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Rules","text":"The operator set is closed . An unknown operator is a compile-time error; escalate to a Function body instead. A mutating node may only appear at the handler boundary — never nested as an operand. Nested nodes take no $expression wrapper; it appears only at the entry or handler boundary. State operands are explicit $ref pointers, never raw state.x strings. reduce requires initial ; map and filter must not declare it. ?: requires target , value , and initial . switch matches on the discriminant's string form, because JSON keys are strings. event#/ resolves only in handler position — referencing it from a non-handler entry is a compile-time error. call targets a named formula or a blessed global; call depth is capped at 64, and statically detectable call cycles are rejected."},{"id":"docs:framework/concepts/expressions#related","collection":"docs","slug":"framework/concepts/expressions","url":"/docs/framework/concepts/expressions/#related","title":"Expressions","description":"Declarative $expression trees in Jx: the blessed operator set, aggregates, conditionals, named formulas, and where expressions may appear.","heading":"Related","text":"Operator reference — every blessed token Formula catalog — composite formulas in Studio's palette Statements — multi-step bodies built from expression nodes Functions and sidecars — the escape hatch to real JavaScript Reactivity — how computed values track their inputs Formulas and expressions in Studio — the visual editor for all of this"},{"id":"docs:framework/concepts/functions","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"","text":"Functions and sidecars Studio writes this format for you. The Monaco editor behind every function body ( Code editing ) reads and writes these entries — this page documents the JSON and the JavaScript contract around it. A 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 and statements first; reach for a function when structure runs out. The smallest complete function — an inline handler: { \"state\" : { \"count\" : 0 , \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" } } } Bind it to an event like any handler: \"onclick\": { \"$ref\": \"#/state/increment\" } . Inline handlers A body string is a raw function body. state is always in scope; arguments names any additional parameters — an event handler names event : { \"handleInput\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"event\" ], \"body\" : \"state.value = event.target.value\" } } Inline computed values A 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: { \"titleClass\" : { \"$prototype\" : \"Function\" , \"body\" : \"return state.score >= 90 ? 'gold' : 'silver'\" } } External sidecars When 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: { \"state\" : { \"increment\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./counter.js\" }, \"decrement\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./counter.js\" }, \"validateEmail\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@myorg/validators\" , \"$export\" : \"validateEmail\" } } } export function increment ( state ) { state.count ++ ; } export function decrement ( state ) { state.count = Math. max ( 0 , state.count - 1 ); } When several entries share a $src , the module is imported once and its named exports extracted; module caching is automatic. Structured bodies A 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 . State access from JavaScript Inside 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: // Read const current = state.count; // Write state.count = current + 1 ; // Mutate arrays in place — mutations are tracked state.items. push (newItem); state.items. splice ( 0 , 1 ); // Nested objects are tracked too state.user.name = \"Alice\" ; Every write triggers the bindings that read that value — see Reactivity . this is never used in Jx-managed code; all component access goes through state . Declaring the interface Optional metadata makes a function legible to tooling: Property Description arguments Parameter names as plain strings (after the implicit state ) parameters CEM-compatible parameter objects — richer alternative to arguments returns JSON Schema describing the return value emits CEM Event objects this function dispatches description Documentation string, surfaced in Studio's completions How it works At 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. Functions marked timing: \"server\" are a separate mechanism — a plain $src / $export entry with no $prototype , executed across the RPC boundary. See Timing . Rules body and $src are mutually exclusive — declaring both is a compile-time error. The first parameter is always state ; arguments / parameters name only what follows it. this is never used. All component state goes through the state proxy. $export defaults to the entry's key name; sidecar exports must be named exports. Function entries use camelCase names, like all state entries. Only $prototype: \"Function\" may point $src at a .js file — other prototypes require a .class.json (see Data prototypes ). Related Expressions — the declarative rung below functions Statements — structured bodies without JavaScript Timing — server functions and the RPC boundary Components — where state and functions are declared Code editing in Studio — the editor for bodies and sidecars"},{"id":"docs:framework/concepts/functions#functions-and-sidecars","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#functions-and-sidecars","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Functions and sidecars","text":"Studio writes this format for you. The Monaco editor behind every function body ( Code editing ) reads and writes these entries — this page documents the JSON and the JavaScript contract around it. A 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 and statements first; reach for a function when structure runs out. The smallest complete function — an inline handler: { \"state\" : { \"count\" : 0 , \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" } } } Bind it to an event like any handler: \"onclick\": { \"$ref\": \"#/state/increment\" } ."},{"id":"docs:framework/concepts/functions#inline-handlers","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#inline-handlers","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Inline handlers","text":"A body string is a raw function body. state is always in scope; arguments names any additional parameters — an event handler names event : { \"handleInput\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"event\" ], \"body\" : \"state.value = event.target.value\" } }"},{"id":"docs:framework/concepts/functions#inline-computed-values","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#inline-computed-values","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Inline computed values","text":"A 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: { \"titleClass\" : { \"$prototype\" : \"Function\" , \"body\" : \"return state.score >= 90 ? 'gold' : 'silver'\" } }"},{"id":"docs:framework/concepts/functions#external-sidecars","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#external-sidecars","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"External sidecars","text":"When 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: { \"state\" : { \"increment\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./counter.js\" }, \"decrement\" : { \"$prototype\" : \"Function\" , \"$src\" : \"./counter.js\" }, \"validateEmail\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@myorg/validators\" , \"$export\" : \"validateEmail\" } } } export function increment ( state ) { state.count ++ ; } export function decrement ( state ) { state.count = Math. max ( 0 , state.count - 1 ); } When several entries share a $src , the module is imported once and its named exports extracted; module caching is automatic."},{"id":"docs:framework/concepts/functions#structured-bodies","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#structured-bodies","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Structured bodies","text":"A 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 ."},{"id":"docs:framework/concepts/functions#state-access-from-javascript","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#state-access-from-javascript","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"State access from JavaScript","text":"Inside 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: // Read const current = state.count; // Write state.count = current + 1 ; // Mutate arrays in place — mutations are tracked state.items. push (newItem); state.items. splice ( 0 , 1 ); // Nested objects are tracked too state.user.name = \"Alice\" ; Every write triggers the bindings that read that value — see Reactivity . this is never used in Jx-managed code; all component access goes through state ."},{"id":"docs:framework/concepts/functions#declaring-the-interface","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#declaring-the-interface","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Declaring the interface","text":"Optional metadata makes a function legible to tooling: Property Description arguments Parameter names as plain strings (after the implicit state ) parameters CEM-compatible parameter objects — richer alternative to arguments returns JSON Schema describing the return value emits CEM Event objects this function dispatches description Documentation string, surfaced in Studio's completions"},{"id":"docs:framework/concepts/functions#how-it-works","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#how-it-works","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"How it works","text":"At 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. Functions marked timing: \"server\" are a separate mechanism — a plain $src / $export entry with no $prototype , executed across the RPC boundary. See Timing ."},{"id":"docs:framework/concepts/functions#rules","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#rules","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Rules","text":"body and $src are mutually exclusive — declaring both is a compile-time error. The first parameter is always state ; arguments / parameters name only what follows it. this is never used. All component state goes through the state proxy. $export defaults to the entry's key name; sidecar exports must be named exports. Function entries use camelCase names, like all state entries. Only $prototype: \"Function\" may point $src at a .js file — other prototypes require a .class.json (see Data prototypes )."},{"id":"docs:framework/concepts/functions#related","collection":"docs","slug":"framework/concepts/functions","url":"/docs/framework/concepts/functions/#related","title":"Functions and sidecars","description":"Function prototype entries in Jx: inline handlers, computed bodies, external .js sidecars, and how JavaScript reads and writes state.","heading":"Related","text":"Expressions — the declarative rung below functions Statements — structured bodies without JavaScript Timing — server functions and the RPC boundary Components — where state and functions are declared Code editing in Studio — the editor for bodies and sidecars"},{"id":"docs:framework/concepts/lists","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"","text":"Lists and iteration Studio writes this format for you. Converting an element with Repeat… and binding its data source ( Repeaters ) writes the Array pseudo-elements on this page. A 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. { \"tagName\" : \"ul\" , \"children\" : [ { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todoList\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : { \"$ref\" : \"$map/item\" } } } ] } The iteration context Inside the map template, the $map/ reference scheme reads the current iteration: Reference Resolves to { \"$ref\": \"$map/item\" } The current array item { \"$ref\": \"$map/index\" } The current zero-based integer index Deeper 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} : { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"h2\" , \"textContent\" : { \"$ref\" : \"$map/item/title\" } }] } } The 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"a\" , \"id\" : \"post-${index}\" , \"attributes\" : { \"href\" : \"${item.url}\" , \"class\" : \"row ${index === state.active ? 'is-active' : ''}\" }, \"textContent\" : \"${item.title}\" } } Mixing with sibling elements The 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. { \"tagName\" : \"ul\" , \"children\" : [ { \"tagName\" : \"li\" , \"textContent\" : \"Header\" }, { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todoList\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : { \"$ref\" : \"$map/item\" } } } ] } Filtering and sorting filter and sort reference 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/allItems\" }, \"filter\" : { \"$ref\" : \"#/state/isVisible\" }, \"sort\" : { \"$ref\" : \"#/state/sortByDate\" }, \"map\" : { \"tagName\" : \"list-item\" , \"item\" : { \"$ref\" : \"$map/item\" } } } Filtering and sorting never mutate the source array — they shape what renders. How it works The 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. Rules The Array object must have $prototype: \"Array\" , an items source, and a map template. items must resolve to an array — a state entry, a data prototype such as a content collection, or a literal array. The list renders wrapper-less; give structure a container by making the parent the container element ( ul , tbody , a grid div ). $map/ references are valid only inside the map template. filter and sort must be $ref s to functions in state . The legacy form where children is itself the Array object is still accepted; Studio normalizes it to a single array member on load. Related References — the $map/ scheme and resolution order State — declaring the arrays lists iterate Data prototypes — collections and requests as items sources Content collections — site content as list data Repeaters — the Studio surface that writes this format"},{"id":"docs:framework/concepts/lists#lists-and-iteration","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#lists-and-iteration","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"Lists and iteration","text":"Studio writes this format for you. Converting an element with Repeat… and binding its data source ( Repeaters ) writes the Array pseudo-elements on this page. A 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. { \"tagName\" : \"ul\" , \"children\" : [ { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todoList\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : { \"$ref\" : \"$map/item\" } } } ] }"},{"id":"docs:framework/concepts/lists#the-iteration-context","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#the-iteration-context","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"The iteration context","text":"Inside the map template, the $map/ reference scheme reads the current iteration: Reference Resolves to { \"$ref\": \"$map/item\" } The current array item { \"$ref\": \"$map/index\" } The current zero-based integer index Deeper 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} : { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"h2\" , \"textContent\" : { \"$ref\" : \"$map/item/title\" } }] } } The 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"a\" , \"id\" : \"post-${index}\" , \"attributes\" : { \"href\" : \"${item.url}\" , \"class\" : \"row ${index === state.active ? 'is-active' : ''}\" }, \"textContent\" : \"${item.title}\" } }"},{"id":"docs:framework/concepts/lists#mixing-with-sibling-elements","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#mixing-with-sibling-elements","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"Mixing with sibling elements","text":"The 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. { \"tagName\" : \"ul\" , \"children\" : [ { \"tagName\" : \"li\" , \"textContent\" : \"Header\" }, { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/todoList\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : { \"$ref\" : \"$map/item\" } } } ] }"},{"id":"docs:framework/concepts/lists#filtering-and-sorting","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#filtering-and-sorting","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"Filtering and sorting","text":"filter and sort reference 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"#/state/allItems\" }, \"filter\" : { \"$ref\" : \"#/state/isVisible\" }, \"sort\" : { \"$ref\" : \"#/state/sortByDate\" }, \"map\" : { \"tagName\" : \"list-item\" , \"item\" : { \"$ref\" : \"$map/item\" } } } Filtering and sorting never mutate the source array — they shape what renders."},{"id":"docs:framework/concepts/lists#how-it-works","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#how-it-works","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"How it works","text":"The 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."},{"id":"docs:framework/concepts/lists#rules","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#rules","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"Rules","text":"The Array object must have $prototype: \"Array\" , an items source, and a map template. items must resolve to an array — a state entry, a data prototype such as a content collection, or a literal array. The list renders wrapper-less; give structure a container by making the parent the container element ( ul , tbody , a grid div ). $map/ references are valid only inside the map template. filter and sort must be $ref s to functions in state . The legacy form where children is itself the Array object is still accepted; Studio normalizes it to a single array member on load."},{"id":"docs:framework/concepts/lists#related","collection":"docs","slug":"framework/concepts/lists","url":"/docs/framework/concepts/lists/#related","title":"Lists and iteration","description":"Repeating elements from data in Jx: the Array pseudo-element, its items and map, the $map iteration context, and filtering and sorting.","heading":"Related","text":"References — the $map/ scheme and resolution order State — declaring the arrays lists iterate Data prototypes — collections and requests as items sources Content collections — site content as list data Repeaters — the Studio surface that writes this format"},{"id":"docs:framework/concepts/props-and-scope","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"","text":"Props and scope Studio writes this format for you. Editing an instance's props in the Properties panel — see Working with components — writes the $props objects on this page. Scope 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. { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Static string\" , \"count\" : { \"$ref\" : \"#/state/count\" } } } Component instances A 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: { \"$elements\" : { \"my-counter\" : { \"$ref\" : \"./components/my-counter.json\" }, \"my-card\" : { \"$ref\" : \"./components/card.json\" } }, \"children\" : [ { \"tagName\" : \"my-counter\" }, { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Hello\" , \"count\" : { \"$ref\" : \"#/state/count\" } } } ] } A 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. Static and bound props A 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: { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Static string\" , \"count\" : { \"$ref\" : \"#/state/count\" }, \"onAction\" : { \"$ref\" : \"#/state/handleAction\" } } } Inside card.json , title and count behave like the component's own state entries — typically declared there with defaults, overridden per instance. Signal forwarding When 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. Scope levels Level Scope Mirrors window Application-wide window global document Document-wide document object Component Custom element boundary CSS custom property scope Globals are reachable from any component via the window#/ and document#/ reference schemes ; everything else is bounded at the component. Resolution order When a name is looked up, scopes are consulted in order: $map/ context — the enclosing repeater iteration Local component state Explicitly passed $props window globals document globals How it works Each 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. Rules $props is the only mechanism for passing state across a component boundary — scope never leaks implicitly. Within a single component, all state entries are available to all descendant elements without passing. Signal scope is bounded at the component (custom element) level, like a CSS custom property. Private # -prefixed state entries can never be set via $props . A $ref prop is live in both directions; a plain value is per-instance and static. Every external dependency must appear in $props — that is what makes documents statically analyzable. Related Components — the component model and custom elements State — declaring the entries props override References — parent#/ , window#/ , document#/ schemes Dynamic switching — swapping whole components on state Working with components — the Studio props workflow"},{"id":"docs:framework/concepts/props-and-scope#props-and-scope","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#props-and-scope","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Props and scope","text":"Studio writes this format for you. Editing an instance's props in the Properties panel — see Working with components — writes the $props objects on this page. Scope 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. { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Static string\" , \"count\" : { \"$ref\" : \"#/state/count\" } } }"},{"id":"docs:framework/concepts/props-and-scope#component-instances","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#component-instances","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Component instances","text":"A 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: { \"$elements\" : { \"my-counter\" : { \"$ref\" : \"./components/my-counter.json\" }, \"my-card\" : { \"$ref\" : \"./components/card.json\" } }, \"children\" : [ { \"tagName\" : \"my-counter\" }, { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Hello\" , \"count\" : { \"$ref\" : \"#/state/count\" } } } ] } A 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."},{"id":"docs:framework/concepts/props-and-scope#static-and-bound-props","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#static-and-bound-props","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Static and bound props","text":"A 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: { \"tagName\" : \"my-card\" , \"$props\" : { \"title\" : \"Static string\" , \"count\" : { \"$ref\" : \"#/state/count\" }, \"onAction\" : { \"$ref\" : \"#/state/handleAction\" } } } Inside card.json , title and count behave like the component's own state entries — typically declared there with defaults, overridden per instance."},{"id":"docs:framework/concepts/props-and-scope#signal-forwarding","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#signal-forwarding","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Signal forwarding","text":"When 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."},{"id":"docs:framework/concepts/props-and-scope#scope-levels","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#scope-levels","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Scope levels","text":"Level Scope Mirrors window Application-wide window global document Document-wide document object Component Custom element boundary CSS custom property scope Globals are reachable from any component via the window#/ and document#/ reference schemes ; everything else is bounded at the component."},{"id":"docs:framework/concepts/props-and-scope#resolution-order","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#resolution-order","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Resolution order","text":"When a name is looked up, scopes are consulted in order: $map/ context — the enclosing repeater iteration Local component state Explicitly passed $props window globals document globals"},{"id":"docs:framework/concepts/props-and-scope#how-it-works","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#how-it-works","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"How it works","text":"Each 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."},{"id":"docs:framework/concepts/props-and-scope#rules","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#rules","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Rules","text":"$props is the only mechanism for passing state across a component boundary — scope never leaks implicitly. Within a single component, all state entries are available to all descendant elements without passing. Signal scope is bounded at the component (custom element) level, like a CSS custom property. Private # -prefixed state entries can never be set via $props . A $ref prop is live in both directions; a plain value is per-instance and static. Every external dependency must appear in $props — that is what makes documents statically analyzable."},{"id":"docs:framework/concepts/props-and-scope#related","collection":"docs","slug":"framework/concepts/props-and-scope","url":"/docs/framework/concepts/props-and-scope/#related","title":"Props and scope","description":"How state crosses component boundaries in Jx: explicit $props, signal forwarding, scope isolation, and the resolution order.","heading":"Related","text":"Components — the component model and custom elements State — declaring the entries props override References — parent#/ , window#/ , document#/ schemes Dynamic switching — swapping whole components on state Working with components — the Studio props workflow"},{"id":"docs:framework/concepts/reactivity","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"","text":"Reactivity Studio writes this format for you. The State, Data, and Events panels ( Script & logic ) generate everything below — this page documents the model if you want to hand-edit or understand it. Template literal syntax ${} is valid anywhere a string value appears in the document tree. All reactivity is powered by @vue/reactivity . Reactive Element Properties { \"tagName\" : \"div\" , \"textContent\" : \"${state.count} items remaining\" , \"className\" : \"${state.active ? 'card active' : 'card'}\" , \"hidden\" : \"${state.items.length === 0}\" } Reactive Style Properties { \"tagName\" : \"div\" , \"style\" : { \"color\" : \"${state.score > 90 ? 'gold' : 'inherit'}\" , \"opacity\" : \"${state.loading ? '0.5' : '1'}\" } } Reactive Attributes { \"tagName\" : \"button\" , \"attributes\" : { \"aria-label\" : \"${state.count} unread messages\" , \"data-state\" : \"${state.status}\" } } How It Works When the compiler encounters ${} in any string-valued property, it wraps the binding in a reactive effect: watchEffect (() => { el.textContent = `${ state . count } items remaining` ; }); Dependencies are tracked automatically by Vue when state.* properties are read. $ref vs Template Strings Pattern Use when { \"$ref\": \"#/state/label\" } Binding to a named signal used in multiple places \"${state.count} items\" Inline computed binding used in exactly one place Computed State Template strings in state become computed() values: { \"state\" : { \"firstName\" : \"Jane\" , \"lastName\" : \"Doe\" , \"fullName\" : \"${state.firstName} ${state.lastName}\" } } Signal Access in JavaScript Within body strings and external .js files, read and write state directly: // Read const current = state.count; // Write state.count = current + 1 ; // Mutate array (Vue tracks mutations) state.items. push (newItem); // Mutate nested object state.user.name = \"Alice\" ; No .get() or .set() calls. No this . All component state is accessed via state . Web API Prototypes Built-in prototypes for common web APIs: $prototype Web API Description Request Fetch API Reactive URL, debounce, abort URLSearchParams URL API Computed .toString() FormData FormData API Field population LocalStorage Storage API Reactive persistence SessionStorage Storage API Session-scoped storage IndexedDB IDB API Store creation, CRUD Array — Dynamic mapped lists Timing Value When \"client\" Resolved at runtime in the browser (default) \"server\" Resolved at runtime on the server via RPC \"compiler\" Resolved at build time, baked into emitted HTML"},{"id":"docs:framework/concepts/reactivity#reactivity","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#reactivity","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Reactivity","text":"Studio writes this format for you. The State, Data, and Events panels ( Script & logic ) generate everything below — this page documents the model if you want to hand-edit or understand it. Template literal syntax ${} is valid anywhere a string value appears in the document tree. All reactivity is powered by @vue/reactivity ."},{"id":"docs:framework/concepts/reactivity#reactive-element-properties","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#reactive-element-properties","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Reactive Element Properties","text":"{ \"tagName\" : \"div\" , \"textContent\" : \"${state.count} items remaining\" , \"className\" : \"${state.active ? 'card active' : 'card'}\" , \"hidden\" : \"${state.items.length === 0}\" }"},{"id":"docs:framework/concepts/reactivity#reactive-style-properties","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#reactive-style-properties","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Reactive Style Properties","text":"{ \"tagName\" : \"div\" , \"style\" : { \"color\" : \"${state.score > 90 ? 'gold' : 'inherit'}\" , \"opacity\" : \"${state.loading ? '0.5' : '1'}\" } }"},{"id":"docs:framework/concepts/reactivity#reactive-attributes","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#reactive-attributes","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Reactive Attributes","text":"{ \"tagName\" : \"button\" , \"attributes\" : { \"aria-label\" : \"${state.count} unread messages\" , \"data-state\" : \"${state.status}\" } }"},{"id":"docs:framework/concepts/reactivity#how-it-works","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#how-it-works","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"How It Works","text":"When the compiler encounters ${} in any string-valued property, it wraps the binding in a reactive effect: watchEffect (() => { el.textContent = `${ state . count } items remaining` ; }); Dependencies are tracked automatically by Vue when state.* properties are read."},{"id":"docs:framework/concepts/reactivity#ref-vs-template-strings","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#ref-vs-template-strings","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"$ref vs Template Strings","text":"Pattern Use when { \"$ref\": \"#/state/label\" } Binding to a named signal used in multiple places \"${state.count} items\" Inline computed binding used in exactly one place"},{"id":"docs:framework/concepts/reactivity#computed-state","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#computed-state","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Computed State","text":"Template strings in state become computed() values: { \"state\" : { \"firstName\" : \"Jane\" , \"lastName\" : \"Doe\" , \"fullName\" : \"${state.firstName} ${state.lastName}\" } }"},{"id":"docs:framework/concepts/reactivity#signal-access-in-javascript","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#signal-access-in-javascript","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Signal Access in JavaScript","text":"Within body strings and external .js files, read and write state directly: // Read const current = state.count; // Write state.count = current + 1 ; // Mutate array (Vue tracks mutations) state.items. push (newItem); // Mutate nested object state.user.name = \"Alice\" ; No .get() or .set() calls. No this . All component state is accessed via state ."},{"id":"docs:framework/concepts/reactivity#web-api-prototypes","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#web-api-prototypes","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Web API Prototypes","text":"Built-in prototypes for common web APIs: $prototype Web API Description Request Fetch API Reactive URL, debounce, abort URLSearchParams URL API Computed .toString() FormData FormData API Field population LocalStorage Storage API Reactive persistence SessionStorage Storage API Session-scoped storage IndexedDB IDB API Store creation, CRUD Array — Dynamic mapped lists"},{"id":"docs:framework/concepts/reactivity#timing","collection":"docs","slug":"framework/concepts/reactivity","url":"/docs/framework/concepts/reactivity/#timing","title":"Reactivity","description":"Template strings, signals, computed values, and reactive bindings in Jx.","heading":"Timing","text":"Value When \"client\" Resolved at runtime in the browser (default) \"server\" Resolved at runtime on the server via RPC \"compiler\" Resolved at build time, baked into emitted HTML"},{"id":"docs:framework/concepts/references","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"","text":"References Studio writes this format for you. Switching any bindable field to $ref mode with its field-mode button ( Formulas and expressions ) writes the $ref objects on this page. A 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 / .) { \"tagName\" : \"p\" , \"textContent\" : { \"$ref\" : \"#/state/count\" } } Reference schemes The prefix of the $ref string selects where the lookup happens: Scheme Example Resolves to Internal state \"#/state/count\" Value or handler in the current document's state Window global \"window#/currentUser\" window.currentUser Document global \"document#/appConfig\" document.appConfig Parent scope \"parent#/sharedState\" Named value passed via $props Map context \"$map/item\" Current item in a repeater iteration Map index \"$map/index\" Current index in a repeater iteration Event context \"event#/target/value\" Property path on the handler event (expressions) External file \"./other.json\" A component document — for $switch cases and $elements registration, not a bare children node (see §13) Paths navigate nested data with further / segments: \"#/state/user/name\" , \"$map/item/title\" . Reactive bindings When 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. { \"tagName\" : \"button\" , \"textContent\" : \"+\" , \"onclick\" : { \"$ref\" : \"#/state/increment\" } } $ref or template string? Both bind reactively; they differ in intent: Pattern Use when { \"$ref\": \"#/state/label\" } Binding to a named value — referenced in multiple places \"${state.count} items\" Inline computed binding used in exactly one place Prefer ${} for single-use bindings and $ref for reused or named values (see Reactivity ). The event# scheme Inside a declarative expression used as an event handler, event#/ reads a property path off the event itself — no function body required: { \"tagName\" : \"input\" , \"attributes\" : { \"placeholder\" : \"Item name\" }, \"oninput\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/name\" }, \"value\" : { \"$ref\" : \"event#/target/value\" } } } } event# is resolvable only in handler position. Referencing it from an expression that is not invoked as a handler is a compile-time error. Resolution The leading token selects exactly one source — it is scheme dispatch, not a cascading fallback: $map/ — iteration context $reduce/acc — fold accumulator ( expression reduce only) $args/ — named-formula parameters (callable bodies only) event#/ — handler event context (handler position only) #/state/ — the current document's scope (which already includes $props , merged in place) parent#/ — resolves against that same merged scope window#/ / document#/ — the corresponding global object A #/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 .) How it works The 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 case or an $elements registration that backs a component instance — but a bare { \"$ref\": \"./x.json\" } node is not (see spec §13). Rules A reference object has exactly one meaningful key: $ref with a string value (component instances may add $props ). id and tagName are protected element properties — they can never be set via $ref . $map/ references are valid only inside a repeater's map template. event#/ is valid only in an expression used as an event handler. parent#/ resolves only names explicitly passed via $props — there is no implicit parent access. 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). Related Reactivity — ${} templates, the inline alternative Expressions — where event# and $args/ live Props and scope — parent#/ and the component boundary Lists and iteration — the $map/ context Formulas and expressions — the Studio binding menu"},{"id":"docs:framework/concepts/references#references","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#references","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"References","text":"Studio writes this format for you. Switching any bindable field to $ref mode with its field-mode button ( Formulas and expressions ) writes the $ref objects on this page. A 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 / .) { \"tagName\" : \"p\" , \"textContent\" : { \"$ref\" : \"#/state/count\" } }"},{"id":"docs:framework/concepts/references#reference-schemes","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#reference-schemes","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"Reference schemes","text":"The prefix of the $ref string selects where the lookup happens: Scheme Example Resolves to Internal state \"#/state/count\" Value or handler in the current document's state Window global \"window#/currentUser\" window.currentUser Document global \"document#/appConfig\" document.appConfig Parent scope \"parent#/sharedState\" Named value passed via $props Map context \"$map/item\" Current item in a repeater iteration Map index \"$map/index\" Current index in a repeater iteration Event context \"event#/target/value\" Property path on the handler event (expressions) External file \"./other.json\" A component document — for $switch cases and $elements registration, not a bare children node (see §13) Paths navigate nested data with further / segments: \"#/state/user/name\" , \"$map/item/title\" ."},{"id":"docs:framework/concepts/references#reactive-bindings","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#reactive-bindings","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"Reactive bindings","text":"When 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. { \"tagName\" : \"button\" , \"textContent\" : \"+\" , \"onclick\" : { \"$ref\" : \"#/state/increment\" } }"},{"id":"docs:framework/concepts/references#ref-or-template-string","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#ref-or-template-string","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"$ref or template string?","text":"Both bind reactively; they differ in intent: Pattern Use when { \"$ref\": \"#/state/label\" } Binding to a named value — referenced in multiple places \"${state.count} items\" Inline computed binding used in exactly one place Prefer ${} for single-use bindings and $ref for reused or named values (see Reactivity )."},{"id":"docs:framework/concepts/references#the-event-scheme","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#the-event-scheme","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"The event# scheme","text":"Inside a declarative expression used as an event handler, event#/ reads a property path off the event itself — no function body required: { \"tagName\" : \"input\" , \"attributes\" : { \"placeholder\" : \"Item name\" }, \"oninput\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/name\" }, \"value\" : { \"$ref\" : \"event#/target/value\" } } } } event# is resolvable only in handler position. Referencing it from an expression that is not invoked as a handler is a compile-time error."},{"id":"docs:framework/concepts/references#resolution","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#resolution","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"Resolution","text":"The leading token selects exactly one source — it is scheme dispatch, not a cascading fallback: $map/ — iteration context $reduce/acc — fold accumulator ( expression reduce only) $args/ — named-formula parameters (callable bodies only) event#/ — handler event context (handler position only) #/state/ — the current document's scope (which already includes $props , merged in place) parent#/ — resolves against that same merged scope window#/ / document#/ — the corresponding global object A #/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 .)"},{"id":"docs:framework/concepts/references#how-it-works","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#how-it-works","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"How it works","text":"The 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 case or an $elements registration that backs a component instance — but a bare { \"$ref\": \"./x.json\" } node is not (see spec §13)."},{"id":"docs:framework/concepts/references#rules","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#rules","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"Rules","text":"A reference object has exactly one meaningful key: $ref with a string value (component instances may add $props ). id and tagName are protected element properties — they can never be set via $ref . $map/ references are valid only inside a repeater's map template. event#/ is valid only in an expression used as an event handler. parent#/ resolves only names explicitly passed via $props — there is no implicit parent access. 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)."},{"id":"docs:framework/concepts/references#related","collection":"docs","slug":"framework/concepts/references","url":"/docs/framework/concepts/references/#related","title":"References","description":"$ref bindings in Jx: JSON Pointer syntax, the reference schemes, reactive bindings, the event# scheme, and resolution order.","heading":"Related","text":"Reactivity — ${} templates, the inline alternative Expressions — where event# and $args/ live Props and scope — parent#/ and the component boundary Lists and iteration — the $map/ context Formulas and expressions — the Studio binding menu"},{"id":"docs:framework/concepts/security","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/","title":"Security","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.","heading":"","text":"Security A 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. Compiled sites run no eval 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 ). Ship compiled output to production and the eval requirement disappears — that is the main reason the compiler exists. The interpreter needs 'unsafe-eval' The 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. ${} 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. Treat documents as code A 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. If you compile documents you did not author — user-submitted content merged into the tree, for example — sanitize or escape ${ sequences first. The data and session boundary A site with a database and sign-ins has a second security surface: the /_jx/* routes the generated worker serves. Nothing there trusts the browser. 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. 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. 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. 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 . A 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. The extension-author side of the same model — what a mount may and may not do — is Security and secrets . The dev server's network controls The 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 and @jxsuite/server §4.2 for the full model. Related References — $ref binding vs. ${} templates Dev server internals — the network security model Auth and secrets — sessions, roles, and where secret values live Data tables — where the permission rules are declared Security and secrets — the same boundary from an extension author's side"},{"id":"docs:framework/concepts/security#security","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#security","title":"Security","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.","heading":"Security","text":"A 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."},{"id":"docs:framework/concepts/security#compiled-sites-run-no-eval","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#compiled-sites-run-no-eval","title":"Security","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.","heading":"Compiled sites run no eval","text":"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 ). Ship compiled output to production and the eval requirement disappears — that is the main reason the compiler exists."},{"id":"docs:framework/concepts/security#the-interpreter-needs-unsafe-eval","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#the-interpreter-needs-unsafe-eval","title":"Security","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.","heading":"The interpreter needs 'unsafe-eval'","text":"The 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. ${} 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."},{"id":"docs:framework/concepts/security#treat-documents-as-code","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#treat-documents-as-code","title":"Security","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.","heading":"Treat documents as code","text":"A 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. If you compile documents you did not author — user-submitted content merged into the tree, for example — sanitize or escape ${ sequences first."},{"id":"docs:framework/concepts/security#the-data-and-session-boundary","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#the-data-and-session-boundary","title":"Security","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.","heading":"The data and session boundary","text":"A site with a database and sign-ins has a second security surface: the /_jx/* routes the generated worker serves. Nothing there trusts the browser. 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. 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. 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. 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 . A 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. The extension-author side of the same model — what a mount may and may not do — is Security and secrets ."},{"id":"docs:framework/concepts/security#the-dev-servers-network-controls","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#the-dev-servers-network-controls","title":"Security","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.","heading":"The dev server's network controls","text":"The 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 and @jxsuite/server §4.2 for the full model."},{"id":"docs:framework/concepts/security#related","collection":"docs","slug":"framework/concepts/security","url":"/docs/framework/concepts/security/#related","title":"Security","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.","heading":"Related","text":"References — $ref binding vs. ${} templates Dev server internals — the network security model Auth and secrets — sessions, roles, and where secret values live Data tables — where the permission rules are declared Security and secrets — the same boundary from an extension author's side"},{"id":"docs:framework/concepts/state","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"","text":"State Studio writes this format for you. Every entry you create in the State panel — values, computed entries, data sources, functions — is saved as one of the shapes on this page. 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. { \"tagName\" : \"my-counter\" , \"state\" : { \"count\" : 0 }, \"children\" : [{ \"tagName\" : \"span\" , \"textContent\" : \"${state.count}\" }] } Shape 1 — naked values A JSON scalar, array, or plain object with no reserved keys is a mutable reactive value, initialized to exactly what you wrote: { \"state\" : { \"count\" : 0 , \"price\" : 9.99 , \"name\" : \"World\" , \"active\" : false , \"data\" : null , \"tags\" : [], \"user\" : { \"id\" : null , \"name\" : \"\" , \"role\" : \"guest\" } } } A plain string counts as a naked value only when it contains no ${} — otherwise it is Shape 3. Shape 2 — typed values An 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 : { \"state\" : { \"count\" : { \"type\" : { \"$ref\" : \"#/$defs/Count\" }, \"default\" : 0 , \"description\" : \"Current counter value\" } } } Schema 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: { \"state\" : { \"bg\" : { \"type\" : \"string\" , \"format\" : \"image\" , \"default\" : \"\" }, \"publishDate\" : { \"type\" : \"string\" , \"format\" : \"date\" , \"default\" : \"\" }, \"accentColor\" : { \"type\" : \"string\" , \"format\" : \"color\" , \"default\" : \"#000000\" } } } Use the typed form when the value needs constraints, documentation, or a shared type; use naked values when none apply. Shape 3 — computed templates A string containing ${} is a computed value — recalculated automatically whenever the state it reads changes: { \"state\" : { \"fullName\" : \"${state.firstName} ${state.lastName}\" , \"displayTitle\" : \"${state.score >= 90 ? 'Expert' : 'Beginner'}\" , \"scoreLabel\" : \"${state.score}%\" , \"isEmpty\" : \"${state.items.length === 0}\" } } The 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 . Shape 4 — prototypes An object with $prototype declares a function or a data source. Functions use $prototype: \"Function\" with an inline body or an external $src (see Functions ); any other constructor name declares a data source (see Data prototypes ): { \"state\" : { \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" }, \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users/\" , \"method\" : \"GET\" } } } Data-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 ). Private entries A # 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 : { \"state\" : { \"count\" : 0 , \"#cache\" : {}, \"#lastFetchTime\" : null } } How it works At 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). Rules Every entry is reactive by default; there is no opt-in flag. Shape follows from inspection alone — a string with ${} is always computed, an object with default and no $prototype is always a typed value. Computed templates must be pure expressions: no statements, no assignments, no semicolons, no return . state inside a template refers only to the current document's scope — never a parent's. Entry names are camelCase ( count , firstName , handleInput ); types in $defs are PascalCase ( TodoItem ). # -prefixed entries are private: hidden from props and tooling exports. On a Function entry, body and $src are mutually exclusive. Related Reactivity — how ${} bindings track dependencies Expressions — the declarative Shape 5 Functions — Function entries, sidecars, and statements Data prototypes — Request, LocalStorage, collections State panel — the Studio surface that writes these entries"},{"id":"docs:framework/concepts/state#state","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#state","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"State","text":"Studio writes this format for you. Every entry you create in the State panel — values, computed entries, data sources, functions — is saved as one of the shapes on this page. 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. { \"tagName\" : \"my-counter\" , \"state\" : { \"count\" : 0 }, \"children\" : [{ \"tagName\" : \"span\" , \"textContent\" : \"${state.count}\" }] }"},{"id":"docs:framework/concepts/state#shape-1-naked-values","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#shape-1-naked-values","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Shape 1 — naked values","text":"A JSON scalar, array, or plain object with no reserved keys is a mutable reactive value, initialized to exactly what you wrote: { \"state\" : { \"count\" : 0 , \"price\" : 9.99 , \"name\" : \"World\" , \"active\" : false , \"data\" : null , \"tags\" : [], \"user\" : { \"id\" : null , \"name\" : \"\" , \"role\" : \"guest\" } } } A plain string counts as a naked value only when it contains no ${} — otherwise it is Shape 3."},{"id":"docs:framework/concepts/state#shape-2-typed-values","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#shape-2-typed-values","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Shape 2 — typed values","text":"An 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 : { \"state\" : { \"count\" : { \"type\" : { \"$ref\" : \"#/$defs/Count\" }, \"default\" : 0 , \"description\" : \"Current counter value\" } } } Schema 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: { \"state\" : { \"bg\" : { \"type\" : \"string\" , \"format\" : \"image\" , \"default\" : \"\" }, \"publishDate\" : { \"type\" : \"string\" , \"format\" : \"date\" , \"default\" : \"\" }, \"accentColor\" : { \"type\" : \"string\" , \"format\" : \"color\" , \"default\" : \"#000000\" } } } Use the typed form when the value needs constraints, documentation, or a shared type; use naked values when none apply."},{"id":"docs:framework/concepts/state#shape-3-computed-templates","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#shape-3-computed-templates","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Shape 3 — computed templates","text":"A string containing ${} is a computed value — recalculated automatically whenever the state it reads changes: { \"state\" : { \"fullName\" : \"${state.firstName} ${state.lastName}\" , \"displayTitle\" : \"${state.score >= 90 ? 'Expert' : 'Beginner'}\" , \"scoreLabel\" : \"${state.score}%\" , \"isEmpty\" : \"${state.items.length === 0}\" } } The 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 ."},{"id":"docs:framework/concepts/state#shape-4-prototypes","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#shape-4-prototypes","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Shape 4 — prototypes","text":"An object with $prototype declares a function or a data source. Functions use $prototype: \"Function\" with an inline body or an external $src (see Functions ); any other constructor name declares a data source (see Data prototypes ): { \"state\" : { \"increment\" : { \"$prototype\" : \"Function\" , \"body\" : \"state.count++\" }, \"userData\" : { \"$prototype\" : \"Request\" , \"url\" : \"/api/users/\" , \"method\" : \"GET\" } } } Data-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 )."},{"id":"docs:framework/concepts/state#private-entries","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#private-entries","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Private entries","text":"A # 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 : { \"state\" : { \"count\" : 0 , \"#cache\" : {}, \"#lastFetchTime\" : null } }"},{"id":"docs:framework/concepts/state#how-it-works","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#how-it-works","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"How it works","text":"At 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)."},{"id":"docs:framework/concepts/state#rules","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#rules","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Rules","text":"Every entry is reactive by default; there is no opt-in flag. Shape follows from inspection alone — a string with ${} is always computed, an object with default and no $prototype is always a typed value. Computed templates must be pure expressions: no statements, no assignments, no semicolons, no return . state inside a template refers only to the current document's scope — never a parent's. Entry names are camelCase ( count , firstName , handleInput ); types in $defs are PascalCase ( TodoItem ). # -prefixed entries are private: hidden from props and tooling exports. On a Function entry, body and $src are mutually exclusive."},{"id":"docs:framework/concepts/state#related","collection":"docs","slug":"framework/concepts/state","url":"/docs/framework/concepts/state/#related","title":"State","description":"The four shapes of a Jx state entry — naked value, typed value, computed template, and $prototype — plus naming rules and private entries.","heading":"Related","text":"Reactivity — how ${} bindings track dependencies Expressions — the declarative Shape 5 Functions — Function entries, sidecars, and statements Data prototypes — Request, LocalStorage, collections State panel — the Studio surface that writes these entries"},{"id":"docs:framework/concepts/statements","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"","text":"Statements Studio writes this format for you. The statement editor ( Statements ) builds these bodies as visual step cards — this page documents the JSON those cards write to disk. A Function entry '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. The smallest complete structured body — mutate, then notify: { \"addItem\" : { \"$prototype\" : \"Function\" , \"body\" : [ { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/items\" }, \"value\" : \"New item\" }, { \"dispatchEvent\" : \"items-changed\" } ] } } There are four statement kinds, each reusing a web-platform name: Kind Shape Source of the name Expression a bare expression node (mutation or call ) ECMAScript ExpressionStatement Branch { \"if\", \"then\", \"else\"? } JSON Schema conditional keywords Multiway { \"$switch\", \"cases\", \"default\"? } Element-level $switch , ECMA switch Dispatch { \"dispatchEvent\", \"detail\"?, \"bubbles\"?, \"composed\"? } WHATWG DOM dispatchEvent Expression statements Any mutating expression node — or a call — stands alone as a step. No wrapper key; the node appears bare in the array: { \"operator\" : \"+=\" , \"target\" : { \"$ref\" : \"#/state/count\" }, \"value\" : 1 } To capture a result, make the step an assignment whose value is a call node — there is no dedicated capture field: { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/total\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/computeTotal\" } } } Branches if holds a pure expression operand; then and else hold statement lists. else is optional: { \"if\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"#/state/cart/length\" }, \"value\" : 10 }, \"then\" : [{ \"dispatchEvent\" : \"cart-full\" }], \"else\" : [{ \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/refreshTotals\" } }] } Multiway branches $switch mirrors the element-level $switch 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: { \"$switch\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"error\" : [{ \"dispatchEvent\" : \"load-failed\" }], \"ready\" : [{ \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/visible\" }, \"value\" : true }] }, \"default\" : [] } Dispatching events dispatchEvent fires a DOM CustomEvent — the statement's other keys mirror CustomEventInit . detail may be a literal, a $ref , or an expression node: { \"dispatchEvent\" : \"cart-changed\" , \"detail\" : { \"$ref\" : \"#/state/cart\" }, \"bubbles\" : true , \"composed\" : true } The 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. Parameters A 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: { \"addToCart\" : { \"$prototype\" : \"Function\" , \"parameters\" : [ \"item\" ], \"body\" : [ { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : { \"$ref\" : \"$args/item\" } } ] } } Statements, expressions, or a body string? The escalation ladder continues inside Shape 4 — prefer the least powerful form: Form Use when A single $expression One operation — toggle, increment, push, a computed value body as a statement array Multiple steps, branching, or dispatching events — still no code body as a JavaScript string Loops, await chains, try/catch, browser APIs — anything structural statements can't express A 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. How it works One 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 tracking works unchanged. Statements execute sequentially. A step whose value is a promise is awaited before the next step runs — async/await semantics without writing await . Rules A body is either a statement array or a source string — one representation per entry. Branch tests ( if ) and $switch discriminants are pure operands: no mutations, no call to a mutating entry. $switch matches String(discriminant) against cases keys, because JSON keys are strings. Statements run in order; thenable results are awaited before the next step. There is no capture field — assign a call 's result with an = statement. dispatchEvent needs a dispatch target; outside a handler (no event ), a compiled custom element dispatches from the component instance. Declare dispatched events in emits — it is the declaration tooling reads. Related Expressions — the nodes statements are built from Functions and sidecars — the entries that own a body Switching — $switch at the element level Statements in Studio — the visual step editor Code editing in Studio — the JavaScript escape hatch"},{"id":"docs:framework/concepts/statements#statements","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#statements","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Statements","text":"Studio writes this format for you. The statement editor ( Statements ) builds these bodies as visual step cards — this page documents the JSON those cards write to disk. A Function entry '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. The smallest complete structured body — mutate, then notify: { \"addItem\" : { \"$prototype\" : \"Function\" , \"body\" : [ { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/items\" }, \"value\" : \"New item\" }, { \"dispatchEvent\" : \"items-changed\" } ] } } There are four statement kinds, each reusing a web-platform name: Kind Shape Source of the name Expression a bare expression node (mutation or call ) ECMAScript ExpressionStatement Branch { \"if\", \"then\", \"else\"? } JSON Schema conditional keywords Multiway { \"$switch\", \"cases\", \"default\"? } Element-level $switch , ECMA switch Dispatch { \"dispatchEvent\", \"detail\"?, \"bubbles\"?, \"composed\"? } WHATWG DOM dispatchEvent"},{"id":"docs:framework/concepts/statements#expression-statements","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#expression-statements","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Expression statements","text":"Any mutating expression node — or a call — stands alone as a step. No wrapper key; the node appears bare in the array: { \"operator\" : \"+=\" , \"target\" : { \"$ref\" : \"#/state/count\" }, \"value\" : 1 } To capture a result, make the step an assignment whose value is a call node — there is no dedicated capture field: { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/total\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/computeTotal\" } } }"},{"id":"docs:framework/concepts/statements#branches","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#branches","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Branches","text":"if holds a pure expression operand; then and else hold statement lists. else is optional: { \"if\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"#/state/cart/length\" }, \"value\" : 10 }, \"then\" : [{ \"dispatchEvent\" : \"cart-full\" }], \"else\" : [{ \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"#/state/refreshTotals\" } }] }"},{"id":"docs:framework/concepts/statements#multiway-branches","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#multiway-branches","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Multiway branches","text":"$switch mirrors the element-level $switch 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: { \"$switch\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"error\" : [{ \"dispatchEvent\" : \"load-failed\" }], \"ready\" : [{ \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/visible\" }, \"value\" : true }] }, \"default\" : [] }"},{"id":"docs:framework/concepts/statements#dispatching-events","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#dispatching-events","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Dispatching events","text":"dispatchEvent fires a DOM CustomEvent — the statement's other keys mirror CustomEventInit . detail may be a literal, a $ref , or an expression node: { \"dispatchEvent\" : \"cart-changed\" , \"detail\" : { \"$ref\" : \"#/state/cart\" }, \"bubbles\" : true , \"composed\" : true } The 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."},{"id":"docs:framework/concepts/statements#parameters","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#parameters","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Parameters","text":"A 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: { \"addToCart\" : { \"$prototype\" : \"Function\" , \"parameters\" : [ \"item\" ], \"body\" : [ { \"operator\" : \"push\" , \"target\" : { \"$ref\" : \"#/state/cart\" }, \"value\" : { \"$ref\" : \"$args/item\" } } ] } }"},{"id":"docs:framework/concepts/statements#statements-expressions-or-a-body-string","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#statements-expressions-or-a-body-string","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Statements, expressions, or a body string?","text":"The escalation ladder continues inside Shape 4 — prefer the least powerful form: Form Use when A single $expression One operation — toggle, increment, push, a computed value body as a statement array Multiple steps, branching, or dispatching events — still no code body as a JavaScript string Loops, await chains, try/catch, browser APIs — anything structural statements can't express A 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."},{"id":"docs:framework/concepts/statements#how-it-works","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#how-it-works","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"How it works","text":"One 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 tracking works unchanged. Statements execute sequentially. A step whose value is a promise is awaited before the next step runs — async/await semantics without writing await ."},{"id":"docs:framework/concepts/statements#rules","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#rules","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Rules","text":"A body is either a statement array or a source string — one representation per entry. Branch tests ( if ) and $switch discriminants are pure operands: no mutations, no call to a mutating entry. $switch matches String(discriminant) against cases keys, because JSON keys are strings. Statements run in order; thenable results are awaited before the next step. There is no capture field — assign a call 's result with an = statement. dispatchEvent needs a dispatch target; outside a handler (no event ), a compiled custom element dispatches from the component instance. Declare dispatched events in emits — it is the declaration tooling reads."},{"id":"docs:framework/concepts/statements#related","collection":"docs","slug":"framework/concepts/statements","url":"/docs/framework/concepts/statements/#related","title":"Statements","description":"Structured function bodies in Jx: the statement kinds, how they lower to JavaScript, and when to use statements over expressions or raw code.","heading":"Related","text":"Expressions — the nodes statements are built from Functions and sidecars — the entries that own a body Switching — $switch at the element level Statements in Studio — the visual step editor Code editing in Studio — the JavaScript escape hatch"},{"id":"docs:framework/concepts/styling","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"","text":"Styling Studio writes this format for you. The Design inspector and Stylebook produce everything below — this page documents the style model for hand-editing and reference. Jx uses JSON objects for styling with camelCase CSS property names, nested selectors, and named media breakpoints. Inline Styles The style property accepts a JSON object: { \"tagName\" : \"div\" , \"style\" : { \"backgroundColor\" : \"blue\" , \"marginTop\" : \"10px\" , \"fontSize\" : \"16px\" , \"display\" : \"flex\" } } Nested CSS Selectors Keys beginning with : , . , & , or [ are treated as nested selectors: { \"style\" : { \"backgroundColor\" : \"blue\" , \":hover\" : { \"backgroundColor\" : \"darkblue\" , \"cursor\" : \"pointer\" }, \".child\" : { \"color\" : \"white\" }, \"&.active\" : { \"outline\" : \"2px solid white\" } } } Inline properties apply directly to the element. Nested rules are emitted as a scoped <style> block using a generated data-jx attribute selector. Named Media Breakpoints Declare breakpoints at root level with $media : { \"$media\" : { \"--sm\" : \"(min-width: 640px)\" , \"--md\" : \"(min-width: 768px)\" , \"--lg\" : \"(min-width: 1024px)\" , \"--dark\" : \"(prefers-color-scheme: dark)\" } } Use @--name keys in any style object: { \"style\" : { \"fontSize\" : \"14px\" , \"@--md\" : { \"fontSize\" : \"16px\" }, \"@--dark\" : { \"color\" : \"#ccc\" }, \"@(min-width: 1280px)\" : { \"fontSize\" : \"18px\" } } } @--name references named breakpoints. @(condition) is a literal inline media query. Color-Scheme Variants A $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 for the full contract and how to build a switcher. Static Style Extraction The compiler extracts all static style definitions into a single <style> block in the document <head> , producing clean, efficient CSS output. Design Tokens with CSS Custom Properties Define tokens in your project.json style block and use them everywhere: { \"style\" : { \":root\" : { \"--color-primary\" : \"#3b82f6\" , \"--color-surface\" : \"#ffffff\" , \"--font-sans\" : \"Inter, system-ui, sans-serif\" } } } Components reference tokens with standard var() : { \"style\" : { \"color\" : \"var(--color-primary)\" , \"fontFamily\" : \"var(--font-sans)\" } } CSS custom properties cascade naturally through the DOM — every component can use them without importing anything."},{"id":"docs:framework/concepts/styling#styling","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#styling","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Styling","text":"Studio writes this format for you. The Design inspector and Stylebook produce everything below — this page documents the style model for hand-editing and reference. Jx uses JSON objects for styling with camelCase CSS property names, nested selectors, and named media breakpoints."},{"id":"docs:framework/concepts/styling#inline-styles","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#inline-styles","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Inline Styles","text":"The style property accepts a JSON object: { \"tagName\" : \"div\" , \"style\" : { \"backgroundColor\" : \"blue\" , \"marginTop\" : \"10px\" , \"fontSize\" : \"16px\" , \"display\" : \"flex\" } }"},{"id":"docs:framework/concepts/styling#nested-css-selectors","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#nested-css-selectors","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Nested CSS Selectors","text":"Keys beginning with : , . , & , or [ are treated as nested selectors: { \"style\" : { \"backgroundColor\" : \"blue\" , \":hover\" : { \"backgroundColor\" : \"darkblue\" , \"cursor\" : \"pointer\" }, \".child\" : { \"color\" : \"white\" }, \"&.active\" : { \"outline\" : \"2px solid white\" } } } Inline properties apply directly to the element. Nested rules are emitted as a scoped <style> block using a generated data-jx attribute selector."},{"id":"docs:framework/concepts/styling#named-media-breakpoints","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#named-media-breakpoints","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Named Media Breakpoints","text":"Declare breakpoints at root level with $media : { \"$media\" : { \"--sm\" : \"(min-width: 640px)\" , \"--md\" : \"(min-width: 768px)\" , \"--lg\" : \"(min-width: 1024px)\" , \"--dark\" : \"(prefers-color-scheme: dark)\" } } Use @--name keys in any style object: { \"style\" : { \"fontSize\" : \"14px\" , \"@--md\" : { \"fontSize\" : \"16px\" }, \"@--dark\" : { \"color\" : \"#ccc\" }, \"@(min-width: 1280px)\" : { \"fontSize\" : \"18px\" } } } @--name references named breakpoints. @(condition) is a literal inline media query."},{"id":"docs:framework/concepts/styling#color-scheme-variants","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#color-scheme-variants","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Color-Scheme Variants","text":"A $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 for the full contract and how to build a switcher."},{"id":"docs:framework/concepts/styling#static-style-extraction","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#static-style-extraction","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Static Style Extraction","text":"The compiler extracts all static style definitions into a single <style> block in the document <head> , producing clean, efficient CSS output."},{"id":"docs:framework/concepts/styling#design-tokens-with-css-custom-properties","collection":"docs","slug":"framework/concepts/styling","url":"/docs/framework/concepts/styling/#design-tokens-with-css-custom-properties","title":"Styling","description":"Inline styles, nested CSS selectors, and named media breakpoints in Jx.","heading":"Design Tokens with CSS Custom Properties","text":"Define tokens in your project.json style block and use them everywhere: { \"style\" : { \":root\" : { \"--color-primary\" : \"#3b82f6\" , \"--color-surface\" : \"#ffffff\" , \"--font-sans\" : \"Inter, system-ui, sans-serif\" } } } Components reference tokens with standard var() : { \"style\" : { \"color\" : \"var(--color-primary)\" , \"fontFamily\" : \"var(--font-sans)\" } } CSS custom properties cascade naturally through the DOM — every component can use them without importing anything."},{"id":"docs:framework/concepts/switching","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/","title":"Dynamic switching","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.","heading":"","text":"Dynamic switching Studio writes this format for you. There is no dedicated panel for $switch yet — add it in Code mode and the canvas renders whichever case is active. A $switch node swaps what renders in one spot based on a state value: the node binds $switch to a reference , 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. { \"tagName\" : \"main\" , \"children\" : [ { \"$switch\" : { \"$ref\" : \"#/state/currentRoute\" }, \"cases\" : { \"home\" : { \"$ref\" : \"./views/home.json\" }, \"about\" : { \"$ref\" : \"./views/about.json\" }, \"profile\" : { \"$ref\" : \"./views/profile.json\" } } } ] } External cases A case value that is a $ref to a .json file loads that document 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. Inline cases A 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: { \"$switch\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"loading\" : { \"tagName\" : \"p\" , \"textContent\" : \"Loading…\" }, \"error\" : { \"tagName\" : \"p\" , \"textContent\" : \"${state.errorMessage}\" }, \"success\" : { \"$ref\" : \"./views/results.json\" } } } Driving the switch The selector is ordinary state, so anything that writes state switches the view — an event handler, an expression , or a data source: { \"state\" : { \"currentRoute\" : \"home\" }, \"children\" : [ { \"tagName\" : \"button\" , \"textContent\" : \"About\" , \"onclick\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/currentRoute\" }, \"value\" : \"about\" } } } ] } How it works The 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. Rules $switch must be a $ref — typically #/state/... ; a literal value is meaningless and does not render. Case keys are matched against the resolved value as strings; there is no default case — an unmatched value renders an empty container. External cases have isolated scope; to share data with a case, make it an inline element, or lift shared state to window#/ globals. Switching fully tears down the outgoing case — its DOM and reactive bindings are disposed, and per-component state re-initializes on the next visit. For URL-driven pages prefer file-based routing ; $switch is for switching within a page. Related References — the $ref schemes $switch binds State — declaring the selector value Props and scope — why external cases are isolated Routing — URL-based page selection for sites Code editing — the Studio surface for authoring $switch"},{"id":"docs:framework/concepts/switching#dynamic-switching","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#dynamic-switching","title":"Dynamic switching","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.","heading":"Dynamic switching","text":"Studio writes this format for you. There is no dedicated panel for $switch yet — add it in Code mode and the canvas renders whichever case is active. A $switch node swaps what renders in one spot based on a state value: the node binds $switch to a reference , 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. { \"tagName\" : \"main\" , \"children\" : [ { \"$switch\" : { \"$ref\" : \"#/state/currentRoute\" }, \"cases\" : { \"home\" : { \"$ref\" : \"./views/home.json\" }, \"about\" : { \"$ref\" : \"./views/about.json\" }, \"profile\" : { \"$ref\" : \"./views/profile.json\" } } } ] }"},{"id":"docs:framework/concepts/switching#external-cases","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#external-cases","title":"Dynamic switching","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.","heading":"External cases","text":"A case value that is a $ref to a .json file loads that document 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."},{"id":"docs:framework/concepts/switching#inline-cases","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#inline-cases","title":"Dynamic switching","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.","heading":"Inline cases","text":"A 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: { \"$switch\" : { \"$ref\" : \"#/state/status\" }, \"cases\" : { \"loading\" : { \"tagName\" : \"p\" , \"textContent\" : \"Loading…\" }, \"error\" : { \"tagName\" : \"p\" , \"textContent\" : \"${state.errorMessage}\" }, \"success\" : { \"$ref\" : \"./views/results.json\" } } }"},{"id":"docs:framework/concepts/switching#driving-the-switch","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#driving-the-switch","title":"Dynamic switching","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.","heading":"Driving the switch","text":"The selector is ordinary state, so anything that writes state switches the view — an event handler, an expression , or a data source: { \"state\" : { \"currentRoute\" : \"home\" }, \"children\" : [ { \"tagName\" : \"button\" , \"textContent\" : \"About\" , \"onclick\" : { \"$expression\" : { \"operator\" : \"=\" , \"target\" : { \"$ref\" : \"#/state/currentRoute\" }, \"value\" : \"about\" } } } ] }"},{"id":"docs:framework/concepts/switching#how-it-works","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#how-it-works","title":"Dynamic switching","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.","heading":"How it works","text":"The 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."},{"id":"docs:framework/concepts/switching#rules","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#rules","title":"Dynamic switching","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.","heading":"Rules","text":"$switch must be a $ref — typically #/state/... ; a literal value is meaningless and does not render. Case keys are matched against the resolved value as strings; there is no default case — an unmatched value renders an empty container. External cases have isolated scope; to share data with a case, make it an inline element, or lift shared state to window#/ globals. Switching fully tears down the outgoing case — its DOM and reactive bindings are disposed, and per-component state re-initializes on the next visit. For URL-driven pages prefer file-based routing ; $switch is for switching within a page."},{"id":"docs:framework/concepts/switching#related","collection":"docs","slug":"framework/concepts/switching","url":"/docs/framework/concepts/switching/#related","title":"Dynamic switching","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.","heading":"Related","text":"References — the $ref schemes $switch binds State — declaring the selector value Props and scope — why external cases are isolated Routing — URL-based page selection for sites Code editing — the Studio surface for authoring $switch"},{"id":"docs:framework/concepts/timing","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/","title":"Timing: client, server, compiler","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.","heading":"","text":"Timing: client, server, compiler Studio writes this format for you. The Timing field on a data source's editor ( Data sources ) sets the values below — this page documents what each one means. The 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: Value When it resolves \"client\" At runtime, in the visitor's browser (the default) \"server\" At runtime, on the server, called over an RPC boundary \"compiler\" At build time — the result is baked into the emitted HTML The smallest complete server-timed entry — a function that runs on the server, whose return value lands in state: { \"state\" : { \"metrics\" : { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"timing\" : \"server\" } } } Client \"client\" is the default — omit timing and the entry resolves in the browser. All the Web-API data prototypes ( Request , LocalStorage , IndexedDB , …) are client-timed unless told otherwise. Server: the RPC function boundary 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, …): export async function fetchMetrics ( args , env ) { const db = env. DB ; // e.g. a Cloudflare D1 binding const { data } = await db. prepare ( \"SELECT * FROM metrics\" ). all (); return data; } A function that needs no bindings simply ignores the second parameter. Arguments An 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: { \"metrics\" : { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"timing\" : \"server\" , \"arguments\" : { \"userId\" : { \"$ref\" : \"#/state/userId\" }, \"filter\" : \"active\" } } } The security boundary Server 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. The 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. Compiler 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: { \"posts\" : { \"$prototype\" : \"MarkdownCollection\" , \"src\" : \"./content/posts/*.md\" , \"timing\" : \"compiler\" } } How it works For each timing: \"server\" entry, the compiler 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. When 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 stands in for both. Compiler-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. Rules \"client\" is the default whenever timing is absent. A server entry uses $src + $export with no $prototype , and the export must be an async named export. The server function's signature is (args, env) ; arguments is one named-values object. A reactive $ref in arguments makes the RPC call reactive. env and everything reachable through it stay server-side; only the return value is serialized to the browser. timing: \"compiler\" values are fixed at build time — rebuild the site to refresh them. Related Data prototypes — the entries timing applies to Functions and sidecars — client-side functions by contrast The build — where server bundling and baking happen Dev server — server timing during development Data sources in Studio — the Timing field in the editor"},{"id":"docs:framework/concepts/timing#timing-client-server-compiler","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#timing-client-server-compiler","title":"Timing: client, server, compiler","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.","heading":"Timing: client, server, compiler","text":"Studio writes this format for you. The Timing field on a data source's editor ( Data sources ) sets the values below — this page documents what each one means. The 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: Value When it resolves \"client\" At runtime, in the visitor's browser (the default) \"server\" At runtime, on the server, called over an RPC boundary \"compiler\" At build time — the result is baked into the emitted HTML The smallest complete server-timed entry — a function that runs on the server, whose return value lands in state: { \"state\" : { \"metrics\" : { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"timing\" : \"server\" } } }"},{"id":"docs:framework/concepts/timing#client","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#client","title":"Timing: client, server, compiler","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.","heading":"Client","text":"\"client\" is the default — omit timing and the entry resolves in the browser. All the Web-API data prototypes ( Request , LocalStorage , IndexedDB , …) are client-timed unless told otherwise."},{"id":"docs:framework/concepts/timing#server-the-rpc-function-boundary","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#server-the-rpc-function-boundary","title":"Timing: client, server, compiler","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.","heading":"Server: the RPC function boundary","text":"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, …): export async function fetchMetrics ( args , env ) { const db = env. DB ; // e.g. a Cloudflare D1 binding const { data } = await db. prepare ( \"SELECT * FROM metrics\" ). all (); return data; } A function that needs no bindings simply ignores the second parameter."},{"id":"docs:framework/concepts/timing#arguments","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#arguments","title":"Timing: client, server, compiler","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.","heading":"Arguments","text":"An 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: { \"metrics\" : { \"$src\" : \"./dashboard.server.js\" , \"$export\" : \"fetchMetrics\" , \"timing\" : \"server\" , \"arguments\" : { \"userId\" : { \"$ref\" : \"#/state/userId\" }, \"filter\" : \"active\" } } }"},{"id":"docs:framework/concepts/timing#the-security-boundary","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#the-security-boundary","title":"Timing: client, server, compiler","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.","heading":"The security boundary","text":"Server 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. The 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."},{"id":"docs:framework/concepts/timing#compiler","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#compiler","title":"Timing: client, server, compiler","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.","heading":"Compiler","text":"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: { \"posts\" : { \"$prototype\" : \"MarkdownCollection\" , \"src\" : \"./content/posts/*.md\" , \"timing\" : \"compiler\" } }"},{"id":"docs:framework/concepts/timing#how-it-works","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#how-it-works","title":"Timing: client, server, compiler","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.","heading":"How it works","text":"For each timing: \"server\" entry, the compiler 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. When 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 stands in for both. Compiler-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."},{"id":"docs:framework/concepts/timing#rules","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#rules","title":"Timing: client, server, compiler","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.","heading":"Rules","text":"\"client\" is the default whenever timing is absent. A server entry uses $src + $export with no $prototype , and the export must be an async named export. The server function's signature is (args, env) ; arguments is one named-values object. A reactive $ref in arguments makes the RPC call reactive. env and everything reachable through it stay server-side; only the return value is serialized to the browser. timing: \"compiler\" values are fixed at build time — rebuild the site to refresh them."},{"id":"docs:framework/concepts/timing#related","collection":"docs","slug":"framework/concepts/timing","url":"/docs/framework/concepts/timing/#related","title":"Timing: client, server, compiler","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.","heading":"Related","text":"Data prototypes — the entries timing applies to Functions and sidecars — client-side functions by contrast The build — where server bundling and baking happen Dev server — server timing during development Data sources in Studio — the Timing field in the editor"},{"id":"docs:framework/reference/formulas","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"","text":"Formula catalog These 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 . average Arithmetic mean of an array of numbers; 0 for an empty array. Parameter Type Description values number[] — { \"operator\" : \"?:\" , \"target\" : { \"$ref\" : \"$args/values/length\" }, \"value\" : { \"operator\" : \"/\" , \"target\" : { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"$ref\" : \"$map/item\" } } }, \"value\" : { \"$ref\" : \"$args/values/length\" } }, \"initial\" : 0 } capitalize Uppercase the first character of a string, leaving the rest unchanged (the CSS text-transform: capitalize shape for a single word). Parameter Type Description text string — { \"operator\" : \"+\" , \"target\" : { \"operator\" : \"toUpperCase\" , \"target\" : { \"operator\" : \"charAt\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : 0 } }, \"value\" : { \"operator\" : \"slice\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : 1 } } clamp Bound a number to the [min, max] range (the TC39 Math.clamp shape): returns min when below, max when above, the value itself otherwise. Parameter Type Description value number — min number — max number — { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/min\" }, \"value\" : [ { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [ { \"$ref\" : \"$args/value\" }, { \"$ref\" : \"$args/min\" } ] }, { \"$ref\" : \"$args/max\" } ] } compact A copy of an array with the falsy elements removed (Array.prototype.filter on truthiness). Parameter Type Description values unknown[] — { \"operator\" : \"filter\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"value\" : { \"operator\" : \"!\" , \"target\" : { \"operator\" : \"!\" , \"target\" : { \"$ref\" : \"$map/item\" } } } } count The length of an array or string, or 0 when the value is missing. Parameter Type Description values unknown[] string — { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/values/length\" }, \"value\" : 0 } first The first element of an array (index 0), or null when empty. Parameter Type Description values unknown[] — { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : null } initials The uppercase first letters of each whitespace-separated word, joined (e.g. \"ada lovelace\" → \"AL\"). Parameter Type Description name string — { \"operator\" : \"join\" , \"target\" : { \"operator\" : \"map\" , \"target\" : { \"operator\" : \"split\" , \"target\" : { \"$ref\" : \"$args/name\" }, \"value\" : \" \" }, \"value\" : { \"operator\" : \"toUpperCase\" , \"target\" : { \"operator\" : \"charAt\" , \"target\" : { \"$ref\" : \"$map/item\" }, \"value\" : 0 } } }, \"value\" : \"\" } isEmpty True when an array or string has no elements (length 0 — a missing value counts as empty). Parameter Type Description value unknown[] string — { \"operator\" : \"===\" , \"target\" : { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/value/length\" }, \"value\" : 0 }, \"value\" : 0 } last The last element of an array (Array.prototype.at(-1)), or undefined when empty. Parameter Type Description values unknown[] — { \"operator\" : \"at\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"value\" : -1 } max The largest number in an array (Math.max folded over the values); undefined for an empty array. Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [ { \"$ref\" : \"$reduce/acc\" }, { \"$ref\" : \"$map/item\" } ] } } min The smallest number in an array (Math.min folded over the values); undefined for an empty array. Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/min\" }, \"value\" : [ { \"$ref\" : \"$reduce/acc\" }, { \"$ref\" : \"$map/item\" } ] } } percent part / whole as a percentage (0 when the whole is 0 or missing). Parameter Type Description part number — whole number — { \"operator\" : \"?:\" , \"target\" : { \"$ref\" : \"$args/whole\" }, \"value\" : { \"operator\" : \"*\" , \"target\" : { \"operator\" : \"/\" , \"target\" : { \"$ref\" : \"$args/part\" }, \"value\" : { \"$ref\" : \"$args/whole\" } }, \"value\" : 100 }, \"initial\" : 0 } roundTo Round a number to the given decimal places (Math.round against a Math.pow(10, digits) scale). Parameter Type Description value number — digits number — { \"operator\" : \"/\" , \"target\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/round\" }, \"value\" : [ { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$args/value\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/pow\" }, \"value\" : [ 10 , { \"$ref\" : \"$args/digits\" } ] } } ] }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/pow\" }, \"value\" : [ 10 , { \"$ref\" : \"$args/digits\" } ] } } sum Add every number in an array (Array.prototype.reduce with +, seeded at 0). Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"$ref\" : \"$map/item\" } } } truncate Shorten a string to at most the given length, appending a horizontal ellipsis when cut. Parameter Type Description text string — length number — { \"operator\" : \"?:\" , \"target\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"$args/text/length\" }, \"value\" : { \"$ref\" : \"$args/length\" } }, \"value\" : { \"operator\" : \"+\" , \"target\" : { \"operator\" : \"slice\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : [ 0 , { \"$ref\" : \"$args/length\" } ] }, \"value\" : \"…\" }, \"initial\" : { \"$ref\" : \"$args/text\" } }"},{"id":"docs:framework/reference/formulas#formula-catalog","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#formula-catalog","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"Formula catalog","text":"These 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 ."},{"id":"docs:framework/reference/formulas#average","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#average","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"average","text":"Arithmetic mean of an array of numbers; 0 for an empty array. Parameter Type Description values number[] — { \"operator\" : \"?:\" , \"target\" : { \"$ref\" : \"$args/values/length\" }, \"value\" : { \"operator\" : \"/\" , \"target\" : { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"$ref\" : \"$map/item\" } } }, \"value\" : { \"$ref\" : \"$args/values/length\" } }, \"initial\" : 0 }"},{"id":"docs:framework/reference/formulas#capitalize","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#capitalize","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"capitalize","text":"Uppercase the first character of a string, leaving the rest unchanged (the CSS text-transform: capitalize shape for a single word). Parameter Type Description text string — { \"operator\" : \"+\" , \"target\" : { \"operator\" : \"toUpperCase\" , \"target\" : { \"operator\" : \"charAt\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : 0 } }, \"value\" : { \"operator\" : \"slice\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : 1 } }"},{"id":"docs:framework/reference/formulas#clamp","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#clamp","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"clamp","text":"Bound a number to the [min, max] range (the TC39 Math.clamp shape): returns min when below, max when above, the value itself otherwise. Parameter Type Description value number — min number — max number — { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/min\" }, \"value\" : [ { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [ { \"$ref\" : \"$args/value\" }, { \"$ref\" : \"$args/min\" } ] }, { \"$ref\" : \"$args/max\" } ] }"},{"id":"docs:framework/reference/formulas#compact","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#compact","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"compact","text":"A copy of an array with the falsy elements removed (Array.prototype.filter on truthiness). Parameter Type Description values unknown[] — { \"operator\" : \"filter\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"value\" : { \"operator\" : \"!\" , \"target\" : { \"operator\" : \"!\" , \"target\" : { \"$ref\" : \"$map/item\" } } } }"},{"id":"docs:framework/reference/formulas#count","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#count","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"count","text":"The length of an array or string, or 0 when the value is missing. Parameter Type Description values unknown[] string — { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/values/length\" }, \"value\" : 0 }"},{"id":"docs:framework/reference/formulas#first","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#first","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"first","text":"The first element of an array (index 0), or null when empty. Parameter Type Description values unknown[] — { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : null }"},{"id":"docs:framework/reference/formulas#initials","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#initials","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"initials","text":"The uppercase first letters of each whitespace-separated word, joined (e.g. \"ada lovelace\" → \"AL\"). Parameter Type Description name string — { \"operator\" : \"join\" , \"target\" : { \"operator\" : \"map\" , \"target\" : { \"operator\" : \"split\" , \"target\" : { \"$ref\" : \"$args/name\" }, \"value\" : \" \" }, \"value\" : { \"operator\" : \"toUpperCase\" , \"target\" : { \"operator\" : \"charAt\" , \"target\" : { \"$ref\" : \"$map/item\" }, \"value\" : 0 } } }, \"value\" : \"\" }"},{"id":"docs:framework/reference/formulas#isempty","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#isempty","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"isEmpty","text":"True when an array or string has no elements (length 0 — a missing value counts as empty). Parameter Type Description value unknown[] string — { \"operator\" : \"===\" , \"target\" : { \"operator\" : \"??\" , \"target\" : { \"$ref\" : \"$args/value/length\" }, \"value\" : 0 }, \"value\" : 0 }"},{"id":"docs:framework/reference/formulas#last","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#last","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"last","text":"The last element of an array (Array.prototype.at(-1)), or undefined when empty. Parameter Type Description values unknown[] — { \"operator\" : \"at\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"value\" : -1 }"},{"id":"docs:framework/reference/formulas#max","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#max","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"max","text":"The largest number in an array (Math.max folded over the values); undefined for an empty array. Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/max\" }, \"value\" : [ { \"$ref\" : \"$reduce/acc\" }, { \"$ref\" : \"$map/item\" } ] } }"},{"id":"docs:framework/reference/formulas#min","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#min","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"min","text":"The smallest number in an array (Math.min folded over the values); undefined for an empty array. Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : { \"$ref\" : \"$args/values/0\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/min\" }, \"value\" : [ { \"$ref\" : \"$reduce/acc\" }, { \"$ref\" : \"$map/item\" } ] } }"},{"id":"docs:framework/reference/formulas#percent","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#percent","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"percent","text":"part / whole as a percentage (0 when the whole is 0 or missing). Parameter Type Description part number — whole number — { \"operator\" : \"?:\" , \"target\" : { \"$ref\" : \"$args/whole\" }, \"value\" : { \"operator\" : \"*\" , \"target\" : { \"operator\" : \"/\" , \"target\" : { \"$ref\" : \"$args/part\" }, \"value\" : { \"$ref\" : \"$args/whole\" } }, \"value\" : 100 }, \"initial\" : 0 }"},{"id":"docs:framework/reference/formulas#roundto","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#roundto","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"roundTo","text":"Round a number to the given decimal places (Math.round against a Math.pow(10, digits) scale). Parameter Type Description value number — digits number — { \"operator\" : \"/\" , \"target\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/round\" }, \"value\" : [ { \"operator\" : \"*\" , \"target\" : { \"$ref\" : \"$args/value\" }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/pow\" }, \"value\" : [ 10 , { \"$ref\" : \"$args/digits\" } ] } } ] }, \"value\" : { \"operator\" : \"call\" , \"target\" : { \"$ref\" : \"window#/Math/pow\" }, \"value\" : [ 10 , { \"$ref\" : \"$args/digits\" } ] } }"},{"id":"docs:framework/reference/formulas#sum","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#sum","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"sum","text":"Add every number in an array (Array.prototype.reduce with +, seeded at 0). Parameter Type Description values number[] — { \"operator\" : \"reduce\" , \"target\" : { \"$ref\" : \"$args/values\" }, \"initial\" : 0 , \"value\" : { \"operator\" : \"+\" , \"target\" : { \"$ref\" : \"$reduce/acc\" }, \"value\" : { \"$ref\" : \"$map/item\" } } }"},{"id":"docs:framework/reference/formulas#truncate","collection":"docs","slug":"framework/reference/formulas","url":"/docs/framework/reference/formulas/#truncate","title":"Formula catalog","description":"Every composite formula that ships with Jx — name, parameters, and the pure expression it expands to.","heading":"truncate","text":"Shorten a string to at most the given length, appending a horizontal ellipsis when cut. Parameter Type Description text string — length number — { \"operator\" : \"?:\" , \"target\" : { \"operator\" : \">\" , \"target\" : { \"$ref\" : \"$args/text/length\" }, \"value\" : { \"$ref\" : \"$args/length\" } }, \"value\" : { \"operator\" : \"+\" , \"target\" : { \"operator\" : \"slice\" , \"target\" : { \"$ref\" : \"$args/text\" }, \"value\" : [ 0 , { \"$ref\" : \"$args/length\" } ] }, \"value\" : \"…\" }, \"initial\" : { \"$ref\" : \"$args/text\" } }"},{"id":"docs:framework/reference/operators","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"","text":"Operator reference Declarative 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 section; named-formula composition is cataloged in the formula catalog . Unary operators Applied to a single target operand. ! · - Binary operators Applied 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). + · - · * · / · % · === · !== · < · <= · > · >= · && · || · ?? Assignment operators Statement-position only: assign (or read-modify-write) a state path. Not valid inside pure expressions. = · += · -= · *= · /= Conditional, switch, and call ?: 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 . ?: · switch · call Pure standard-library methods Genuine 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. 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 Mutation methods Statement-position array mutations. pop / shift take no argument; push / unshift take one; splice takes start , deleteCount , and items . pop · shift · push · unshift · splice Iteration methods 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. reduce · map · filter"},{"id":"docs:framework/reference/operators#operator-reference","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#operator-reference","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Operator reference","text":"Declarative 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 section; named-formula composition is cataloged in the formula catalog ."},{"id":"docs:framework/reference/operators#unary-operators","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#unary-operators","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Unary operators","text":"Applied to a single target operand. ! · -"},{"id":"docs:framework/reference/operators#binary-operators","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#binary-operators","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Binary operators","text":"Applied 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). + · - · * · / · % · === · !== · < · <= · > · >= · && · || · ??"},{"id":"docs:framework/reference/operators#assignment-operators","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#assignment-operators","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Assignment operators","text":"Statement-position only: assign (or read-modify-write) a state path. Not valid inside pure expressions. = · += · -= · *= · /="},{"id":"docs:framework/reference/operators#conditional-switch-and-call","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#conditional-switch-and-call","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Conditional, switch, and call","text":"?: 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 . ?: · switch · call"},{"id":"docs:framework/reference/operators#pure-standard-library-methods","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#pure-standard-library-methods","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Pure standard-library methods","text":"Genuine 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. 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"},{"id":"docs:framework/reference/operators#mutation-methods","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#mutation-methods","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Mutation methods","text":"Statement-position array mutations. pop / shift take no argument; push / unshift take one; splice takes start , deleteCount , and items . pop · shift · push · unshift · splice"},{"id":"docs:framework/reference/operators#iteration-methods","collection":"docs","slug":"framework/reference/operators","url":"/docs/framework/reference/operators/#iteration-methods","title":"Operator reference","description":"The blessed operator set for declarative $expression trees — every operator and method token the schema admits.","heading":"Iteration methods","text":"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. reduce · map · filter"},{"id":"docs:framework/site/content-collections","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"","text":"Content collections Studio writes this format for you — the content-type builder edits the content section visually, and entries show up in Browse your project with schema-driven forms. Content 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 . Formats other than JSON come from extension packages, so a site using Markdown or CSV content enables the parser in project.json : \"extensions\": [\"@jxsuite/parser\"] . Defining a content type { \"content\" : { \"blog\" : { \"source\" : \"./content/blog/\" , \"format\" : \"Markdown\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" }, \"pubDate\" : { \"type\" : \"string\" } }, \"required\" : [ \"title\" , \"pubDate\" ] } } } } Each definition takes four keys: 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. 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 ). $elements — components available inside entries as directives, e.g. jxsuite.com registers doc-note and doc-tip for these docs. See Jx Markdown . schema — a JSON Schema every entry's data must satisfy. Entry ids Every entry has an id , derived from the filesystem: Flat directories — the filename without extension: content/blog/hello-world.md → hello-world . 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 . 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. Media beside your content Entries reference images the way any markdown editor expects — relative to the file itself: ![ A diagram of the pipeline ]( ./images/diagram.png ) Keep 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/ . Only files an entry actually references are published; unreferenced siblings and the entry files themselves never reach dist/ . Remapping 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 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. Querying with ContentCollection Pages read a collection through a state entry with $prototype: \"ContentCollection\" : { \"state\" : { \"posts\" : { \"$prototype\" : \"ContentCollection\" , \"contentType\" : \"blog\" , \"filter\" : { \"draft\" : false }, \"sort\" : { \"field\" : \"pubDate\" , \"order\" : \"desc\" }, \"limit\" : 10 } } } 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. sort — a rule or array of rules, { \"field\": \"pubDate\", \"order\": \"desc\" } ; asc is the default. limit — maximum number of entries. The result is an array of entries, rendered with a repeater : { \"tagName\" : \"ul\" , \"children\" : { \"$prototype\" : \"Array\" , \"of\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : \"${item.data.title}\" } } } Single entries with ContentEntry $prototype: \"ContentEntry\" fetches one entry by id — usually bound to a route parameter: { \"state\" : { \"post\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"blog\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } } } By 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). A resolved entry has this shape: { \"id\" : \"hello-world\" , \"data\" : { \"title\" : \"Hello World\" , \"pubDate\" : \"2024-01-15\" }, \"body\" : \"# Hello \\n\\n This is my first post.\" , \"$children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Hello\" }] } Frontmatter 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 ?? []}\" . Markdown 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. Fenced 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 . See Jx Markdown for the language set. Schema validation Every 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 and the content-type builder's field editor. Relationships A schema field can reference another content type: { \"author\" : { \"$ref\" : \"#/content/authors\" } } An 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 . Related Routing — generating one page per entry with $paths Jx Markdown — the entry format for prose content project.json — where the content section lives"},{"id":"docs:framework/site/content-collections#content-collections","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#content-collections","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Content collections","text":"Studio writes this format for you — the content-type builder edits the content section visually, and entries show up in Browse your project with schema-driven forms. Content 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 . Formats other than JSON come from extension packages, so a site using Markdown or CSV content enables the parser in project.json : \"extensions\": [\"@jxsuite/parser\"] ."},{"id":"docs:framework/site/content-collections#defining-a-content-type","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#defining-a-content-type","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Defining a content type","text":"{ \"content\" : { \"blog\" : { \"source\" : \"./content/blog/\" , \"format\" : \"Markdown\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" }, \"pubDate\" : { \"type\" : \"string\" } }, \"required\" : [ \"title\" , \"pubDate\" ] } } } } Each definition takes four keys: 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. 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 ). $elements — components available inside entries as directives, e.g. jxsuite.com registers doc-note and doc-tip for these docs. See Jx Markdown . schema — a JSON Schema every entry's data must satisfy."},{"id":"docs:framework/site/content-collections#entry-ids","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#entry-ids","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Entry ids","text":"Every entry has an id , derived from the filesystem: Flat directories — the filename without extension: content/blog/hello-world.md → hello-world . 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 . 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."},{"id":"docs:framework/site/content-collections#media-beside-your-content","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#media-beside-your-content","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Media beside your content","text":"Entries reference images the way any markdown editor expects — relative to the file itself: ![ A diagram of the pipeline ]( ./images/diagram.png ) Keep 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/ . Only files an entry actually references are published; unreferenced siblings and the entry files themselves never reach dist/ . Remapping 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 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."},{"id":"docs:framework/site/content-collections#querying-with-contentcollection","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#querying-with-contentcollection","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Querying with ContentCollection","text":"Pages read a collection through a state entry with $prototype: \"ContentCollection\" : { \"state\" : { \"posts\" : { \"$prototype\" : \"ContentCollection\" , \"contentType\" : \"blog\" , \"filter\" : { \"draft\" : false }, \"sort\" : { \"field\" : \"pubDate\" , \"order\" : \"desc\" }, \"limit\" : 10 } } } 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. sort — a rule or array of rules, { \"field\": \"pubDate\", \"order\": \"desc\" } ; asc is the default. limit — maximum number of entries. The result is an array of entries, rendered with a repeater : { \"tagName\" : \"ul\" , \"children\" : { \"$prototype\" : \"Array\" , \"of\" : { \"$ref\" : \"#/state/posts\" }, \"map\" : { \"tagName\" : \"li\" , \"textContent\" : \"${item.data.title}\" } } }"},{"id":"docs:framework/site/content-collections#single-entries-with-contententry","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#single-entries-with-contententry","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Single entries with ContentEntry","text":"$prototype: \"ContentEntry\" fetches one entry by id — usually bound to a route parameter: { \"state\" : { \"post\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"blog\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } } } By 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). A resolved entry has this shape: { \"id\" : \"hello-world\" , \"data\" : { \"title\" : \"Hello World\" , \"pubDate\" : \"2024-01-15\" }, \"body\" : \"# Hello \\n\\n This is my first post.\" , \"$children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Hello\" }] } Frontmatter 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 ?? []}\" . Markdown 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. Fenced 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 . See Jx Markdown for the language set."},{"id":"docs:framework/site/content-collections#schema-validation","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#schema-validation","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Schema validation","text":"Every 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 and the content-type builder's field editor."},{"id":"docs:framework/site/content-collections#relationships","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#relationships","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Relationships","text":"A schema field can reference another content type: { \"author\" : { \"$ref\" : \"#/content/authors\" } } An 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 ."},{"id":"docs:framework/site/content-collections#related","collection":"docs","slug":"framework/site/content-collections","url":"/docs/framework/site/content-collections/#related","title":"Content collections","description":"The content section of project.json: sources, formats, entry ids, ContentCollection queries, ContentEntry lookups, and schema validation.","heading":"Related","text":"Routing — generating one page per entry with $paths Jx Markdown — the entry format for prose content project.json — where the content section lives"},{"id":"docs:framework/site/deployment","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"","text":"Build output and adapters 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. Run 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 . The dist/ contract Everything lands in one directory: dist/ ├── index.html # One HTML file per route ├── about/index.html ├── blog/hello-world/index.html ├── components/ # Compiled component JS + CSS sidecars ├── images/_optimized/ # Responsive image variants ├── sitemap.xml # When url is set in project.json ├── robots.txt # From public/, with a Sitemap: line appended ├── _redirects # From the redirects map ├── favicon.svg # public/ is copied in verbatim └── worker.js # Only with a server adapter (see below) Pages 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. Build options The build section of project.json : Property Default What it does outDir \"./dist\" Output directory trailingSlash \"always\" URL shape: \"always\" or \"never\" (below) sitemap true Set false to skip sitemap generation adapter (none) \"cloudflare-workers\" , \"cloudflare-pages\" , \"node\" , or \"bun\" trailingSlash \"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. Adapters Without 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: Adapter Extra output (none / static) Nothing — plain dist/ \"cloudflare-workers\" dist/worker.js , a Hono server that also serves the static assets \"cloudflare-pages\" dist/_worker.js + dist/_routes.json , only when there is a server tier \"node\" / \"bun\" dist/worker.js , a Hono server for that runtime What the worker serves Three route families, and nothing else: Route Served by /_jx/auth/* The auth extension — sign-up, sign-in, sign-out, session lookup /_jx/data/* The connector extension — table CRUD /_jx/server/<export> One route per timing: \"server\" state entry 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 . Details worth knowing: 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. 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. 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. 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. Switching hosts means switching the adapter — your source never changes. Hooking up a host The recipe is the same everywhere: build command bunx jx build , publish directory dist . Other hosts walks through Netlify and GitHub Pages, and Cloudflare Pages is built into Studio's publish flow. Related The build pipeline — what happens between source and dist/ Redirects — the _redirects file in the output SEO and metadata — sitemap.xml and robots.txt generation Databases — the tables behind /_jx/data , and why they need an adapter Timing — how a timing: \"server\" entry becomes a /_jx/server/ route"},{"id":"docs:framework/site/deployment#build-output-and-adapters","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#build-output-and-adapters","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"Build output and adapters","text":"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. Run 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 ."},{"id":"docs:framework/site/deployment#the-dist-contract","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#the-dist-contract","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"The dist/ contract","text":"Everything lands in one directory: dist/ ├── index.html # One HTML file per route ├── about/index.html ├── blog/hello-world/index.html ├── components/ # Compiled component JS + CSS sidecars ├── images/_optimized/ # Responsive image variants ├── sitemap.xml # When url is set in project.json ├── robots.txt # From public/, with a Sitemap: line appended ├── _redirects # From the redirects map ├── favicon.svg # public/ is copied in verbatim └── worker.js # Only with a server adapter (see below) Pages 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."},{"id":"docs:framework/site/deployment#build-options","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#build-options","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"Build options","text":"The build section of project.json : Property Default What it does outDir \"./dist\" Output directory trailingSlash \"always\" URL shape: \"always\" or \"never\" (below) sitemap true Set false to skip sitemap generation adapter (none) \"cloudflare-workers\" , \"cloudflare-pages\" , \"node\" , or \"bun\""},{"id":"docs:framework/site/deployment#trailingslash","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#trailingslash","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"trailingSlash","text":"\"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."},{"id":"docs:framework/site/deployment#adapters","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#adapters","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"Adapters","text":"Without 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: Adapter Extra output (none / static) Nothing — plain dist/ \"cloudflare-workers\" dist/worker.js , a Hono server that also serves the static assets \"cloudflare-pages\" dist/_worker.js + dist/_routes.json , only when there is a server tier \"node\" / \"bun\" dist/worker.js , a Hono server for that runtime"},{"id":"docs:framework/site/deployment#what-the-worker-serves","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#what-the-worker-serves","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"What the worker serves","text":"Three route families, and nothing else: Route Served by /_jx/auth/* The auth extension — sign-up, sign-in, sign-out, session lookup /_jx/data/* The connector extension — table CRUD /_jx/server/<export> One route per timing: \"server\" state entry 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 . Details worth knowing: 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. 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. 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. 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. Switching hosts means switching the adapter — your source never changes."},{"id":"docs:framework/site/deployment#hooking-up-a-host","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#hooking-up-a-host","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"Hooking up a host","text":"The recipe is the same everywhere: build command bunx jx build , publish directory dist . Other hosts walks through Netlify and GitHub Pages, and Cloudflare Pages is built into Studio's publish flow."},{"id":"docs:framework/site/deployment#related","collection":"docs","slug":"framework/site/deployment","url":"/docs/framework/site/deployment/#related","title":"Build output and adapters","description":"What bunx jx build writes to dist/, how trailingSlash shapes URLs, and the deployment adapters for static hosts, Cloudflare, Node, and Bun.","heading":"Related","text":"The build pipeline — what happens between source and dist/ Redirects — the _redirects file in the output SEO and metadata — sitemap.xml and robots.txt generation Databases — the tables behind /_jx/data , and why they need an adapter Timing — how a timing: \"server\" entry becomes a /_jx/server/ route"},{"id":"docs:framework/site/images","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"","text":"Images The 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 . Configuration All properties are optional; these are the defaults: { \"images\" : { \"optimize\" : true , \"widths\" : [ 320 , 640 , 960 , 1280 , 1920 ], \"formats\" : [ \"webp\" , \"avif\" ], \"quality\" : { \"webp\" : 80 , \"avif\" : 65 , \"jpeg\" : 80 , \"png\" : 80 }, \"sizes\" : \"(max-width: 768px) 100vw, 50vw\" , \"lazyLoad\" : true , \"service\" : \"build\" } } Property What it controls optimize Master switch — false disables all image processing widths Pixel widths for the responsive variants formats Output formats ( \"webp\" , \"avif\" , \"jpeg\" , \"png\" ) quality Per-format compression quality (0–100) sizes Default CSS sizes attribute injected alongside srcset lazyLoad Adds loading=\"lazy\" and decoding=\"async\" service \"build\" = Sharp at build time; \"cloudflare\" = transform URLs served by Cloudflare (below) remoteDomains Https hostnames whose remote images also get transform srcsets — \"cloudflare\" service only What the build does For each eligible image, the pipeline (powered by Sharp ): Filters widths to those at or below the image's natural width — no upscaling; the original width is always included as a breakpoint. Generates one variant per width × format and writes it to dist/images/_optimized/{stem}-{width}-{hash}.{format} (e.g. hero-640-a1b2c3d4.webp ). 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. Images 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> . Which images are eligible Processed: <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. Skipped automatically: External URLs ( http:// , https:// , // , data: ) SVGs and GIFs Dynamic src values (template expressions or bindings) Anything carrying a data-no-optimize attribute Where srcs resolve In 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. Content entries are the exception — they resolve against themselves, so a collection stays readable in a markdown editor: ![ A diagram ]( ./images/diagram.png ) A 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 . Per-image overrides Individual images can override the global config through ordinary attributes: { \"tagName\" : \"img\" , \"attributes\" : { \"src\" : \"/images/hero.jpg\" , \"alt\" : \"Hero image\" , \"sizes\" : \"(max-width: 640px) 80vw, 40vw\" , \"loading\" : \"eager\" } } sizes — replaces the configured default for this image loading=\"eager\" — keeps loading=\"lazy\" off an above-the-fold image data-no-optimize — skips the pipeline entirely Caching Variants 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. The 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. The Cloudflare service Setting \"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. This 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. Related Build output and adapters — where the variants land in dist/ Media in Studio — browsing and placing images visually project.json — the full configuration reference"},{"id":"docs:framework/site/images#images","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#images","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Images","text":"The 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 ."},{"id":"docs:framework/site/images#configuration","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#configuration","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Configuration","text":"All properties are optional; these are the defaults: { \"images\" : { \"optimize\" : true , \"widths\" : [ 320 , 640 , 960 , 1280 , 1920 ], \"formats\" : [ \"webp\" , \"avif\" ], \"quality\" : { \"webp\" : 80 , \"avif\" : 65 , \"jpeg\" : 80 , \"png\" : 80 }, \"sizes\" : \"(max-width: 768px) 100vw, 50vw\" , \"lazyLoad\" : true , \"service\" : \"build\" } } Property What it controls optimize Master switch — false disables all image processing widths Pixel widths for the responsive variants formats Output formats ( \"webp\" , \"avif\" , \"jpeg\" , \"png\" ) quality Per-format compression quality (0–100) sizes Default CSS sizes attribute injected alongside srcset lazyLoad Adds loading=\"lazy\" and decoding=\"async\" service \"build\" = Sharp at build time; \"cloudflare\" = transform URLs served by Cloudflare (below) remoteDomains Https hostnames whose remote images also get transform srcsets — \"cloudflare\" service only"},{"id":"docs:framework/site/images#what-the-build-does","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#what-the-build-does","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"What the build does","text":"For each eligible image, the pipeline (powered by Sharp ): Filters widths to those at or below the image's natural width — no upscaling; the original width is always included as a breakpoint. Generates one variant per width × format and writes it to dist/images/_optimized/{stem}-{width}-{hash}.{format} (e.g. hero-640-a1b2c3d4.webp ). 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. Images 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> ."},{"id":"docs:framework/site/images#which-images-are-eligible","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#which-images-are-eligible","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Which images are eligible","text":"Processed: <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. Skipped automatically: External URLs ( http:// , https:// , // , data: ) SVGs and GIFs Dynamic src values (template expressions or bindings) Anything carrying a data-no-optimize attribute"},{"id":"docs:framework/site/images#where-srcs-resolve","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#where-srcs-resolve","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Where srcs resolve","text":"In 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. Content entries are the exception — they resolve against themselves, so a collection stays readable in a markdown editor: ![ A diagram ]( ./images/diagram.png ) A 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 ."},{"id":"docs:framework/site/images#per-image-overrides","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#per-image-overrides","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Per-image overrides","text":"Individual images can override the global config through ordinary attributes: { \"tagName\" : \"img\" , \"attributes\" : { \"src\" : \"/images/hero.jpg\" , \"alt\" : \"Hero image\" , \"sizes\" : \"(max-width: 640px) 80vw, 40vw\" , \"loading\" : \"eager\" } } sizes — replaces the configured default for this image loading=\"eager\" — keeps loading=\"lazy\" off an above-the-fold image data-no-optimize — skips the pipeline entirely"},{"id":"docs:framework/site/images#caching","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#caching","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Caching","text":"Variants 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. The 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."},{"id":"docs:framework/site/images#the-cloudflare-service","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#the-cloudflare-service","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"The Cloudflare service","text":"Setting \"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. This 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."},{"id":"docs:framework/site/images#related","collection":"docs","slug":"framework/site/images","url":"/docs/framework/site/images/#related","title":"Images","description":"The build-time image pipeline: responsive srcset variants in WebP and AVIF, lazy-loading attributes, per-image overrides, and caching.","heading":"Related","text":"Build output and adapters — where the variants land in dist/ Media in Studio — browsing and placing images visually project.json — the full configuration reference"},{"id":"docs:framework/site/jx-markdown","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"","text":"Jx Markdown Studio writes this format for you. The writing surface and frontmatter panel edit .md files visually — this page documents the syntax for when you hand-edit one or read a diff. A 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: YAML frontmatter — top-level document properties ( tagName , state , style , …) Directives — explicit elements using remark-directive syntax Standard Markdown — headings, paragraphs, lists, and links mapped to HTML elements Markdown 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. A minimal component --- tagName : my-greeting state : name : type : string default : World --- :::div # Hello, ${state.name}! ::: The 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. Frontmatter All 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 . Directives Three directive forms cover the element tree: Container directives wrap children. Outer containers use more colons than inner ones (minimum three), and the closing fence matches the opening count: ::::section{className=\"hero\"} :::h1 Welcome ::: :::: Leaf directives (two colons) are self-closing: ::img{src=\"/photo.jpg\" alt=\"A photo\"} Text directives (one colon) sit inline within prose: Click :a[ here ]{href=\"/about\"} for details. The directive name is the element's tagName — any HTML tag or any registered custom element. Attributes Attributes 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: You write The document gets prototype , ref , component , props , switch , elements $prototype , $ref , $component , $props , $switch , $elements --title , --description $title , $description (element annotations, dropped from HTML output) style.hover.color (see below) style[\":hover\"].color DOM 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. Nested objects are written as dot-separated keys. Props on a component instance use props.* : :::pricing-card{props.plan=\"Pro\" props.price.ref=\"#/state/proPrice\"} ::: which expands to: { \"tagName\" : \"pricing-card\" , \"$props\" : { \"plan\" : \"Pro\" , \"price\" : { \"$ref\" : \"#/state/proPrice\" } } } Style attributes Element styles are style.* dot-path attributes. CSS pseudo-classes drop their : prefix and named media queries drop their @ prefix — the transpiler restores both: ::button{style.padding=\"8px 16px\" style.hover.backgroundColor=\"blue\" style.--dark.color=\"#f0f0f0\"} Click me { \"tagName\" : \"button\" , \"style\" : { \"padding\" : \"8px 16px\" , \":hover\" : { \"backgroundColor\" : \"blue\" }, \"@--dark\" : { \"color\" : \"#f0f0f0\" } } } Root-level styles go in frontmatter under style , where YAML allows :hover and @--dark keys to be written directly. Repeaters An array repeater has no tagName , so it serializes as a directive named after its $prototype , with the nested block as the map template: # Recent posts :::Array{items.ref=\"#/state/posts\"} :li{children.0=\"${$map/item/title}\"} ::: Standard Markdown Plain 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. Fenced 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 automatically. Unknown languages fall back to plain text — no fence ever breaks. These 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. Markdown or JSON? Both formats produce the same document, and Studio edits both transparently — so choose by what the file mostly contains: Prefer Markdown Prefer JSON Content-heavy pages (blog posts, docs) Complex interactive components Components with significant prose Components with many state functions Landing pages, marketing content Deeply nested element hierarchies Quick prototyping Components with complex $prototype usage Two 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. Related Content collections — Markdown entries with validated frontmatter Components — the JSON document model both formats produce Writing — editing Markdown visually in Studio"},{"id":"docs:framework/site/jx-markdown#jx-markdown","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#jx-markdown","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Jx Markdown","text":"Studio writes this format for you. The writing surface and frontmatter panel edit .md files visually — this page documents the syntax for when you hand-edit one or read a diff. A 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: YAML frontmatter — top-level document properties ( tagName , state , style , …) Directives — explicit elements using remark-directive syntax Standard Markdown — headings, paragraphs, lists, and links mapped to HTML elements Markdown 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."},{"id":"docs:framework/site/jx-markdown#a-minimal-component","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#a-minimal-component","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"A minimal component","text":"--- tagName : my-greeting state : name : type : string default : World --- :::div # Hello, ${state.name}! ::: The 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."},{"id":"docs:framework/site/jx-markdown#frontmatter","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#frontmatter","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Frontmatter","text":"All 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 ."},{"id":"docs:framework/site/jx-markdown#directives","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#directives","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Directives","text":"Three directive forms cover the element tree: Container directives wrap children. Outer containers use more colons than inner ones (minimum three), and the closing fence matches the opening count: ::::section{className=\"hero\"} :::h1 Welcome ::: :::: Leaf directives (two colons) are self-closing: ::img{src=\"/photo.jpg\" alt=\"A photo\"} Text directives (one colon) sit inline within prose: Click :a[ here ]{href=\"/about\"} for details. The directive name is the element's tagName — any HTML tag or any registered custom element."},{"id":"docs:framework/site/jx-markdown#attributes","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#attributes","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Attributes","text":"Attributes 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: You write The document gets prototype , ref , component , props , switch , elements $prototype , $ref , $component , $props , $switch , $elements --title , --description $title , $description (element annotations, dropped from HTML output) style.hover.color (see below) style[\":hover\"].color DOM 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. Nested objects are written as dot-separated keys. Props on a component instance use props.* : :::pricing-card{props.plan=\"Pro\" props.price.ref=\"#/state/proPrice\"} ::: which expands to: { \"tagName\" : \"pricing-card\" , \"$props\" : { \"plan\" : \"Pro\" , \"price\" : { \"$ref\" : \"#/state/proPrice\" } } }"},{"id":"docs:framework/site/jx-markdown#style-attributes","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#style-attributes","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Style attributes","text":"Element styles are style.* dot-path attributes. CSS pseudo-classes drop their : prefix and named media queries drop their @ prefix — the transpiler restores both: ::button{style.padding=\"8px 16px\" style.hover.backgroundColor=\"blue\" style.--dark.color=\"#f0f0f0\"} Click me { \"tagName\" : \"button\" , \"style\" : { \"padding\" : \"8px 16px\" , \":hover\" : { \"backgroundColor\" : \"blue\" }, \"@--dark\" : { \"color\" : \"#f0f0f0\" } } } Root-level styles go in frontmatter under style , where YAML allows :hover and @--dark keys to be written directly."},{"id":"docs:framework/site/jx-markdown#repeaters","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#repeaters","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Repeaters","text":"An array repeater has no tagName , so it serializes as a directive named after its $prototype , with the nested block as the map template: # Recent posts :::Array{items.ref=\"#/state/posts\"} :li{children.0=\"${$map/item/title}\"} :::"},{"id":"docs:framework/site/jx-markdown#standard-markdown","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#standard-markdown","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Standard Markdown","text":"Plain 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. Fenced 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 automatically. Unknown languages fall back to plain text — no fence ever breaks. These 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."},{"id":"docs:framework/site/jx-markdown#markdown-or-json","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#markdown-or-json","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Markdown or JSON?","text":"Both formats produce the same document, and Studio edits both transparently — so choose by what the file mostly contains: Prefer Markdown Prefer JSON Content-heavy pages (blog posts, docs) Complex interactive components Components with significant prose Components with many state functions Landing pages, marketing content Deeply nested element hierarchies Quick prototyping Components with complex $prototype usage Two 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."},{"id":"docs:framework/site/jx-markdown#related","collection":"docs","slug":"framework/site/jx-markdown","url":"/docs/framework/site/jx-markdown/#related","title":"Jx Markdown","description":"Author Jx components and pages in Markdown: YAML frontmatter, directive syntax, attribute conventions, and when to choose Markdown over JSON.","heading":"Related","text":"Content collections — Markdown entries with validated frontmatter Components — the JSON document model both formats produce Writing — editing Markdown visually in Studio"},{"id":"docs:framework/site/layouts","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"","text":"Layouts Studio writes this format for you — layouts appear alongside pages and components in Browse your project , and you edit them on the same canvas. A 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 use, run at compile time instead of DOM time. Layout documents A minimal layout registers its chrome components via $elements (paths relative to the layout file) and places one default <slot> : { \"$elements\" : [{ \"$ref\" : \"../components/site-header.json\" }], \"tagName\" : \"html\" , \"children\" : [ { \"tagName\" : \"body\" , \"children\" : [ { \"tagName\" : \"site-header\" }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } ] } A 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> . Declaring a layout Pages opt in with a top-level $layout , resolved from the project root (not the page's folder): { \"$layout\" : \"./layouts/base.json\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"About us\" }] } If a page omits $layout , the site default from project.json defaults.layout applies. \"$layout\": false renders the page with no layout at all — useful for standalone landing pages and embeds. The page's children are distributed into the layout's <slot> positions when the page compiles. Named slots Layouts can define several regions by naming their slots: { \"tagName\" : \"body\" , \"children\" : [ { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } Pages target a named slot with the standard slot attribute; children without one go to the default slot: { \"$layout\" : \"./layouts/docs.json\" , \"children\" : [ { \"tagName\" : \"nav\" , \"attributes\" : { \"slot\" : \"sidebar\" }, \"children\" : [{ \"tagName\" : \"a\" , \"href\" : \"/docs/intro\" , \"textContent\" : \"Intro\" }] }, { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Documentation\" }] } ] } Per the HTML spec, a <slot> 's own children are fallback content — they render only when the page supplies nothing for that slot. Nesting A layout may declare its own $layout , composing shells: { \"$layout\" : \"./layouts/base.json\" , \"children\" : [ { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } Saved 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. What merges Compiling a page against its layout produces one document: Children — page children are distributed into the layout's slots. 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 . $media , style , attributes — merged the same way, page over layout. $head — merged site → layout → page, later entries winning for singletons like <title> ; see SEO and metadata . Layouts also receive the compiler-injected contexts: $page ( title , url , params ) and $site ( name , url , plus site state) — see project.json . Related Routing — which pages exist and how they resolve Components — the runtime slot mechanism layouts reuse SEO and metadata — the $head merge in detail"},{"id":"docs:framework/site/layouts#layouts","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#layouts","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Layouts","text":"Studio writes this format for you — layouts appear alongside pages and components in Browse your project , and you edit them on the same canvas. A 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 use, run at compile time instead of DOM time."},{"id":"docs:framework/site/layouts#layout-documents","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#layout-documents","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Layout documents","text":"A minimal layout registers its chrome components via $elements (paths relative to the layout file) and places one default <slot> : { \"$elements\" : [{ \"$ref\" : \"../components/site-header.json\" }], \"tagName\" : \"html\" , \"children\" : [ { \"tagName\" : \"body\" , \"children\" : [ { \"tagName\" : \"site-header\" }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } ] } A 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> ."},{"id":"docs:framework/site/layouts#declaring-a-layout","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#declaring-a-layout","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Declaring a layout","text":"Pages opt in with a top-level $layout , resolved from the project root (not the page's folder): { \"$layout\" : \"./layouts/base.json\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"About us\" }] } If a page omits $layout , the site default from project.json defaults.layout applies. \"$layout\": false renders the page with no layout at all — useful for standalone landing pages and embeds. The page's children are distributed into the layout's <slot> positions when the page compiles."},{"id":"docs:framework/site/layouts#named-slots","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#named-slots","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Named slots","text":"Layouts can define several regions by naming their slots: { \"tagName\" : \"body\" , \"children\" : [ { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"main\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } Pages target a named slot with the standard slot attribute; children without one go to the default slot: { \"$layout\" : \"./layouts/docs.json\" , \"children\" : [ { \"tagName\" : \"nav\" , \"attributes\" : { \"slot\" : \"sidebar\" }, \"children\" : [{ \"tagName\" : \"a\" , \"href\" : \"/docs/intro\" , \"textContent\" : \"Intro\" }] }, { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"h1\" , \"textContent\" : \"Documentation\" }] } ] } Per the HTML spec, a <slot> 's own children are fallback content — they render only when the page supplies nothing for that slot."},{"id":"docs:framework/site/layouts#nesting","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#nesting","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Nesting","text":"A layout may declare its own $layout , composing shells: { \"$layout\" : \"./layouts/base.json\" , \"children\" : [ { \"tagName\" : \"aside\" , \"children\" : [{ \"tagName\" : \"slot\" , \"attributes\" : { \"name\" : \"sidebar\" } }] }, { \"tagName\" : \"article\" , \"children\" : [{ \"tagName\" : \"slot\" }] } ] } Saved 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."},{"id":"docs:framework/site/layouts#what-merges","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#what-merges","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"What merges","text":"Compiling a page against its layout produces one document: Children — page children are distributed into the layout's slots. 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 . $media , style , attributes — merged the same way, page over layout. $head — merged site → layout → page, later entries winning for singletons like <title> ; see SEO and metadata . Layouts also receive the compiler-injected contexts: $page ( title , url , params ) and $site ( name , url , plus site state) — see project.json ."},{"id":"docs:framework/site/layouts#related","collection":"docs","slug":"framework/site/layouts","url":"/docs/framework/site/layouts/#related","title":"Layouts","description":"Shared page shells in Jx sites: $layout, slot injection, named slots, layout nesting, and how layout and page state merge.","heading":"Related","text":"Routing — which pages exist and how they resolve Components — the runtime slot mechanism layouts reuse SEO and metadata — the $head merge in detail"},{"id":"docs:framework/site/project-json","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/","title":"project.json","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.","heading":"","text":"project.json Studio writes this file for you — Project settings edits the identity, defaults, and build keys; the content-type builder writes content ; the design-token editor writes style . project.json at the project root is the only required configuration file in a Jx site. A minimal real one (abbreviated from jxsuite.com): { \"$schema\" : \"./project.schema.json\" , \"name\" : \"Jx Suite\" , \"url\" : \"https://jxsuite.com\" , \"defaults\" : { \"layout\" : \"./layouts/base.json\" , \"lang\" : \"en\" }, \"build\" : { \"outDir\" : \"./dist\" , \"trailingSlash\" : \"always\" }, \"extensions\" : [ \"@jxsuite/parser\" ] } Key reference Key Type Purpose name string Site name — the default <title> and $site.name url string Production URL, used for canonical URLs and the sitemap defaults object layout , lang , and charset applied to every page $head array Global <head> entries injected into every page $media object Named breakpoints available in every style object style object Design tokens ( -- keys) and global element rules state object Site-wide state, merged read-only into every page $defs object Shared JSON Schema type definitions for the whole project content object Content types — see Content collections imports object $prototype name → class path, cascaded to all pages extensions array Extension packages providing formats, sections, and classes redirects object Source path → destination — see Redirects copy object Extra source → destination file mappings copied into the build output build object outDir , trailingSlash , adapter — see Deployment images object Image optimization settings — see Images Enabled extensions add sections of their own alongside these. The first-party ones: Key Type Contributed by Purpose connections object @jxsuite/connector Named database connections — see Connections data object @jxsuite/connector Dynamic tables served over /_jx/data — see Data tables auth object @jxsuite/auth Visitor accounts and sessions — see Auth and secrets search object @jxsuite/search Build-time search index — see Search Identity and defaults 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. Global head entries $head entries are injected into every page's <head> , before layout- and page-level entries: { \"$head\" : [ { \"tagName\" : \"link\" , \"attributes\" : { \"rel\" : \"icon\" , \"href\" : \"/favicon.svg\" } }, { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"viewport\" , \"content\" : \"width=device-width, initial-scale=1\" } } ] } A 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 . Breakpoints and style tokens $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 — @--dark token overrides in style become the dark variant of the design. { \"$media\" : { \"--md\" : \"(max-width: 768px)\" , \"--sm\" : \"(max-width: 640px)\" }, \"style\" : { \"--color-accent\" : \"#3b82f6\" , \"--radius\" : \"8px\" , \"fontFamily\" : \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\" , \"backgroundColor\" : \"var(--color-bg-primary)\" , \"table\" : { \"width\" : \"100%\" , \"borderCollapse\" : \"collapse\" } } } Every component in the project can reference var(--color-accent) and use \"@--md\": { … } overrides without importing anything. See Styling for the style object format and Design tokens for the Studio editor. Site state and shared types 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. { \"state\" : { \"siteName\" : \"My Site\" , \"socialLinks\" : [{ \"label\" : \"GitHub\" , \"url\" : \"https://github.com/example\" }] } } $defs holds reusable JSON Schema type definitions — 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 . Content types The 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): { \"content\" : { \"docs\" : { \"source\" : \"../../docs\" , \"format\" : \"Markdown\" , \"$elements\" : [{ \"$ref\" : \"./components/doc-note.json\" }], \"schema\" : { \"type\" : \"object\" , \"required\" : [ \"title\" ] } } } } The keys and query prototypes are covered in Content collections . Redirects redirects maps old paths to new ones — a plain string for a 301, or an object to pick the status: { \"redirects\" : { \"/docs/get-studio\" : \"/docs/start/install/\" , \"/legacy/post/:slug\" : { \"destination\" : \"/blog/:slug\" , \"status\" : 301 } } } Patterns and status codes are covered in Redirects . Copying extra files copy maps files from anywhere in the repository into the build output — useful for publishing artifacts that live outside public/ : { \"copy\" : { \"../../packages/schema/project-schema.json\" : \"schema/project/v1/index.json\" } } Sources 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. Build build controls output: outDir (default ./dist ), trailingSlash , an optional deployment adapter , and sitemap generation. See the build pipeline and Deployment . Extensions and imports 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 and Dependencies and imports . Extension-contributed sections An 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. { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"@jxsuite/auth\" ], \"connections\" : { \"main\" : { \"provider\" : \"sqlite\" } }, \"data\" : { \"comments\" : { \"connection\" : \"main\" , \"permissions\" : { \"read\" : \"public\" , \"insert\" : \"authenticated\" , \"update\" : \"owner\" }, \"ownerField\" : \"author_id\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"message\" : { \"type\" : \"string\" }, \"author_id\" : { \"type\" : \"string\" } }, \"required\" : [ \"message\" ] } } }, \"auth\" : { \"connection\" : \"main\" , \"methods\" : { \"emailPassword\" : true }, \"redirects\" : { \"afterSignIn\" : \"/\" , \"afterSignOut\" : \"/\" } }, \"build\" : { \"adapter\" : \"cloudflare-pages\" } } Two rules run through all of them: 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 . 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 . Studio writes these sections for you from Settings > Connections , Data Tables , and Authentication — see Databases . What documents inherit Site-level declarations cascade into every page automatically — no imports needed: $head entries are prepended to every page's <head> . $media breakpoints and style tokens are available in every component. defaults.lang sets <html lang> everywhere. state merges into page state (page wins) and is exposed as $site . imports and $elements merge with page-level entries (page wins on collision). Everything else is deliberate: files in data/ load via an explicit $ref , collection data requires ContentCollection or ContentEntry declarations, and components receive outside data only through $props — scope never leaks across a component boundary."},{"id":"docs:framework/site/project-json#projectjson","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#projectjson","title":"project.json","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.","heading":"project.json","text":"Studio writes this file for you — Project settings edits the identity, defaults, and build keys; the content-type builder writes content ; the design-token editor writes style . project.json at the project root is the only required configuration file in a Jx site. A minimal real one (abbreviated from jxsuite.com): { \"$schema\" : \"./project.schema.json\" , \"name\" : \"Jx Suite\" , \"url\" : \"https://jxsuite.com\" , \"defaults\" : { \"layout\" : \"./layouts/base.json\" , \"lang\" : \"en\" }, \"build\" : { \"outDir\" : \"./dist\" , \"trailingSlash\" : \"always\" }, \"extensions\" : [ \"@jxsuite/parser\" ] }"},{"id":"docs:framework/site/project-json#key-reference","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#key-reference","title":"project.json","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.","heading":"Key reference","text":"Key Type Purpose name string Site name — the default <title> and $site.name url string Production URL, used for canonical URLs and the sitemap defaults object layout , lang , and charset applied to every page $head array Global <head> entries injected into every page $media object Named breakpoints available in every style object style object Design tokens ( -- keys) and global element rules state object Site-wide state, merged read-only into every page $defs object Shared JSON Schema type definitions for the whole project content object Content types — see Content collections imports object $prototype name → class path, cascaded to all pages extensions array Extension packages providing formats, sections, and classes redirects object Source path → destination — see Redirects copy object Extra source → destination file mappings copied into the build output build object outDir , trailingSlash , adapter — see Deployment images object Image optimization settings — see Images Enabled extensions add sections of their own alongside these. The first-party ones: Key Type Contributed by Purpose connections object @jxsuite/connector Named database connections — see Connections data object @jxsuite/connector Dynamic tables served over /_jx/data — see Data tables auth object @jxsuite/auth Visitor accounts and sessions — see Auth and secrets search object @jxsuite/search Build-time search index — see Search"},{"id":"docs:framework/site/project-json#identity-and-defaults","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#identity-and-defaults","title":"project.json","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.","heading":"Identity and defaults","text":"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."},{"id":"docs:framework/site/project-json#global-head-entries","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#global-head-entries","title":"project.json","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.","heading":"Global head entries","text":"$head entries are injected into every page's <head> , before layout- and page-level entries: { \"$head\" : [ { \"tagName\" : \"link\" , \"attributes\" : { \"rel\" : \"icon\" , \"href\" : \"/favicon.svg\" } }, { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"viewport\" , \"content\" : \"width=device-width, initial-scale=1\" } } ] } A 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 ."},{"id":"docs:framework/site/project-json#breakpoints-and-style-tokens","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#breakpoints-and-style-tokens","title":"project.json","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.","heading":"Breakpoints and style tokens","text":"$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 — @--dark token overrides in style become the dark variant of the design. { \"$media\" : { \"--md\" : \"(max-width: 768px)\" , \"--sm\" : \"(max-width: 640px)\" }, \"style\" : { \"--color-accent\" : \"#3b82f6\" , \"--radius\" : \"8px\" , \"fontFamily\" : \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\" , \"backgroundColor\" : \"var(--color-bg-primary)\" , \"table\" : { \"width\" : \"100%\" , \"borderCollapse\" : \"collapse\" } } } Every component in the project can reference var(--color-accent) and use \"@--md\": { … } overrides without importing anything. See Styling for the style object format and Design tokens for the Studio editor."},{"id":"docs:framework/site/project-json#site-state-and-shared-types","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#site-state-and-shared-types","title":"project.json","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.","heading":"Site state and shared types","text":"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. { \"state\" : { \"siteName\" : \"My Site\" , \"socialLinks\" : [{ \"label\" : \"GitHub\" , \"url\" : \"https://github.com/example\" }] } } $defs holds reusable JSON Schema type definitions — 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 ."},{"id":"docs:framework/site/project-json#content-types","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#content-types","title":"project.json","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.","heading":"Content types","text":"The 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): { \"content\" : { \"docs\" : { \"source\" : \"../../docs\" , \"format\" : \"Markdown\" , \"$elements\" : [{ \"$ref\" : \"./components/doc-note.json\" }], \"schema\" : { \"type\" : \"object\" , \"required\" : [ \"title\" ] } } } } The keys and query prototypes are covered in Content collections ."},{"id":"docs:framework/site/project-json#redirects","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#redirects","title":"project.json","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.","heading":"Redirects","text":"redirects maps old paths to new ones — a plain string for a 301, or an object to pick the status: { \"redirects\" : { \"/docs/get-studio\" : \"/docs/start/install/\" , \"/legacy/post/:slug\" : { \"destination\" : \"/blog/:slug\" , \"status\" : 301 } } } Patterns and status codes are covered in Redirects ."},{"id":"docs:framework/site/project-json#copying-extra-files","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#copying-extra-files","title":"project.json","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.","heading":"Copying extra files","text":"copy maps files from anywhere in the repository into the build output — useful for publishing artifacts that live outside public/ : { \"copy\" : { \"../../packages/schema/project-schema.json\" : \"schema/project/v1/index.json\" } } Sources 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."},{"id":"docs:framework/site/project-json#build","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#build","title":"project.json","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.","heading":"Build","text":"build controls output: outDir (default ./dist ), trailingSlash , an optional deployment adapter , and sitemap generation. See the build pipeline and Deployment ."},{"id":"docs:framework/site/project-json#extensions-and-imports","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#extensions-and-imports","title":"project.json","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.","heading":"Extensions and imports","text":"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 and Dependencies and imports ."},{"id":"docs:framework/site/project-json#extension-contributed-sections","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#extension-contributed-sections","title":"project.json","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.","heading":"Extension-contributed sections","text":"An 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. { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/connector\" , \"@jxsuite/auth\" ], \"connections\" : { \"main\" : { \"provider\" : \"sqlite\" } }, \"data\" : { \"comments\" : { \"connection\" : \"main\" , \"permissions\" : { \"read\" : \"public\" , \"insert\" : \"authenticated\" , \"update\" : \"owner\" }, \"ownerField\" : \"author_id\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"message\" : { \"type\" : \"string\" }, \"author_id\" : { \"type\" : \"string\" } }, \"required\" : [ \"message\" ] } } }, \"auth\" : { \"connection\" : \"main\" , \"methods\" : { \"emailPassword\" : true }, \"redirects\" : { \"afterSignIn\" : \"/\" , \"afterSignOut\" : \"/\" } }, \"build\" : { \"adapter\" : \"cloudflare-pages\" } } Two rules run through all of them: 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 . 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 . Studio writes these sections for you from Settings > Connections , Data Tables , and Authentication — see Databases ."},{"id":"docs:framework/site/project-json#what-documents-inherit","collection":"docs","slug":"framework/site/project-json","url":"/docs/framework/site/project-json/#what-documents-inherit","title":"project.json","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.","heading":"What documents inherit","text":"Site-level declarations cascade into every page automatically — no imports needed: $head entries are prepended to every page's <head> . $media breakpoints and style tokens are available in every component. defaults.lang sets <html lang> everywhere. state merges into page state (page wins) and is exposed as $site . imports and $elements merge with page-level entries (page wins on collision). Everything else is deliberate: files in data/ load via an explicit $ref , collection data requires ContentCollection or ContentEntry declarations, and components receive outside data only through $props — scope never leaks across a component boundary."},{"id":"docs:framework/site/redirects","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/","title":"Redirects","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.","heading":"","text":"Redirects When 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. The redirects map redirects maps source paths to destinations. These are real entries from jxsuite.com: { \"redirects\" : { \"/docs/get-studio\" : \"/docs/start/install/\" , \"/docs/components\" : \"/docs/framework/concepts/components/\" , \"/docs/spec\" : \"/docs/framework/\" } } Each value is either a plain destination string, or an object when you need a specific status code: { \"redirects\" : { \"/old-page\" : \"/new-page\" , \"/moved-permanently\" : { \"destination\" : \"/new-location\" , \"status\" : 301 }, \"/temporary-redirect\" : { \"destination\" : \"/other-page\" , \"status\" : 302 } } } The string form defaults to 301 (permanent). Use 302 for temporary moves; on hosts that support rewrites, 200 serves the destination without redirecting. What the build emits For 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: < meta http-equiv = \"refresh\" content = \"0;url=/new-page\" /> < link rel = \"canonical\" href = \"/new-page\" /> It also writes a single dist/_redirects file — the Netlify/Cloudflare format, one rule per line: /old-page /new-page 301 /blog/:slug /posts/:slug 301 Pattern 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. Platform behavior 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. GitHub Pages and other plain static hosts ignore _redirects ; the meta-refresh HTML serves as the redirect. Static sources work; pattern rules don't. Redirect sources are not pages: they never appear in the sitemap , and the canonical link on each fallback page points crawlers at the destination. A 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. Related Build output and adapters — where _redirects fits in the dist/ contract Routing — how real pages claim their URLs Other hosts — per-host publishing recipes"},{"id":"docs:framework/site/redirects#redirects","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/#redirects","title":"Redirects","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.","heading":"Redirects","text":"When 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."},{"id":"docs:framework/site/redirects#the-redirects-map","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/#the-redirects-map","title":"Redirects","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.","heading":"The redirects map","text":"redirects maps source paths to destinations. These are real entries from jxsuite.com: { \"redirects\" : { \"/docs/get-studio\" : \"/docs/start/install/\" , \"/docs/components\" : \"/docs/framework/concepts/components/\" , \"/docs/spec\" : \"/docs/framework/\" } } Each value is either a plain destination string, or an object when you need a specific status code: { \"redirects\" : { \"/old-page\" : \"/new-page\" , \"/moved-permanently\" : { \"destination\" : \"/new-location\" , \"status\" : 301 }, \"/temporary-redirect\" : { \"destination\" : \"/other-page\" , \"status\" : 302 } } } The string form defaults to 301 (permanent). Use 302 for temporary moves; on hosts that support rewrites, 200 serves the destination without redirecting."},{"id":"docs:framework/site/redirects#what-the-build-emits","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/#what-the-build-emits","title":"Redirects","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.","heading":"What the build emits","text":"For 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: < meta http-equiv = \"refresh\" content = \"0;url=/new-page\" /> < link rel = \"canonical\" href = \"/new-page\" /> It also writes a single dist/_redirects file — the Netlify/Cloudflare format, one rule per line: /old-page /new-page 301 /blog/:slug /posts/:slug 301 Pattern 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."},{"id":"docs:framework/site/redirects#platform-behavior","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/#platform-behavior","title":"Redirects","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.","heading":"Platform behavior","text":"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. GitHub Pages and other plain static hosts ignore _redirects ; the meta-refresh HTML serves as the redirect. Static sources work; pattern rules don't. Redirect sources are not pages: they never appear in the sitemap , and the canonical link on each fallback page points crawlers at the destination. A 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."},{"id":"docs:framework/site/redirects#related","collection":"docs","slug":"framework/site/redirects","url":"/docs/framework/site/redirects/#related","title":"Redirects","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.","heading":"Related","text":"Build output and adapters — where _redirects fits in the dist/ contract Routing — how real pages claim their URLs Other hosts — per-host publishing recipes"},{"id":"docs:framework/site/relationships","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"","text":"Relationships Studio writes this format for you. The content-type builder creates reference fields visually — this page documents the schema they produce and how references resolve at build time. A 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. The reference form A reference field's schema points at the target content type with a #/content/<type> pointer: { \"content\" : { \"authors\" : { \"source\" : \"./content/authors/\" , \"format\" : \"Markdown\" }, \"blog\" : { \"source\" : \"./content/blog/\" , \"format\" : \"Markdown\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" }, \"author\" : { \"$ref\" : \"#/content/authors\" } } } } } } Cardinality is expressed with plain JSON Schema — there is no separate relationship syntax: Cardinality Field schema To-one { \"$ref\": \"#/content/authors\" } To-many { \"type\": \"array\", \"items\": { \"$ref\": \"#/content/tags\" } } What entries store Entries 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: --- title : My Post author : jane-doe tags : [ intro , tooling ] --- Anything other than a string (or array of strings, for to-many) fails the field's validation. Resolution References 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: { \"tagName\" : \"span\" , \"textContent\" : \"${state.post.data.author.data.name}\" } For a to-many field, each id in the array is replaced by its entry the same way. An 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. Beyond content The 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. Related Content collections — defining content types and their schemas Content types in Studio — building these schemas visually References — $ref inside documents, a different mechanism"},{"id":"docs:framework/site/relationships#relationships","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#relationships","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"Relationships","text":"Studio writes this format for you. The content-type builder creates reference fields visually — this page documents the schema they produce and how references resolve at build time. A 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."},{"id":"docs:framework/site/relationships#the-reference-form","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#the-reference-form","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"The reference form","text":"A reference field's schema points at the target content type with a #/content/<type> pointer: { \"content\" : { \"authors\" : { \"source\" : \"./content/authors/\" , \"format\" : \"Markdown\" }, \"blog\" : { \"source\" : \"./content/blog/\" , \"format\" : \"Markdown\" , \"schema\" : { \"type\" : \"object\" , \"properties\" : { \"title\" : { \"type\" : \"string\" }, \"author\" : { \"$ref\" : \"#/content/authors\" } } } } } } Cardinality is expressed with plain JSON Schema — there is no separate relationship syntax: Cardinality Field schema To-one { \"$ref\": \"#/content/authors\" } To-many { \"type\": \"array\", \"items\": { \"$ref\": \"#/content/tags\" } }"},{"id":"docs:framework/site/relationships#what-entries-store","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#what-entries-store","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"What entries store","text":"Entries 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: --- title : My Post author : jane-doe tags : [ intro , tooling ] --- Anything other than a string (or array of strings, for to-many) fails the field's validation."},{"id":"docs:framework/site/relationships#resolution","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#resolution","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"Resolution","text":"References 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: { \"tagName\" : \"span\" , \"textContent\" : \"${state.post.data.author.data.name}\" } For a to-many field, each id in the array is replaced by its entry the same way. An 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."},{"id":"docs:framework/site/relationships#beyond-content","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#beyond-content","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"Beyond content","text":"The 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."},{"id":"docs:framework/site/relationships#related","collection":"docs","slug":"framework/site/relationships","url":"/docs/framework/site/relationships/#related","title":"Relationships","description":"Link content entries to each other with #/content references: to-one and to-many fields, stored ids, and build-time resolution.","heading":"Related","text":"Content collections — defining content types and their schemas Content types in Studio — building these schemas visually References — $ref inside documents, a different mechanism"},{"id":"docs:framework/site/routing","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"","text":"Routing Every 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 ). Static routes The file path determines the URL path: File URL pages/index.json / pages/about.json /about pages/about/index.json /about pages/blog/index.json /blog pages/blog/first-post.json /blog/first-post 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. Files 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 . Dynamic routes Bracket syntax in filenames creates parameterized routes: File URL pattern Example match pages/blog/[slug].json /blog/:slug /blog/hello-world pages/[category]/[id].json /:category/:id /products/42 pages/docs/[...path].json /docs/* /docs/api/runtime/install [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. Generating pages with $paths A 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 — this is (abbreviated) how the docs page you are reading is generated, from pages/docs/[...slug].json : { \"$layout\" : \"./layouts/docs.json\" , \"$paths\" : { \"contentType\" : \"docs\" , \"param\" : \"slug\" }, \"state\" : { \"page\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"docs\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } }, \"children\" : [{ \"tagName\" : \"article\" , \"children\" : \"${state.page.$children ?? []}\" }] } The 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). $paths takes three shapes: { \"contentType\" : \"blog\" , \"param\" : \"slug\" , \"field\" : \"id\" } One page per collection entry. param names the route parameter (default slug ); field picks which entry field supplies the value (default id , the entry id). { \"values\" : [ \"en\" , \"fr\" , \"de\" ], \"param\" : \"lang\" } An explicit list of values. { \"$ref\" : \"./data/products.json\" , \"param\" : \"id\" , \"field\" : \"sku\" } A JSON array file, one page per item, with field selecting the property to use. For 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. Your 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 after changing extensions to keep that set current. Route priority When several routes could match the same URL, the more specific one wins: Static routes beat dynamic routes — /about beats /[slug] . Named parameters beat catch-alls — /blog/[slug] beats /blog/[...path] . More specific paths beat less specific ones — /blog/[slug] beats /[...path] . Params at runtime Inside 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: { \"id\" : { \"$ref\" : \"#/$params/slug\" } } Each 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 ). In static builds all of this resolves at compile time; nothing route-related ships to the browser. In Studio, opening a dynamic page shows a params picker in the tab bar so the canvas previews a real entry instead of a placeholder. Related Content collections — the entries that drive $paths Layouts — the shells dynamic pages render into Deployment — how generated routes are served"},{"id":"docs:framework/site/routing#routing","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#routing","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Routing","text":"Every 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 )."},{"id":"docs:framework/site/routing#static-routes","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#static-routes","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Static routes","text":"The file path determines the URL path: File URL pages/index.json / pages/about.json /about pages/about/index.json /about pages/blog/index.json /blog pages/blog/first-post.json /blog/first-post 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. Files 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 ."},{"id":"docs:framework/site/routing#dynamic-routes","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#dynamic-routes","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Dynamic routes","text":"Bracket syntax in filenames creates parameterized routes: File URL pattern Example match pages/blog/[slug].json /blog/:slug /blog/hello-world pages/[category]/[id].json /:category/:id /products/42 pages/docs/[...path].json /docs/* /docs/api/runtime/install [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."},{"id":"docs:framework/site/routing#generating-pages-with-paths","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#generating-pages-with-paths","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Generating pages with $paths","text":"A 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 — this is (abbreviated) how the docs page you are reading is generated, from pages/docs/[...slug].json : { \"$layout\" : \"./layouts/docs.json\" , \"$paths\" : { \"contentType\" : \"docs\" , \"param\" : \"slug\" }, \"state\" : { \"page\" : { \"$prototype\" : \"ContentEntry\" , \"contentType\" : \"docs\" , \"id\" : { \"$ref\" : \"#/$params/slug\" } } }, \"children\" : [{ \"tagName\" : \"article\" , \"children\" : \"${state.page.$children ?? []}\" }] } The 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). $paths takes three shapes: { \"contentType\" : \"blog\" , \"param\" : \"slug\" , \"field\" : \"id\" } One page per collection entry. param names the route parameter (default slug ); field picks which entry field supplies the value (default id , the entry id). { \"values\" : [ \"en\" , \"fr\" , \"de\" ], \"param\" : \"lang\" } An explicit list of values. { \"$ref\" : \"./data/products.json\" , \"param\" : \"id\" , \"field\" : \"sku\" } A JSON array file, one page per item, with field selecting the property to use. For 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. Your 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 after changing extensions to keep that set current."},{"id":"docs:framework/site/routing#route-priority","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#route-priority","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Route priority","text":"When several routes could match the same URL, the more specific one wins: Static routes beat dynamic routes — /about beats /[slug] . Named parameters beat catch-alls — /blog/[slug] beats /blog/[...path] . More specific paths beat less specific ones — /blog/[slug] beats /[...path] ."},{"id":"docs:framework/site/routing#params-at-runtime","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#params-at-runtime","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Params at runtime","text":"Inside 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: { \"id\" : { \"$ref\" : \"#/$params/slug\" } } Each 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 ). In static builds all of this resolves at compile time; nothing route-related ships to the browser. In Studio, opening a dynamic page shows a params picker in the tab bar so the canvas previews a real entry instead of a placeholder."},{"id":"docs:framework/site/routing#related","collection":"docs","slug":"framework/site/routing","url":"/docs/framework/site/routing/#related","title":"Routing","description":"File-based routing in Jx sites: static routes, [param] dynamic routes, [...path] catch-alls, $paths expansion, priority, and route params at runtime.","heading":"Related","text":"Content collections — the entries that drive $paths Layouts — the shells dynamic pages render into Deployment — how generated routes are served"},{"id":"docs:framework/site/search","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/","title":"Site search","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.","heading":"","text":"Site search Jx 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 ). Enabling the section Add the extension and a search section to project.json , then regenerate schemas with jx schema : { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/search\" ], \"search\" : { \"collections\" : { \"docs\" : { \"basePath\" : \"/docs/\" , \"boost\" : { \"title\" : 4 , \"heading\" : 2 } } } } } Per collection: Key Default Meaning basePath (required) URL prefix mapping entry ids to routes fields [\"title\", \"heading\", \"text\"] Document fields the index searches boost {} Per-field score boosts sections true Also index one document per heading, with #anchor deep links sectionDepth 3 Deepest heading level that gets its own section document Top-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). 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. Querying from page state The Search prototype gives any page reactive results with zero client wiring: \"state\" : { \"q\" : \"\" , \"results\" : { \"$prototype\" : \"Search\" , \"query\" : { \"$ref\" : \"#/state/q\" }, \"limit\" : 8 } } Bind an input to state.q and map over state.results . Each result group is a page with its matching sections: { \"slug\" : \"framework/site\" , \"title\" : \"Site architecture\" , \"url\" : \"/docs/framework/site/\" , \"score\" : 12.4 , \"hits\" : [{ \"heading\" : \"Assets\" , \"url\" : \"/docs/framework/site/#assets\" , \"score\" : 9.1 }] } In 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). Building a search UI component Interactive 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: \"state\" : { \"searchQuery\" : \"\" , \"searchResults\" : [], \"searchReady\" : false , \"searchActive\" : 0 , \"searchInit\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@jxsuite/search/client\" , \"parameters\" : [ \"state\" ] }, \"runSearch\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@jxsuite/search/client\" , \"parameters\" : [ \"state\" , \"e\" ] }, \"onMount\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"state\" ], \"body\" : \"state.searchInit(state);\" } } searchInit(state) preloads the index and flips state.searchReady (re-running any pending query). 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 . The 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. For full control, import the core API from the same module: preload(indexUrl?) , isReady() , and the synchronous query(text, { limit, group, pageCap }) . Rendering a result row Flat 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: { \"url\" : \"/docs/framework/site/#assets\" , \"title\" : \"Site architecture\" , \"heading\" : \"Assets\" , \"crumbs\" : [ \"Framework\" , \"Site architecture\" ], \"titleTokens\" : [{ \"t\" : \"Assets\" , \"m\" : true }], \"excerptTokens\" : [ { \"t\" : \"…referenced from a page are copied into \" , \"m\" : false }, { \"t\" : \"assets\" , \"m\" : true }, { \"t\" : \"/ at build time…\" , \"m\" : false } ], \"score\" : 9.1 } A 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"$map/item/titleTokens\" }, \"map\" : { \"tagName\" : \"span\" , \"attributes\" : { \"class\" : \"${item.m ? 'hl' : ''}\" }, \"textContent\" : \"${item.t}\" } } 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. Grouped 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. One 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."},{"id":"docs:framework/site/search#site-search","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/#site-search","title":"Site search","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.","heading":"Site search","text":"Jx 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 )."},{"id":"docs:framework/site/search#enabling-the-section","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/#enabling-the-section","title":"Site search","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.","heading":"Enabling the section","text":"Add the extension and a search section to project.json , then regenerate schemas with jx schema : { \"extensions\" : [ \"@jxsuite/parser\" , \"@jxsuite/search\" ], \"search\" : { \"collections\" : { \"docs\" : { \"basePath\" : \"/docs/\" , \"boost\" : { \"title\" : 4 , \"heading\" : 2 } } } } } Per collection: Key Default Meaning basePath (required) URL prefix mapping entry ids to routes fields [\"title\", \"heading\", \"text\"] Document fields the index searches boost {} Per-field score boosts sections true Also index one document per heading, with #anchor deep links sectionDepth 3 Deepest heading level that gets its own section document Top-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). 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."},{"id":"docs:framework/site/search#querying-from-page-state","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/#querying-from-page-state","title":"Site search","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.","heading":"Querying from page state","text":"The Search prototype gives any page reactive results with zero client wiring: \"state\" : { \"q\" : \"\" , \"results\" : { \"$prototype\" : \"Search\" , \"query\" : { \"$ref\" : \"#/state/q\" }, \"limit\" : 8 } } Bind an input to state.q and map over state.results . Each result group is a page with its matching sections: { \"slug\" : \"framework/site\" , \"title\" : \"Site architecture\" , \"url\" : \"/docs/framework/site/\" , \"score\" : 12.4 , \"hits\" : [{ \"heading\" : \"Assets\" , \"url\" : \"/docs/framework/site/#assets\" , \"score\" : 9.1 }] } In 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)."},{"id":"docs:framework/site/search#building-a-search-ui-component","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/#building-a-search-ui-component","title":"Site search","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.","heading":"Building a search UI component","text":"Interactive 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: \"state\" : { \"searchQuery\" : \"\" , \"searchResults\" : [], \"searchReady\" : false , \"searchActive\" : 0 , \"searchInit\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@jxsuite/search/client\" , \"parameters\" : [ \"state\" ] }, \"runSearch\" : { \"$prototype\" : \"Function\" , \"$src\" : \"npm:@jxsuite/search/client\" , \"parameters\" : [ \"state\" , \"e\" ] }, \"onMount\" : { \"$prototype\" : \"Function\" , \"arguments\" : [ \"state\" ], \"body\" : \"state.searchInit(state);\" } } searchInit(state) preloads the index and flips state.searchReady (re-running any pending query). 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 . The 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. For full control, import the core API from the same module: preload(indexUrl?) , isReady() , and the synchronous query(text, { limit, group, pageCap }) ."},{"id":"docs:framework/site/search#rendering-a-result-row","collection":"docs","slug":"framework/site/search","url":"/docs/framework/site/search/#rendering-a-result-row","title":"Site search","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.","heading":"Rendering a result row","text":"Flat 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: { \"url\" : \"/docs/framework/site/#assets\" , \"title\" : \"Site architecture\" , \"heading\" : \"Assets\" , \"crumbs\" : [ \"Framework\" , \"Site architecture\" ], \"titleTokens\" : [{ \"t\" : \"Assets\" , \"m\" : true }], \"excerptTokens\" : [ { \"t\" : \"…referenced from a page are copied into \" , \"m\" : false }, { \"t\" : \"assets\" , \"m\" : true }, { \"t\" : \"/ at build time…\" , \"m\" : false } ], \"score\" : 9.1 } A 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: { \"$prototype\" : \"Array\" , \"items\" : { \"$ref\" : \"$map/item/titleTokens\" }, \"map\" : { \"tagName\" : \"span\" , \"attributes\" : { \"class\" : \"${item.m ? 'hl' : ''}\" }, \"textContent\" : \"${item.t}\" } } 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. Grouped 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. One 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."},{"id":"docs:framework/site/seo","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"","text":"SEO and metadata Every 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. Page-level $head A page declares metadata as an array of head elements under $head , and its title as a top-level title property: { \"title\" : \"My Blog Post — My Site\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"A great blog post about things\" } } ] } Each 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. Templated metadata 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: { \"$paths\" : { \"contentType\" : \"docs\" , \"param\" : \"slug\" , \"field\" : \"id\" }, \"title\" : \"${state.page.data.title} — Jx Suite\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"${state.page.data.description}\" } } ] } The 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} . Merge order The build assembles each page's <head> from four layers, later entries winning: Built-in defaults — <meta charset> and a standard viewport tag Site — $head in project.json (favicon, fonts, global meta) Layout — the layout document's $head Page — the page's $head Duplicates are detected by element identity, so a page-level entry replaces the site-level one rather than appearing twice: Element Deduplication key <title> , <meta charset> singleton <meta name=\"...\"> name <meta property=\"...\"> property (Open Graph) <link rel=\"...\"> rel + href <script src=\"...\"> src Two 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). Sitemap When url is set in project.json , the build emits dist/sitemap.xml from the route table — one <url> per compiled page, with: <loc> — absolute, built from url + the route, identical to the page's canonical URL <lastmod> — the page source file's modification date ( YYYY-MM-DD ) Dynamic 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. To 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. robots.txt After 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. Related project.json — site-level $head , url , and defaults Layouts — where layout-level head entries come from Content collections — the entry data that feeds templated metadata"},{"id":"docs:framework/site/seo#seo-and-metadata","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#seo-and-metadata","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"SEO and metadata","text":"Every 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."},{"id":"docs:framework/site/seo#page-level-head","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#page-level-head","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"Page-level $head","text":"A page declares metadata as an array of head elements under $head , and its title as a top-level title property: { \"title\" : \"My Blog Post — My Site\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"A great blog post about things\" } } ] } Each 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."},{"id":"docs:framework/site/seo#templated-metadata","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#templated-metadata","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"Templated metadata","text":"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: { \"$paths\" : { \"contentType\" : \"docs\" , \"param\" : \"slug\" , \"field\" : \"id\" }, \"title\" : \"${state.page.data.title} — Jx Suite\" , \"$head\" : [ { \"tagName\" : \"meta\" , \"attributes\" : { \"name\" : \"description\" , \"content\" : \"${state.page.data.description}\" } } ] } The 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} ."},{"id":"docs:framework/site/seo#merge-order","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#merge-order","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"Merge order","text":"The build assembles each page's <head> from four layers, later entries winning: Built-in defaults — <meta charset> and a standard viewport tag Site — $head in project.json (favicon, fonts, global meta) Layout — the layout document's $head Page — the page's $head Duplicates are detected by element identity, so a page-level entry replaces the site-level one rather than appearing twice: Element Deduplication key <title> , <meta charset> singleton <meta name=\"...\"> name <meta property=\"...\"> property (Open Graph) <link rel=\"...\"> rel + href <script src=\"...\"> src Two 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)."},{"id":"docs:framework/site/seo#sitemap","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#sitemap","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"Sitemap","text":"When url is set in project.json , the build emits dist/sitemap.xml from the route table — one <url> per compiled page, with: <loc> — absolute, built from url + the route, identical to the page's canonical URL <lastmod> — the page source file's modification date ( YYYY-MM-DD ) Dynamic 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. To 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."},{"id":"docs:framework/site/seo#robotstxt","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#robotstxt","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"robots.txt","text":"After 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."},{"id":"docs:framework/site/seo#related","collection":"docs","slug":"framework/site/seo","url":"/docs/framework/site/seo/#related","title":"SEO and metadata","description":"Declare page metadata with $head, template it from state, and let the build merge heads and emit sitemap.xml and robots.txt.","heading":"Related","text":"project.json — site-level $head , url , and defaults Layouts — where layout-level head entries come from Content collections — the entry data that feeds templated metadata"},{"id":"docs:studio/ai/chat","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"","text":"The AI sidebar The 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. Open it Click 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. If no AI provider is set up yet, the sidebar shows the key form instead of a chat — see Connect a provider . Send a message Type in the message box at the bottom and press Enter to send — Shift+Enter inserts a newline. The box grows as you type, and the send button enables once there's something to send. The row under the message box holds the composer's controls: Attach context (paperclip) — pin the current page or the selected element to your message (below). The model picker — switch models mid-conversation; the list comes from your provider. API key & endpoint (gear) — reopen the provider form. Send — becomes Stop while the assistant is replying; click it to halt the reply and any further actions. Attach context The paperclip menu offers two attachments, each shown as a removable chip above the message box: Current page — the file open in the active tab. Selected element — the element currently selected on the canvas, identified by its tag and a snippet of its text. Attaching 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. Even 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. Watch it work The 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. Document 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. Review and undo edits What the assistant may change, and how you take it back, follows two rules: 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 ⌘Z (macOS) or 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. 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. For disk-level changes, source control is the review tool: the Source Control panel shows every file the assistant touched as a pending change you can diff or discard before committing. Chats and history The header names the current chat and holds two buttons: the history button (left) opens the Chats list, and + starts a new chat. Chats are titled after your first message and listed newest-first with a timestamp and message count. Click a chat to reopen it; the conversation continues where it left off. Hover a row and click the trash button to delete a chat. Deleting the open one leaves you in a fresh empty chat. When you reopen Studio, your last open chat is restored. History 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. Next How the assistant edits the open page: Document assistant What each message shares with your provider: AI assistant Where saved and unsaved files live: Tabs and files"},{"id":"docs:studio/ai/chat#the-ai-sidebar","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#the-ai-sidebar","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"The AI sidebar","text":"The 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."},{"id":"docs:studio/ai/chat#open-it","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#open-it","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Open it","text":"Click 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. If no AI provider is set up yet, the sidebar shows the key form instead of a chat — see Connect a provider ."},{"id":"docs:studio/ai/chat#send-a-message","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#send-a-message","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Send a message","text":"Type in the message box at the bottom and press Enter to send — Shift+Enter inserts a newline. The box grows as you type, and the send button enables once there's something to send. The row under the message box holds the composer's controls: Attach context (paperclip) — pin the current page or the selected element to your message (below). The model picker — switch models mid-conversation; the list comes from your provider. API key & endpoint (gear) — reopen the provider form. Send — becomes Stop while the assistant is replying; click it to halt the reply and any further actions."},{"id":"docs:studio/ai/chat#attach-context","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#attach-context","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Attach context","text":"The paperclip menu offers two attachments, each shown as a removable chip above the message box: Current page — the file open in the active tab. Selected element — the element currently selected on the canvas, identified by its tag and a snippet of its text. Attaching 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. Even 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."},{"id":"docs:studio/ai/chat#watch-it-work","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#watch-it-work","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Watch it work","text":"The 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. Document 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."},{"id":"docs:studio/ai/chat#review-and-undo-edits","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#review-and-undo-edits","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Review and undo edits","text":"What the assistant may change, and how you take it back, follows two rules: 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 ⌘Z (macOS) or 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. 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. For disk-level changes, source control is the review tool: the Source Control panel shows every file the assistant touched as a pending change you can diff or discard before committing."},{"id":"docs:studio/ai/chat#chats-and-history","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#chats-and-history","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Chats and history","text":"The header names the current chat and holds two buttons: the history button (left) opens the Chats list, and + starts a new chat. Chats are titled after your first message and listed newest-first with a timestamp and message count. Click a chat to reopen it; the conversation continues where it left off. Hover a row and click the trash button to delete a chat. Deleting the open one leaves you in a fresh empty chat. When you reopen Studio, your last open chat is restored. History 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."},{"id":"docs:studio/ai/chat#next","collection":"docs","slug":"studio/ai/chat","url":"/docs/studio/ai/chat/#next","title":"The AI sidebar","description":"The persistent chat sidebar — open it, attach context, watch the assistant's edits land, manage chat history, and review or undo its changes.","heading":"Next","text":"How the assistant edits the open page: Document assistant What each message shares with your provider: AI assistant Where saved and unsaved files live: Tabs and files"},{"id":"docs:studio/ai/document-assistant","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/","title":"Document assistant","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.","heading":"","text":"Document assistant The document assistant is what the AI sidebar 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. What it can do on the open page With a document active, the assistant edits the page the way you would, one change at a time: rewrite any text, restyle elements property by property — the same styles you'd set in the style inspector , change element properties: tags, classes, attributes, component options, add new elements, move them, and remove them, add or update the page's state entries — the same ones the State panel shows. Every 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 . Every edit is checked After 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. The rules it's checked against are your project's own — the same generated schemas the code editor 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. If 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. Undo and save Document edits follow Studio's normal editing rules: They go into the page's undo history as one step per request — ⌘Z (macOS) or 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. They live in the open editor, not on disk: the tab is marked unsaved until you save it, and closing without saving discards them. The full review workflow — including the file-level changes that don't work this way — is covered in Review and undo edits . Page open or not — which to choose Where the assistant works depends only on what's on the canvas, so you steer it by what you open: 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. 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. 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. A simple habit: if you'd want to press undo afterwards, open the page before you ask. Next The chat itself — composer, history, reviewing edits: The AI sidebar What the assistant can do in every state, and provider setup: AI assistant The state entries it adds are explained in the State panel"},{"id":"docs:studio/ai/document-assistant#document-assistant","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#document-assistant","title":"Document assistant","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.","heading":"Document assistant","text":"The document assistant is what the AI sidebar 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."},{"id":"docs:studio/ai/document-assistant#what-it-can-do-on-the-open-page","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#what-it-can-do-on-the-open-page","title":"Document assistant","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.","heading":"What it can do on the open page","text":"With a document active, the assistant edits the page the way you would, one change at a time: rewrite any text, restyle elements property by property — the same styles you'd set in the style inspector , change element properties: tags, classes, attributes, component options, add new elements, move them, and remove them, add or update the page's state entries — the same ones the State panel shows. Every 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 ."},{"id":"docs:studio/ai/document-assistant#every-edit-is-checked","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#every-edit-is-checked","title":"Document assistant","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.","heading":"Every edit is checked","text":"After 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. The rules it's checked against are your project's own — the same generated schemas the code editor 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. If 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."},{"id":"docs:studio/ai/document-assistant#undo-and-save","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#undo-and-save","title":"Document assistant","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.","heading":"Undo and save","text":"Document edits follow Studio's normal editing rules: They go into the page's undo history as one step per request — ⌘Z (macOS) or 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. They live in the open editor, not on disk: the tab is marked unsaved until you save it, and closing without saving discards them. The full review workflow — including the file-level changes that don't work this way — is covered in Review and undo edits ."},{"id":"docs:studio/ai/document-assistant#page-open-or-not-which-to-choose","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#page-open-or-not-which-to-choose","title":"Document assistant","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.","heading":"Page open or not — which to choose","text":"Where the assistant works depends only on what's on the canvas, so you steer it by what you open: 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. 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. 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. A simple habit: if you'd want to press undo afterwards, open the page before you ask."},{"id":"docs:studio/ai/document-assistant#next","collection":"docs","slug":"studio/ai/document-assistant","url":"/docs/studio/ai/document-assistant/#next","title":"Document assistant","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.","heading":"Next","text":"The chat itself — composer, history, reviewing edits: The AI sidebar What the assistant can do in every state, and provider setup: AI assistant The state entries it adds are explained in the State panel"},{"id":"docs:studio/data/auth-and-secrets","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/","title":"Auth and secrets","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.","heading":"","text":"Auth and secrets Two 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. Secrets: values stay put, names travel A 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: 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. 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. In Studio you meet this as the secret field : settings that hold something sensitive (a Supabase URL in Connections , the auth signing secret below) render as a password-style box. Paste the value and press 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. .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. The auth extension The @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. Turn it on Auth is an extension package, and it keeps its accounts in a database, so it builds on what Databases already set up: Have a connection. Add one in Connections if the project has none — the account tables live on one of them. Install the package. Type @jxsuite/auth into Settings > Dependencies and click Add (see Dependencies and imports ). 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 ; the array is described in project.json . Pick a server-capable adapter. In Project 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. Reopen Settings and an Authentication section is waiting. Filling it in writes an auth section into project.json . The Authentication settings section The section is a single form, and every field is optional — the defaults below are what applies when you leave one blank: connection — which of your connections stores the account tables. Defaults to the first one you declared. 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. methods — first-party sign-in methods; emailPassword (on by default) enables classic email + password accounts. 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. redirects — afterSignIn and afterSignOut : the paths a visitor lands on after each. Leave them empty and the page stays where it is. 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. trustedOrigins — extra origins allowed to call the sign-in routes; most sites leave this empty, since a site's own pages are always allowed. Account tables and roles Auth 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 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. The tables are created for you two ways: jx dev creates them the first time anything touches /_jx/auth , and Push Schema plans them as its own steps after the connector's, so a dry run shows them before anything runs. 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. What the rules mean Declaring auth is what makes table permissions beyond public / none work ( Data tables is where you set them): authenticated — any signed-in user. 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. role:<name> — users whose role matches. Without the auth extension these rules simply deny — the door fails closed, never open. Sign-in pages There 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 , alongside every other data source . 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. One 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. 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: Handler Reads signInEmail email , password signUpEmail email , password , and optionally name — without it, the part of the address before the @ is used signInSocial provider — or the default provider set on the state entry signOut nothing — wire it to a button's click An input named anything else is invisible to the handler, and the attempt goes out with an empty value. On 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. The wiring is two state entries and one property on the form. The Events panel picker only offers plain functions, so point the form at its handler in Code mode : { \"state\" : { \"session\" : { \"$prototype\" : \"Session\" , \"timing\" : \"client\" }, \"auth\" : { \"$prototype\" : \"AuthActions\" , \"timing\" : \"client\" } }, \"tagName\" : \"main\" , \"children\" : [ { \"tagName\" : \"form\" , \"onsubmit\" : { \"$ref\" : \"#/state/auth/signInEmail\" }, \"children\" : [ { \"tagName\" : \"input\" , \"attributes\" : { \"type\" : \"email\" , \"name\" : \"email\" , \"required\" : \"\" } }, { \"tagName\" : \"input\" , \"attributes\" : { \"type\" : \"password\" , \"name\" : \"password\" , \"required\" : \"\" } }, { \"tagName\" : \"button\" , \"textContent\" : \"Sign in\" } ] }, { \"tagName\" : \"p\" , \"textContent\" : \"${state.session ? state.session.user.email : 'Signed out'}\" } ] } 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 . Social sign-in A provider needs three things, in this order: 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 . 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. 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). Two 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. Current limits No emails are sent yet — no address verification and no password reset; email accounts work immediately after sign-up. A table with insert: \"public\" accepts writes from anyone on the internet. Prefer authenticated inserts unless you knowingly want an open drop-box. Sign-in errors aren't surfaced for you; a failed attempt leaves the form as it was. Next Data tables — put the permission rules on your tables Data grid — manage users and roles by editing rows project.json — the auth , connections , and data sections as committed config Security — what the server enforces, and what it never trusts"},{"id":"docs:studio/data/auth-and-secrets#auth-and-secrets","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#auth-and-secrets","title":"Auth and secrets","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.","heading":"Auth and secrets","text":"Two 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."},{"id":"docs:studio/data/auth-and-secrets#secrets-values-stay-put-names-travel","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#secrets-values-stay-put-names-travel","title":"Auth and secrets","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.","heading":"Secrets: values stay put, names travel","text":"A 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: 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. 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. In Studio you meet this as the secret field : settings that hold something sensitive (a Supabase URL in Connections , the auth signing secret below) render as a password-style box. Paste the value and press 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. .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."},{"id":"docs:studio/data/auth-and-secrets#the-auth-extension","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#the-auth-extension","title":"Auth and secrets","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.","heading":"The auth extension","text":"The @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."},{"id":"docs:studio/data/auth-and-secrets#turn-it-on","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#turn-it-on","title":"Auth and secrets","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.","heading":"Turn it on","text":"Auth is an extension package, and it keeps its accounts in a database, so it builds on what Databases already set up: Have a connection. Add one in Connections if the project has none — the account tables live on one of them. Install the package. Type @jxsuite/auth into Settings > Dependencies and click Add (see Dependencies and imports ). 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 ; the array is described in project.json . Pick a server-capable adapter. In Project 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. Reopen Settings and an Authentication section is waiting. Filling it in writes an auth section into project.json ."},{"id":"docs:studio/data/auth-and-secrets#the-authentication-settings-section","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#the-authentication-settings-section","title":"Auth and secrets","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.","heading":"The Authentication settings section","text":"The section is a single form, and every field is optional — the defaults below are what applies when you leave one blank: connection — which of your connections stores the account tables. Defaults to the first one you declared. 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. methods — first-party sign-in methods; emailPassword (on by default) enables classic email + password accounts. 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. redirects — afterSignIn and afterSignOut : the paths a visitor lands on after each. Leave them empty and the page stays where it is. 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. trustedOrigins — extra origins allowed to call the sign-in routes; most sites leave this empty, since a site's own pages are always allowed."},{"id":"docs:studio/data/auth-and-secrets#account-tables-and-roles","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#account-tables-and-roles","title":"Auth and secrets","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.","heading":"Account tables and roles","text":"Auth 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 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. The tables are created for you two ways: jx dev creates them the first time anything touches /_jx/auth , and Push Schema plans them as its own steps after the connector's, so a dry run shows them before anything runs. 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."},{"id":"docs:studio/data/auth-and-secrets#what-the-rules-mean","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#what-the-rules-mean","title":"Auth and secrets","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.","heading":"What the rules mean","text":"Declaring auth is what makes table permissions beyond public / none work ( Data tables is where you set them): authenticated — any signed-in user. 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. role:<name> — users whose role matches. Without the auth extension these rules simply deny — the door fails closed, never open."},{"id":"docs:studio/data/auth-and-secrets#sign-in-pages","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#sign-in-pages","title":"Auth and secrets","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.","heading":"Sign-in pages","text":"There 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 , alongside every other data source . 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. One 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. 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: Handler Reads signInEmail email , password signUpEmail email , password , and optionally name — without it, the part of the address before the @ is used signInSocial provider — or the default provider set on the state entry signOut nothing — wire it to a button's click An input named anything else is invisible to the handler, and the attempt goes out with an empty value. On 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. The wiring is two state entries and one property on the form. The Events panel picker only offers plain functions, so point the form at its handler in Code mode : { \"state\" : { \"session\" : { \"$prototype\" : \"Session\" , \"timing\" : \"client\" }, \"auth\" : { \"$prototype\" : \"AuthActions\" , \"timing\" : \"client\" } }, \"tagName\" : \"main\" , \"children\" : [ { \"tagName\" : \"form\" , \"onsubmit\" : { \"$ref\" : \"#/state/auth/signInEmail\" }, \"children\" : [ { \"tagName\" : \"input\" , \"attributes\" : { \"type\" : \"email\" , \"name\" : \"email\" , \"required\" : \"\" } }, { \"tagName\" : \"input\" , \"attributes\" : { \"type\" : \"password\" , \"name\" : \"password\" , \"required\" : \"\" } }, { \"tagName\" : \"button\" , \"textContent\" : \"Sign in\" } ] }, { \"tagName\" : \"p\" , \"textContent\" : \"${state.session ? state.session.user.email : 'Signed out'}\" } ] } 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 ."},{"id":"docs:studio/data/auth-and-secrets#social-sign-in","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#social-sign-in","title":"Auth and secrets","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.","heading":"Social sign-in","text":"A provider needs three things, in this order: 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 . 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. 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). Two 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."},{"id":"docs:studio/data/auth-and-secrets#current-limits","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#current-limits","title":"Auth and secrets","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.","heading":"Current limits","text":"No emails are sent yet — no address verification and no password reset; email accounts work immediately after sign-up. A table with insert: \"public\" accepts writes from anyone on the internet. Prefer authenticated inserts unless you knowingly want an open drop-box. Sign-in errors aren't surfaced for you; a failed attempt leaves the form as it was."},{"id":"docs:studio/data/auth-and-secrets#next","collection":"docs","slug":"studio/data/auth-and-secrets","url":"/docs/studio/data/auth-and-secrets/#next","title":"Auth and secrets","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.","heading":"Next","text":"Data tables — put the permission rules on your tables Data grid — manage users and roles by editing rows project.json — the auth , connections , and data sections as committed config Security — what the server enforces, and what it never trusts"},{"id":"docs:studio/data/connections","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/","title":"Connections","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.","heading":"","text":"Connections A 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. Add a connection Click New Entry at the bottom of the list. Type a name — main is a fine first choice — and click Create . The 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. To 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. SQLite — a file in your project The zero-setup option, and the local stand-in the others build on. file — where the database file lives, relative to the project root. Leave it empty for the default, .jx/data/<name>.sqlite . Cloudflare D1 — provider d1 Cloudflare's hosted SQLite, for sites deployed to Workers or Pages (see Publish to Cloudflare ): binding — the name your deployed site reaches the database under. databaseId — the database's UUID from the Cloudflare dashboard. An identifier, not a secret. accountId — your Cloudflare account id; leave it empty to use the CLOUDFLARE_ACCOUNT_ID environment variable. While 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. Supabase — provider supabase A hosted Postgres service: 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. hyperdriveId and binding — for sites deployed to Cloudflare Workers, the Hyperdrive configuration that fronts the Postgres connection there. Leave both empty otherwise. Names on file, values in the vault Nothing 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 . Test a connection Select 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. With a connection selected, Push Schema in the same row is scoped to just that connection — table schemas and pushing are covered in Data tables . This 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 ), the entries remain editable but the Test, Push, and grid actions are hidden. Next Data tables — define what's in the database Auth and secrets — where the secret values actually live"},{"id":"docs:studio/data/connections#connections","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#connections","title":"Connections","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.","heading":"Connections","text":"A 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."},{"id":"docs:studio/data/connections#add-a-connection","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#add-a-connection","title":"Connections","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.","heading":"Add a connection","text":"Click New Entry at the bottom of the list. Type a name — main is a fine first choice — and click Create . The 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. To 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."},{"id":"docs:studio/data/connections#sqlite-a-file-in-your-project","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#sqlite-a-file-in-your-project","title":"Connections","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.","heading":"SQLite — a file in your project","text":"The zero-setup option, and the local stand-in the others build on. file — where the database file lives, relative to the project root. Leave it empty for the default, .jx/data/<name>.sqlite ."},{"id":"docs:studio/data/connections#cloudflare-d1-provider-d1","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#cloudflare-d1-provider-d1","title":"Connections","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.","heading":"Cloudflare D1 — provider d1","text":"Cloudflare's hosted SQLite, for sites deployed to Workers or Pages (see Publish to Cloudflare ): binding — the name your deployed site reaches the database under. databaseId — the database's UUID from the Cloudflare dashboard. An identifier, not a secret. accountId — your Cloudflare account id; leave it empty to use the CLOUDFLARE_ACCOUNT_ID environment variable. While 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."},{"id":"docs:studio/data/connections#supabase-provider-supabase","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#supabase-provider-supabase","title":"Connections","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.","heading":"Supabase — provider supabase","text":"A hosted Postgres service: 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. hyperdriveId and binding — for sites deployed to Cloudflare Workers, the Hyperdrive configuration that fronts the Postgres connection there. Leave both empty otherwise."},{"id":"docs:studio/data/connections#names-on-file-values-in-the-vault","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#names-on-file-values-in-the-vault","title":"Connections","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.","heading":"Names on file, values in the vault","text":"Nothing 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 ."},{"id":"docs:studio/data/connections#test-a-connection","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#test-a-connection","title":"Connections","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.","heading":"Test a connection","text":"Select 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. With a connection selected, Push Schema in the same row is scoped to just that connection — table schemas and pushing are covered in Data tables . This 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 ), the entries remain editable but the Test, Push, and grid actions are hidden."},{"id":"docs:studio/data/connections#next","collection":"docs","slug":"studio/data/connections","url":"/docs/studio/data/connections/#next","title":"Connections","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.","heading":"Next","text":"Data tables — define what's in the database Auth and secrets — where the secret values actually live"},{"id":"docs:studio/data/grid","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/","title":"Data grid","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.","heading":"","text":"Data grid The data grid is Grid mode 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. Open a table Click 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 , then come back. Each table opens as its own tab, so you can keep a grid alongside the page you're building. Read the columns Columns 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. Page through rows Big 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. Edit, add, delete Editing works exactly as in Grid mode — double-click a cell, edit ranges, Fill Down , Replace , undo and redo — and nothing touches the database until Save : Add Row appends a pending row; fill in its cells (required fields must be provided). Delete Rows marks the selected rows; they're removed on save, after a confirmation. Save ( ⌘S / Ctrl+S ) writes the whole batch — the button counts your pending changes. On 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. Database 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. Where it's different from file grids File grids (collections, pages, CSV) Data grid Rows are files in your project rows in a database Loaded whole 50 per page Save writes files (frontmatter, CSV cells) one database write per row Conflict safety stale-file detection per-row database errors Undo after save revert the file in Source control none — the write is live User accounts in the grid With 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 . Next Grid mode — the shared spreadsheet surface in full: ranges, fill down, find & replace Data tables — change what columns a table has"},{"id":"docs:studio/data/grid#data-grid","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#data-grid","title":"Data grid","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.","heading":"Data grid","text":"The data grid is Grid mode 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."},{"id":"docs:studio/data/grid#open-a-table","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#open-a-table","title":"Data grid","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.","heading":"Open a table","text":"Click 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 , then come back. Each table opens as its own tab, so you can keep a grid alongside the page you're building."},{"id":"docs:studio/data/grid#read-the-columns","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#read-the-columns","title":"Data grid","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.","heading":"Read the columns","text":"Columns 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."},{"id":"docs:studio/data/grid#page-through-rows","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#page-through-rows","title":"Data grid","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.","heading":"Page through rows","text":"Big 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."},{"id":"docs:studio/data/grid#edit-add-delete","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#edit-add-delete","title":"Data grid","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.","heading":"Edit, add, delete","text":"Editing works exactly as in Grid mode — double-click a cell, edit ranges, Fill Down , Replace , undo and redo — and nothing touches the database until Save : Add Row appends a pending row; fill in its cells (required fields must be provided). Delete Rows marks the selected rows; they're removed on save, after a confirmation. Save ( ⌘S / Ctrl+S ) writes the whole batch — the button counts your pending changes. On 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. Database 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."},{"id":"docs:studio/data/grid#where-its-different-from-file-grids","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#where-its-different-from-file-grids","title":"Data grid","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.","heading":"Where it's different from file grids","text":"File grids (collections, pages, CSV) Data grid Rows are files in your project rows in a database Loaded whole 50 per page Save writes files (frontmatter, CSV cells) one database write per row Conflict safety stale-file detection per-row database errors Undo after save revert the file in Source control none — the write is live"},{"id":"docs:studio/data/grid#user-accounts-in-the-grid","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#user-accounts-in-the-grid","title":"Data grid","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.","heading":"User accounts in the grid","text":"With 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 ."},{"id":"docs:studio/data/grid#next","collection":"docs","slug":"studio/data/grid","url":"/docs/studio/data/grid/#next","title":"Data grid","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.","heading":"Next","text":"Grid mode — the shared spreadsheet surface in full: ranges, fill down, find & replace Data tables — change what columns a table has"},{"id":"docs:studio/data/tables","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/","title":"Data tables","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.","heading":"","text":"Data tables A 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. Create a table Click New Entry at the bottom of the table list. Type a name — \"Comments\" becomes comments — and click Create . The new table starts empty, readable by everyone and writable by no one. First pick its connection — a dropdown of the entries from Connections . Build the fields The schema editor is the same visual field builder as 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. A 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 . Under 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. Table options id — how rows are identified: uuid (random text ids, the default) or integer (1, 2, 3…). timestamps — on by default; every row gets created_at and updated_at columns the server maintains for you. indexes — column names to index for faster lookups; an inner list makes one composite index. 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 . ownerField — the column that records which user a row belongs to; required for owner rules, and stamped automatically on signed-in inserts. Push the schema Defining a table describes it; pushing creates it. Click Push Schema in the action row: 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. 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. From the Data Tables section a push covers every connection; from the Connections section, selecting a connection first scopes the push to it. Pushes 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. A 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 . 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. Using tables from pages Rows never pass through your project files — pages talk to the tables live. In the State panel , the connector's sources appear in the + Add… picker alongside the built-in data sources : 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. 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. 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. Filtering, sorting, and paging all happen in the database rather than in the browser, so a query with a limit fetches only that many rows. Compiled 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. In the JSON, a route parameter binds as { \"$ref\": \"#/$params/id\" } (the same way a dynamic route 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 today — the Events panel picker offers plain functions only. To see and fix the rows by hand, open the Data grid . Next Data grid — browse, insert, and edit rows Auth and secrets — make permission rules beyond public work"},{"id":"docs:studio/data/tables#data-tables","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#data-tables","title":"Data tables","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.","heading":"Data tables","text":"A 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."},{"id":"docs:studio/data/tables#create-a-table","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#create-a-table","title":"Data tables","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.","heading":"Create a table","text":"Click New Entry at the bottom of the table list. Type a name — \"Comments\" becomes comments — and click Create . The new table starts empty, readable by everyone and writable by no one. First pick its connection — a dropdown of the entries from Connections ."},{"id":"docs:studio/data/tables#build-the-fields","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#build-the-fields","title":"Data tables","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.","heading":"Build the fields","text":"The schema editor is the same visual field builder as 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. A 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 . Under 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."},{"id":"docs:studio/data/tables#table-options","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#table-options","title":"Data tables","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.","heading":"Table options","text":"id — how rows are identified: uuid (random text ids, the default) or integer (1, 2, 3…). timestamps — on by default; every row gets created_at and updated_at columns the server maintains for you. indexes — column names to index for faster lookups; an inner list makes one composite index. 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 . ownerField — the column that records which user a row belongs to; required for owner rules, and stamped automatically on signed-in inserts."},{"id":"docs:studio/data/tables#push-the-schema","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#push-the-schema","title":"Data tables","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.","heading":"Push the schema","text":"Defining a table describes it; pushing creates it. Click Push Schema in the action row: 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. 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. From the Data Tables section a push covers every connection; from the Connections section, selecting a connection first scopes the push to it. Pushes 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. A 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 . 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."},{"id":"docs:studio/data/tables#using-tables-from-pages","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#using-tables-from-pages","title":"Data tables","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.","heading":"Using tables from pages","text":"Rows never pass through your project files — pages talk to the tables live. In the State panel , the connector's sources appear in the + Add… picker alongside the built-in data sources : 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. 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. 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. Filtering, sorting, and paging all happen in the database rather than in the browser, so a query with a limit fetches only that many rows. Compiled 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. In the JSON, a route parameter binds as { \"$ref\": \"#/$params/id\" } (the same way a dynamic route 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 today — the Events panel picker offers plain functions only. To see and fix the rows by hand, open the Data grid ."},{"id":"docs:studio/data/tables#next","collection":"docs","slug":"studio/data/tables","url":"/docs/studio/data/tables/#next","title":"Data tables","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.","heading":"Next","text":"Data grid — browse, insert, and edit rows Auth and secrets — make permission rules beyond public work"},{"id":"docs:studio/design/breakpoints","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"","text":"Breakpoints A 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. The active breakpoint Styling always targets one breakpoint at a time. Two controls stay in sync: The breakpoint tabs at the top of the Style inspector — Base plus one tab per breakpoint. The panel headers on the canvas — click a panel's header to make its breakpoint active. Pick Base to edit the styles that apply everywhere; pick a breakpoint to edit that screen size's overrides. How the cascade works Base is the design; breakpoints are exceptions to it: Values on the Base tab apply at every size. On a breakpoint tab, everything inherited from earlier in the cascade shows as a dimmed placeholder — nothing is duplicated. Set a value on a breakpoint tab and it becomes an override for that breakpoint only, marked with a set-dot. Click the dot to remove the override; the inherited value shows through again. Breakpoints layer in the same order a browser applies their media queries, so what you see per panel is exactly what ships. Define your breakpoints Breakpoints 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. A single file can also carry breakpoints of its own: select the page root in Layers and open the Media section of the Properties panel . 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 . Only 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. A 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 does on the published site. The control appears only when the project declares a scheme breakpoint. Breakpoints 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 . Next Use the breakpoint tabs while styling in the Style inspector Element defaults respond to breakpoints too — Stylebook"},{"id":"docs:studio/design/breakpoints#breakpoints","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/#breakpoints","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"Breakpoints","text":"A 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."},{"id":"docs:studio/design/breakpoints#the-active-breakpoint","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/#the-active-breakpoint","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"The active breakpoint","text":"Styling always targets one breakpoint at a time. Two controls stay in sync: The breakpoint tabs at the top of the Style inspector — Base plus one tab per breakpoint. The panel headers on the canvas — click a panel's header to make its breakpoint active. Pick Base to edit the styles that apply everywhere; pick a breakpoint to edit that screen size's overrides."},{"id":"docs:studio/design/breakpoints#how-the-cascade-works","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/#how-the-cascade-works","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"How the cascade works","text":"Base is the design; breakpoints are exceptions to it: Values on the Base tab apply at every size. On a breakpoint tab, everything inherited from earlier in the cascade shows as a dimmed placeholder — nothing is duplicated. Set a value on a breakpoint tab and it becomes an override for that breakpoint only, marked with a set-dot. Click the dot to remove the override; the inherited value shows through again. Breakpoints layer in the same order a browser applies their media queries, so what you see per panel is exactly what ships."},{"id":"docs:studio/design/breakpoints#define-your-breakpoints","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/#define-your-breakpoints","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"Define your breakpoints","text":"Breakpoints 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. A single file can also carry breakpoints of its own: select the page root in Layers and open the Media section of the Properties panel . 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 . Only 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. A 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 does on the published site. The control appears only when the project declares a scheme breakpoint. Breakpoints 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 ."},{"id":"docs:studio/design/breakpoints#next","collection":"docs","slug":"studio/design/breakpoints","url":"/docs/studio/design/breakpoints/#next","title":"Breakpoints","description":"Breakpoints in Jx Studio: one live canvas per screen size, defining breakpoints in Settings, and how base styles cascade into overrides.","heading":"Next","text":"Use the breakpoint tabs while styling in the Style inspector Element defaults respond to breakpoints too — Stylebook"},{"id":"docs:studio/design/components","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/","title":"Working with components","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.","heading":"","text":"Working with components A 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 . Convert a selection into a component The natural way to make a component is to design it in place first, then promote it: Build the element on the canvas — structure, styles, content. Right-click it (on the canvas or in Layers ) and choose Convert to Component . 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. Studio 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 — drop more instances anywhere from there. If the component's slots need attention, Studio says so in the status bar. To 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. Props: the component's options Props 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 for declaring state. On an instance, the Properties panel 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. Slots: openings for content By 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 , slots show a ▣ badge; hover it for the slot's name. Layouts distribute page content the same way. Preview with test props A 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. Test 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 . A 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 . Next Repeat a component per item of a list — Repeaters Insert instances from the Elements panel"},{"id":"docs:studio/design/components#working-with-components","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#working-with-components","title":"Working with components","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.","heading":"Working with components","text":"A 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 ."},{"id":"docs:studio/design/components#convert-a-selection-into-a-component","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#convert-a-selection-into-a-component","title":"Working with components","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.","heading":"Convert a selection into a component","text":"The natural way to make a component is to design it in place first, then promote it: Build the element on the canvas — structure, styles, content. Right-click it (on the canvas or in Layers ) and choose Convert to Component . 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. Studio 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 — drop more instances anywhere from there. If the component's slots need attention, Studio says so in the status bar. To 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."},{"id":"docs:studio/design/components#props-the-components-options","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#props-the-components-options","title":"Working with components","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.","heading":"Props: the component's options","text":"Props 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 for declaring state. On an instance, the Properties panel 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."},{"id":"docs:studio/design/components#slots-openings-for-content","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#slots-openings-for-content","title":"Working with components","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.","heading":"Slots: openings for content","text":"By 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 , slots show a ▣ badge; hover it for the slot's name. Layouts distribute page content the same way."},{"id":"docs:studio/design/components#preview-with-test-props","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#preview-with-test-props","title":"Working with components","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.","heading":"Preview with test props","text":"A 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. Test 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 . A 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 ."},{"id":"docs:studio/design/components#next","collection":"docs","slug":"studio/design/components","url":"/docs/studio/design/components/#next","title":"Working with components","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.","heading":"Next","text":"Repeat a component per item of a list — Repeaters Insert instances from the Elements panel"},{"id":"docs:studio/design/elements","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/","title":"Elements panel","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.","heading":"","text":"Elements panel Elements 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. What's in the palette Components — at the top, your project's own components, plus components from installed packages once they're enabled for the project (see Dependencies ). Project components render a live preview on their card, so you see the real thing before you place it. 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. Type 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. Insert by click On the canvas or in Layers , select the element you want to insert into. Click a card. The new element lands inside the selection, after its existing children. With nothing selected, the element is added at the end of the document. Insert by drag For an exact placement, drag the card instead: Press and drag any card toward the canvas. As you move over the page, an indicator line shows where the element will land — before, after, or inside the element under the cursor. Drop to insert, or press Esc to cancel. The new element arrives selected, ready for the Properties panel and 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 . Inserting adds one element node to the open file — a component card adds an instance tag like <site-card> . The format is described in Components . Next Make your own cards appear here — Working with components Rearrange what you inserted in the Layers panel"},{"id":"docs:studio/design/elements#elements-panel","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/#elements-panel","title":"Elements panel","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.","heading":"Elements panel","text":"Elements 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."},{"id":"docs:studio/design/elements#whats-in-the-palette","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/#whats-in-the-palette","title":"Elements panel","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.","heading":"What's in the palette","text":"Components — at the top, your project's own components, plus components from installed packages once they're enabled for the project (see Dependencies ). Project components render a live preview on their card, so you see the real thing before you place it. 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. Type 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."},{"id":"docs:studio/design/elements#insert-by-click","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/#insert-by-click","title":"Elements panel","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.","heading":"Insert by click","text":"On the canvas or in Layers , select the element you want to insert into. Click a card. The new element lands inside the selection, after its existing children. With nothing selected, the element is added at the end of the document."},{"id":"docs:studio/design/elements#insert-by-drag","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/#insert-by-drag","title":"Elements panel","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.","heading":"Insert by drag","text":"For an exact placement, drag the card instead: Press and drag any card toward the canvas. As you move over the page, an indicator line shows where the element will land — before, after, or inside the element under the cursor. Drop to insert, or press Esc to cancel. The new element arrives selected, ready for the Properties panel and 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 . Inserting adds one element node to the open file — a component card adds an instance tag like <site-card> . The format is described in Components ."},{"id":"docs:studio/design/elements#next","collection":"docs","slug":"studio/design/elements","url":"/docs/studio/design/elements/#next","title":"Elements panel","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.","heading":"Next","text":"Make your own cards appear here — Working with components Rearrange what you inserted in the Layers panel"},{"id":"docs:studio/design/layers","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/","title":"Layers panel","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.","heading":"","text":"Layers panel Layers 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. Read the tree Each row shows a badge and a name. The badge tells you what the row is: The element's type — div , h2 , img , and so on. ↻ — a repeater , an element that renders once per item of a list. ⇄ — a switch, an element that shows one of several cases; each case appears as a child row named after the case. ▣ — a slot, the placeholder where a component or layout receives content. Hover it to see the slot's name. Text 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. Select and navigate Click a row to select that element. The canvas pans to bring it into view, and the Properties panel and 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. Rename an element Rows 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 Enter . Press Esc to cancel, or commit an empty name to go back to the automatic label. The 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. Rearrange the page Hover a row and its actions appear on the right: Move up / Move down arrows swap the element with its neighbors. The right arrow moves the element inside the sibling above it (shown when that sibling can hold children). The left arrow moves the element out of its parent, to sit just after it. ✕ deletes the element and everything inside it. For 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 Esc mid-drag to cancel with nothing changed. Dropping an element into itself or its own descendants is blocked. Right-click for everything else Right-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 . Set Title in that menu starts the same rename as double-clicking. In Stylebook mode the Layers activity switches to the element catalog instead of the document tree — see Stylebook . Next Add new elements from the Elements panel Inspect what you selected in the Properties panel"},{"id":"docs:studio/design/layers#layers-panel","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#layers-panel","title":"Layers panel","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.","heading":"Layers panel","text":"Layers 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."},{"id":"docs:studio/design/layers#read-the-tree","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#read-the-tree","title":"Layers panel","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.","heading":"Read the tree","text":"Each row shows a badge and a name. The badge tells you what the row is: The element's type — div , h2 , img , and so on. ↻ — a repeater , an element that renders once per item of a list. ⇄ — a switch, an element that shows one of several cases; each case appears as a child row named after the case. ▣ — a slot, the placeholder where a component or layout receives content. Hover it to see the slot's name. Text 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."},{"id":"docs:studio/design/layers#select-and-navigate","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#select-and-navigate","title":"Layers panel","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.","heading":"Select and navigate","text":"Click a row to select that element. The canvas pans to bring it into view, and the Properties panel and 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."},{"id":"docs:studio/design/layers#rename-an-element","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#rename-an-element","title":"Layers panel","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.","heading":"Rename an element","text":"Rows 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 Enter . Press Esc to cancel, or commit an empty name to go back to the automatic label. The 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."},{"id":"docs:studio/design/layers#rearrange-the-page","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#rearrange-the-page","title":"Layers panel","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.","heading":"Rearrange the page","text":"Hover a row and its actions appear on the right: Move up / Move down arrows swap the element with its neighbors. The right arrow moves the element inside the sibling above it (shown when that sibling can hold children). The left arrow moves the element out of its parent, to sit just after it. ✕ deletes the element and everything inside it. For 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 Esc mid-drag to cancel with nothing changed. Dropping an element into itself or its own descendants is blocked."},{"id":"docs:studio/design/layers#right-click-for-everything-else","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#right-click-for-everything-else","title":"Layers panel","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.","heading":"Right-click for everything else","text":"Right-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 . Set Title in that menu starts the same rename as double-clicking. In Stylebook mode the Layers activity switches to the element catalog instead of the document tree — see Stylebook ."},{"id":"docs:studio/design/layers#next","collection":"docs","slug":"studio/design/layers","url":"/docs/studio/design/layers/#next","title":"Layers panel","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.","heading":"Next","text":"Add new elements from the Elements panel Inspect what you selected in the Properties panel"},{"id":"docs:studio/design/properties","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"","text":"Properties panel Properties 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 , then click Properties at the top of the right panel. The Element section Every element starts with the same basics: Tag — the element's type. Change it to turn a div into a section , a p into an h2 . ID and Class — the element's identifier and CSS classes. Text Content — the element's text, shown when it has no child elements. Hidden — a checkbox that removes the element from the rendered page without deleting it. A small dot appears next to any field that has a value; click the dot to clear it. Attribute sections Below 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. Links 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. Anything not covered lives in the Custom section: click + Add attribute to add any attribute by name, edit its value inline, or remove it with ✕ . Component props When the selection is a component instance, a Component Props section lists the options the component exposes — see Working with 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. Make any value dynamic Most 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 . Page and root settings Select the page root — the topmost row in Layers — and extra sections appear: Page (site pages only) — pick the page's Layout : the site default, none, or any layout in the project. See Pages, layouts, components . Media — the file's breakpoints. Covered in Breakpoints . Special elements swap in their own sections too: a repeater shows a Repeater section (see Repeaters ), and a switch element lists its cases with an expression field. Every 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 . Next Style the same selection in the Style inspector Bind props and attributes to real data in Script & logic"},{"id":"docs:studio/design/properties#properties-panel","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#properties-panel","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Properties panel","text":"Properties 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 , then click Properties at the top of the right panel."},{"id":"docs:studio/design/properties#the-element-section","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#the-element-section","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"The Element section","text":"Every element starts with the same basics: Tag — the element's type. Change it to turn a div into a section , a p into an h2 . ID and Class — the element's identifier and CSS classes. Text Content — the element's text, shown when it has no child elements. Hidden — a checkbox that removes the element from the rendered page without deleting it. A small dot appears next to any field that has a value; click the dot to clear it."},{"id":"docs:studio/design/properties#attribute-sections","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#attribute-sections","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Attribute sections","text":"Below 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. Links 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. Anything not covered lives in the Custom section: click + Add attribute to add any attribute by name, edit its value inline, or remove it with ✕ ."},{"id":"docs:studio/design/properties#component-props","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#component-props","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Component props","text":"When the selection is a component instance, a Component Props section lists the options the component exposes — see Working with 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."},{"id":"docs:studio/design/properties#make-any-value-dynamic","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#make-any-value-dynamic","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Make any value dynamic","text":"Most 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 ."},{"id":"docs:studio/design/properties#page-and-root-settings","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#page-and-root-settings","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Page and root settings","text":"Select the page root — the topmost row in Layers — and extra sections appear: Page (site pages only) — pick the page's Layout : the site default, none, or any layout in the project. See Pages, layouts, components . Media — the file's breakpoints. Covered in Breakpoints . Special elements swap in their own sections too: a repeater shows a Repeater section (see Repeaters ), and a switch element lists its cases with an expression field. Every 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 ."},{"id":"docs:studio/design/properties#next","collection":"docs","slug":"studio/design/properties","url":"/docs/studio/design/properties/#next","title":"Properties panel","description":"The Properties panel in Jx Studio: edit element attributes, link targets, component props, and page settings for the selected element.","heading":"Next","text":"Style the same selection in the Style inspector Bind props and attributes to real data in Script & logic"},{"id":"docs:studio/design/repeaters","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/","title":"Repeaters","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.","heading":"","text":"Repeaters A 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. Turn an element into a repeater Design one instance of the repeating thing — one card, one row, one gallery tile. Right-click it (on the canvas or in Layers ) and choose Repeat… . In the dialog, pick the Items source — a list from the document's state, or Create new… to declare a fresh one by name. Optionally pick a Filter or Sort function, if the document defines any. Click Create Repeater . Your 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. Bind the list The items source is what makes a repeater useful. It can be: A plain list in state — start with Create new… and fill in items later in the State panel. A content collection — every blog post, product, or team member in your project's content. See Content types . A data source — anything that produces a list, like a Request fetching from an API. Declaring and feeding these lives in Script & logic . With the repeater selected, the Properties panel shows a Repeater section where you can rebind Items and add or change Filter and Sort after the fact. Edit the template Select 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 ). Inside the template, each item's data is in scope: 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. In the Properties panel, binding menus offer the current item and its position wherever a $ref binding is available. Bind the template's text and attributes to item fields and every rendered copy fills itself in from its own item. The 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 ) or use data-driven styling. A 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 . Next Feed repeaters from collections — Content types Make the template a reusable component — Working with components"},{"id":"docs:studio/design/repeaters#repeaters","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/#repeaters","title":"Repeaters","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.","heading":"Repeaters","text":"A 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."},{"id":"docs:studio/design/repeaters#turn-an-element-into-a-repeater","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/#turn-an-element-into-a-repeater","title":"Repeaters","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.","heading":"Turn an element into a repeater","text":"Design one instance of the repeating thing — one card, one row, one gallery tile. Right-click it (on the canvas or in Layers ) and choose Repeat… . In the dialog, pick the Items source — a list from the document's state, or Create new… to declare a fresh one by name. Optionally pick a Filter or Sort function, if the document defines any. Click Create Repeater . Your 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."},{"id":"docs:studio/design/repeaters#bind-the-list","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/#bind-the-list","title":"Repeaters","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.","heading":"Bind the list","text":"The items source is what makes a repeater useful. It can be: A plain list in state — start with Create new… and fill in items later in the State panel. A content collection — every blog post, product, or team member in your project's content. See Content types . A data source — anything that produces a list, like a Request fetching from an API. Declaring and feeding these lives in Script & logic . With the repeater selected, the Properties panel shows a Repeater section where you can rebind Items and add or change Filter and Sort after the fact."},{"id":"docs:studio/design/repeaters#edit-the-template","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/#edit-the-template","title":"Repeaters","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.","heading":"Edit the template","text":"Select 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 ). Inside the template, each item's data is in scope: 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. In the Properties panel, binding menus offer the current item and its position wherever a $ref binding is available. Bind the template's text and attributes to item fields and every rendered copy fills itself in from its own item. The 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 ) or use data-driven styling. A 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 ."},{"id":"docs:studio/design/repeaters#next","collection":"docs","slug":"studio/design/repeaters","url":"/docs/studio/design/repeaters/#next","title":"Repeaters","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.","heading":"Next","text":"Feed repeaters from collections — Content types Make the template a reusable component — Working with components"},{"id":"docs:studio/design/states-and-selectors","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"","text":"Hover states and selectors Buttons 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 's toolbar, next to the breakpoint tabs. Style a state Select the element and open the Style tab. Open the selector menu — it reads (base) when you're styling the element's normal look. Pick a state, say . Edit styles as usual. Everything you set now applies only in that state. The 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. The built-in states The menu offers the common ones: , , , , , , , , 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. They 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. Add your own selector Choose + Add custom… at the bottom of the menu, type a selector, and press Enter : :nth-child(2) — any state or position selector beyond the built-ins. .featured — only when the element has that class. &.active — only when the element itself carries the active class. [disabled] — only when the element has that attribute. A 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. Remove state styles Switch 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. Each 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 . Next Combine states with screen sizes in Breakpoints Give every link and button a default hover in the Stylebook"},{"id":"docs:studio/design/states-and-selectors#hover-states-and-selectors","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#hover-states-and-selectors","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"Hover states and selectors","text":"Buttons 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 's toolbar, next to the breakpoint tabs."},{"id":"docs:studio/design/states-and-selectors#style-a-state","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#style-a-state","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"Style a state","text":"Select the element and open the Style tab. Open the selector menu — it reads (base) when you're styling the element's normal look. Pick a state, say . Edit styles as usual. Everything you set now applies only in that state. The 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."},{"id":"docs:studio/design/states-and-selectors#the-built-in-states","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#the-built-in-states","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"The built-in states","text":"The menu offers the common ones: , , , , , , , , 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. They 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."},{"id":"docs:studio/design/states-and-selectors#add-your-own-selector","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#add-your-own-selector","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"Add your own selector","text":"Choose + Add custom… at the bottom of the menu, type a selector, and press Enter : :nth-child(2) — any state or position selector beyond the built-ins. .featured — only when the element has that class. &.active — only when the element itself carries the active class. [disabled] — only when the element has that attribute. A 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."},{"id":"docs:studio/design/states-and-selectors#remove-state-styles","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#remove-state-styles","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"Remove state styles","text":"Switch 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. Each 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 ."},{"id":"docs:studio/design/states-and-selectors#next","collection":"docs","slug":"studio/design/states-and-selectors","url":"/docs/studio/design/states-and-selectors/#next","title":"Hover states and selectors","description":"Style hover, focus, and other states in Jx Studio with the selector menu in the Style inspector — plus custom selectors of your own.","heading":"Next","text":"Combine states with screen sizes in Breakpoints Give every link and button a default hover in the Stylebook"},{"id":"docs:studio/design/style-inspector","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"","text":"Style inspector The 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. The toolbar Two controls sit above the sections: Breakpoint tabs — Base plus one tab per breakpoint. Base values apply everywhere; a breakpoint tab edits overrides for that screen size only. See Breakpoints . 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. The selector menu — style the element's :hover , :focus , and other states, or a selector of your own. See Hover states and selectors . Below 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. Sections Properties are grouped into accordion sections: Layout , Size , Spacing , Positioning , Typography , Background , Border , and Effects . Sections that already have values open automatically. A dot is the inspector's \"this is set\" signal, and it doubles as the eraser: A dot next to a property means it has a value here — click the dot to clear it. A dot on a section header means something inside is set — click it to clear the whole section. Some 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. The inputs Each property gets a control built for it: Number + unit — type the number, pick the unit ( px , rem , % , vw , and friends) or a keyword like auto from the attached menu. 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 . 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. Keyword menus — properties with fixed values get a dropdown; typography menus preview each choice (weights render at their weight, transforms as they transform). Shorthand 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. When 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 . Custom and relative styling Two sections close the list: Custom — any CSS property by name. Type the property, press Enter , and fill in its value; each row shows the property's browser default as a placeholder. 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 . Everything here writes plain CSS into the element's style object in the open file — the same nested format documented in Styling . Next Set element-wide defaults instead of styling one element at a time — Stylebook Name your colors, fonts, and sizes in Design tokens"},{"id":"docs:studio/design/style-inspector#style-inspector","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#style-inspector","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"Style inspector","text":"The 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."},{"id":"docs:studio/design/style-inspector#the-toolbar","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#the-toolbar","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"The toolbar","text":"Two controls sit above the sections: Breakpoint tabs — Base plus one tab per breakpoint. Base values apply everywhere; a breakpoint tab edits overrides for that screen size only. See Breakpoints . 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. The selector menu — style the element's :hover , :focus , and other states, or a selector of your own. See Hover states and selectors . Below 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."},{"id":"docs:studio/design/style-inspector#sections","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#sections","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"Sections","text":"Properties are grouped into accordion sections: Layout , Size , Spacing , Positioning , Typography , Background , Border , and Effects . Sections that already have values open automatically. A dot is the inspector's \"this is set\" signal, and it doubles as the eraser: A dot next to a property means it has a value here — click the dot to clear it. A dot on a section header means something inside is set — click it to clear the whole section. Some 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."},{"id":"docs:studio/design/style-inspector#the-inputs","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#the-inputs","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"The inputs","text":"Each property gets a control built for it: Number + unit — type the number, pick the unit ( px , rem , % , vw , and friends) or a keyword like auto from the attached menu. 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 . 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. Keyword menus — properties with fixed values get a dropdown; typography menus preview each choice (weights render at their weight, transforms as they transform). Shorthand 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. When 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 ."},{"id":"docs:studio/design/style-inspector#custom-and-relative-styling","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#custom-and-relative-styling","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"Custom and relative styling","text":"Two sections close the list: Custom — any CSS property by name. Type the property, press Enter , and fill in its value; each row shows the property's browser default as a placeholder. 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 . Everything here writes plain CSS into the element's style object in the open file — the same nested format documented in Styling ."},{"id":"docs:studio/design/style-inspector#next","collection":"docs","slug":"studio/design/style-inspector","url":"/docs/studio/design/style-inspector/#next","title":"Style inspector","description":"The Style inspector in Jx Studio: visual CSS controls grouped by section, with set-indicator dots, a property filter, and inherited placeholders.","heading":"Next","text":"Set element-wide defaults instead of styling one element at a time — Stylebook Name your colors, fonts, and sizes in Design tokens"},{"id":"docs:studio/design/stylebook","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"","text":"Stylebook Stylebook 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. Your 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. Read the catalog Like 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. Two 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. Style an element type 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: <h1> (or whatever you picked). Edit styles with the full Style inspector — sections, set-dots, and all. 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. The inspector's toolbar works the same as in Design mode: breakpoint tabs scope a default to a screen size (see Breakpoints ), and the selector menu adds states — give every link a default :hover in one edit (see Hover states and selectors ). Nested parts style as compound selections, like the header cells inside tables. Stylebook, element styles, or component styles? Three layers, from broadest to narrowest: Stylebook defaults — \"every h2 looks like this.\" Your typography, link, and form baseline. Change it here and the whole site follows. 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 . Per-element styles — \"this one element is special.\" The Style inspector on a single selection, layered on top of both. The narrower layer wins where they overlap, so keep the common look in the Stylebook and reserve per-element styling for true exceptions. Design tokens slot in underneath all three — defaults built from tokens keep even your baseline swappable. Element 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 . Next Name the values your defaults are built from in Design tokens How Stylebook fits among the modes — Modes and preview"},{"id":"docs:studio/design/stylebook#stylebook","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/#stylebook","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"Stylebook","text":"Stylebook 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. Your 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."},{"id":"docs:studio/design/stylebook#read-the-catalog","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/#read-the-catalog","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"Read the catalog","text":"Like 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. Two 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."},{"id":"docs:studio/design/stylebook#style-an-element-type","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/#style-an-element-type","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"Style an element type","text":"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: <h1> (or whatever you picked). Edit styles with the full Style inspector — sections, set-dots, and all. 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. The inspector's toolbar works the same as in Design mode: breakpoint tabs scope a default to a screen size (see Breakpoints ), and the selector menu adds states — give every link a default :hover in one edit (see Hover states and selectors ). Nested parts style as compound selections, like the header cells inside tables."},{"id":"docs:studio/design/stylebook#stylebook-element-styles-or-component-styles","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/#stylebook-element-styles-or-component-styles","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"Stylebook, element styles, or component styles?","text":"Three layers, from broadest to narrowest: Stylebook defaults — \"every h2 looks like this.\" Your typography, link, and form baseline. Change it here and the whole site follows. 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 . Per-element styles — \"this one element is special.\" The Style inspector on a single selection, layered on top of both. The narrower layer wins where they overlap, so keep the common look in the Stylebook and reserve per-element styling for true exceptions. Design tokens slot in underneath all three — defaults built from tokens keep even your baseline swappable. Element 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 ."},{"id":"docs:studio/design/stylebook#next","collection":"docs","slug":"studio/design/stylebook","url":"/docs/studio/design/stylebook/#next","title":"Stylebook","description":"Stylebook mode in Jx Studio: set the default look of every heading, button, and link in one catalog, live at every breakpoint.","heading":"Next","text":"Name the values your defaults are built from in Design tokens How Stylebook fits among the modes — Modes and preview"},{"id":"docs:studio/design/tokens","collection":"docs","slug":"studio/design/tokens","url":"/docs/studio/design/tokens/","title":"Design tokens","description":"Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.","heading":"","text":"Design tokens A 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. Define tokens Click the Settings gear at the bottom of the activity bar. Open the CSS Variables section. Tokens are grouped by what they name: 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 , 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. Fonts — each font renders a preview sentence in its own face below the row. Sizes & Spacing — widths, gaps, and radii. When a size token has a different value at a breakpoint, the override appears beneath it for editing. Other — anything that doesn't fit the groups above. To 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. Use tokens while styling Tokens surface right inside the Style inspector 's controls: 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. 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. Any field accepts a token typed directly as var(--color-primary-blue) — useful for the occasional property without a picker. A field showing a token name is following the token: edit the token in Settings and every element using it updates. Studio 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. Tokens 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 . Next Pair tokens with element defaults in the Stylebook Size tokens that shift per screen — see Breakpoints"},{"id":"docs:studio/design/tokens#design-tokens","collection":"docs","slug":"studio/design/tokens","url":"/docs/studio/design/tokens/#design-tokens","title":"Design tokens","description":"Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.","heading":"Design tokens","text":"A 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."},{"id":"docs:studio/design/tokens#define-tokens","collection":"docs","slug":"studio/design/tokens","url":"/docs/studio/design/tokens/#define-tokens","title":"Design tokens","description":"Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.","heading":"Define tokens","text":"Click the Settings gear at the bottom of the activity bar. Open the CSS Variables section. Tokens are grouped by what they name: 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 , 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. Fonts — each font renders a preview sentence in its own face below the row. Sizes & Spacing — widths, gaps, and radii. When a size token has a different value at a breakpoint, the override appears beneath it for editing. Other — anything that doesn't fit the groups above. To 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."},{"id":"docs:studio/design/tokens#use-tokens-while-styling","collection":"docs","slug":"studio/design/tokens","url":"/docs/studio/design/tokens/#use-tokens-while-styling","title":"Design tokens","description":"Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.","heading":"Use tokens while styling","text":"Tokens surface right inside the Style inspector 's controls: 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. 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. Any field accepts a token typed directly as var(--color-primary-blue) — useful for the occasional property without a picker. A field showing a token name is following the token: edit the token in Settings and every element using it updates. Studio 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. Tokens 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 ."},{"id":"docs:studio/design/tokens#next","collection":"docs","slug":"studio/design/tokens","url":"/docs/studio/design/tokens/#next","title":"Design tokens","description":"Design tokens in Jx Studio: define colors, fonts, and sizes once under Settings > CSS Variables and reuse them from every picker.","heading":"Next","text":"Pair tokens with element defaults in the Stylebook Size tokens that shift per screen — see Breakpoints"},{"id":"docs:studio/editing/frontmatter","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"","text":"Frontmatter and page metadata Every 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. Two 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. The Document panel Click Document in the activity bar to open it. Its sections, top to bottom, depend on the file you have open: Frontmatter For 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: Text fields, number fields, and date fields (dates as YYYY-MM-DD ) On/off checkboxes A dropdown for fields with a fixed set of choices A media picker for image fields Comma-separated entry for list fields Any 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 . Layout Site pages also get a Layout picker: keep the project default, choose a specific layout, or pick None — see Pages, layouts, components . Page The basics every page should have: Title — the browser-tab and search-result title. Description — the summary search engines show under the title. Viewport — leave the suggested default unless you have a specific reason not to. Icon — the small icon in the browser tab, picked from your media. OpenGraph The 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. Custom Tags The 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. The Properties bar When 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. On 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. Next Edit a whole collection's frontmatter at once in Grid mode Define collections and their fields in Content types"},{"id":"docs:studio/editing/frontmatter#frontmatter-and-page-metadata","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#frontmatter-and-page-metadata","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Frontmatter and page metadata","text":"Every 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. Two 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."},{"id":"docs:studio/editing/frontmatter#the-document-panel","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#the-document-panel","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"The Document panel","text":"Click Document in the activity bar to open it. Its sections, top to bottom, depend on the file you have open:"},{"id":"docs:studio/editing/frontmatter#frontmatter","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#frontmatter","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Frontmatter","text":"For 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: Text fields, number fields, and date fields (dates as YYYY-MM-DD ) On/off checkboxes A dropdown for fields with a fixed set of choices A media picker for image fields Comma-separated entry for list fields Any 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 ."},{"id":"docs:studio/editing/frontmatter#layout","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#layout","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Layout","text":"Site pages also get a Layout picker: keep the project default, choose a specific layout, or pick None — see Pages, layouts, components ."},{"id":"docs:studio/editing/frontmatter#page","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#page","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Page","text":"The basics every page should have: Title — the browser-tab and search-result title. Description — the summary search engines show under the title. Viewport — leave the suggested default unless you have a specific reason not to. Icon — the small icon in the browser tab, picked from your media."},{"id":"docs:studio/editing/frontmatter#opengraph","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#opengraph","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"OpenGraph","text":"The 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."},{"id":"docs:studio/editing/frontmatter#custom-tags","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#custom-tags","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Custom Tags","text":"The 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."},{"id":"docs:studio/editing/frontmatter#the-properties-bar","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#the-properties-bar","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"The Properties bar","text":"When 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. On 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."},{"id":"docs:studio/editing/frontmatter#next","collection":"docs","slug":"studio/editing/frontmatter","url":"/docs/studio/editing/frontmatter/#next","title":"Frontmatter and page metadata","description":"Fill in page titles, descriptions, social cards, and content-type fields with Jx Studio's Document panel and Properties bar — no YAML required.","heading":"Next","text":"Edit a whole collection's frontmatter at once in Grid mode Define collections and their fields in Content types"},{"id":"docs:studio/editing/grid","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"","text":"Grid mode Grid 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. Open a grid A CSV file — open it from Files (or Quick Access); spreadsheet files open straight into Grid. A content collection — right-click the collection's folder in Files and choose Edit Collection in Grid . All pages — right-click the pages folder and choose Edit Pages in Grid . 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. Edit cells Double-click a cell to edit it. Columns are typed, so each kind gets the right control: Text and number cells — type; number cells clean up currency symbols and commas for you. On/off cells — a checkbox; choice cells — a dropdown; date cells — a date field. List cells — chips: Enter or a comma adds one, Backspace removes the last, and each chip has its own remove button. Image cells open the media picker, and relationship cells — a field that points at another collection — open a picker of that collection's entries. Edited cells are highlighted, and nothing touches your files yet — everything waits for Save . Undo and redo ( ⌘Z / Ctrl+Z ) work on grid edits like anywhere else. Work in ranges Drag across cells to select a range. Copy and paste work on ranges — including pasting rows copied from a real spreadsheet. Delete or Backspace clears the selected cells, as one undoable step. Fill Down copies the range's first row into every row below it ( ⌘D / Ctrl+D ). Replace opens find & replace across all text cells; Replace All buffers the whole replacement as one undoable change — save to apply it. Filter rows searches across every column; each column header also has its own filter box and click-to-sort. Drag column headers to reorder them and drag their edges to resize — each grid remembers its column layout. Add and remove rows Add Row appends a pending row. In a collection or pages grid, fill in its Path cell — the file the new entry will become. Delete Rows marks the selected rows for deletion; they're removed when you save, after a confirmation. Added and marked rows are tinted until you save. Save — one batch Nothing writes until you click Save — the button counts your pending changes ( ⌘S / 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. Studio also protects you from crossed wires: A row whose file is open in a tab with unsaved changes won't save — save or close that tab first. 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. Saving with rows marked for deletion permanently deletes those entry files. Studio asks for confirmation before it does. Collection and pages grids A 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 ; collections themselves are defined in Content types . Saving 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. CSV grids A 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. Next The keys the grid claims for itself, and the ones that stay global: Keyboard shortcuts Bulk-edit the fields behind your content: Frontmatter and page metadata"},{"id":"docs:studio/editing/grid#grid-mode","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#grid-mode","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Grid mode","text":"Grid 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."},{"id":"docs:studio/editing/grid#open-a-grid","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#open-a-grid","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Open a grid","text":"A CSV file — open it from Files (or Quick Access); spreadsheet files open straight into Grid. A content collection — right-click the collection's folder in Files and choose Edit Collection in Grid . All pages — right-click the pages folder and choose Edit Pages in Grid . 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."},{"id":"docs:studio/editing/grid#edit-cells","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#edit-cells","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Edit cells","text":"Double-click a cell to edit it. Columns are typed, so each kind gets the right control: Text and number cells — type; number cells clean up currency symbols and commas for you. On/off cells — a checkbox; choice cells — a dropdown; date cells — a date field. List cells — chips: Enter or a comma adds one, Backspace removes the last, and each chip has its own remove button. Image cells open the media picker, and relationship cells — a field that points at another collection — open a picker of that collection's entries. Edited cells are highlighted, and nothing touches your files yet — everything waits for Save . Undo and redo ( ⌘Z / Ctrl+Z ) work on grid edits like anywhere else."},{"id":"docs:studio/editing/grid#work-in-ranges","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#work-in-ranges","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Work in ranges","text":"Drag across cells to select a range. Copy and paste work on ranges — including pasting rows copied from a real spreadsheet. Delete or Backspace clears the selected cells, as one undoable step. Fill Down copies the range's first row into every row below it ( ⌘D / Ctrl+D ). Replace opens find & replace across all text cells; Replace All buffers the whole replacement as one undoable change — save to apply it. Filter rows searches across every column; each column header also has its own filter box and click-to-sort. Drag column headers to reorder them and drag their edges to resize — each grid remembers its column layout."},{"id":"docs:studio/editing/grid#add-and-remove-rows","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#add-and-remove-rows","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Add and remove rows","text":"Add Row appends a pending row. In a collection or pages grid, fill in its Path cell — the file the new entry will become. Delete Rows marks the selected rows for deletion; they're removed when you save, after a confirmation. Added and marked rows are tinted until you save."},{"id":"docs:studio/editing/grid#save-one-batch","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#save-one-batch","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Save — one batch","text":"Nothing writes until you click Save — the button counts your pending changes ( ⌘S / 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. Studio also protects you from crossed wires: A row whose file is open in a tab with unsaved changes won't save — save or close that tab first. 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. Saving with rows marked for deletion permanently deletes those entry files. Studio asks for confirmation before it does."},{"id":"docs:studio/editing/grid#collection-and-pages-grids","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#collection-and-pages-grids","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Collection and pages grids","text":"A 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 ; collections themselves are defined in Content types . Saving 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."},{"id":"docs:studio/editing/grid#csv-grids","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#csv-grids","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"CSV grids","text":"A 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."},{"id":"docs:studio/editing/grid#next","collection":"docs","slug":"studio/editing/grid","url":"/docs/studio/editing/grid/#next","title":"Grid mode","description":"Edit collections, CSV files, and page metadata as a spreadsheet in Jx Studio: typed cells, fill down, find & replace, and one batched Save.","heading":"Next","text":"The keys the grid claims for itself, and the ones that stay global: Keyboard shortcuts Bulk-edit the fields behind your content: Frontmatter and page metadata"},{"id":"docs:studio/editing/slash-commands","collection":"docs","slug":"studio/editing/slash-commands","url":"/docs/studio/editing/slash-commands/","title":"Slash commands","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.","heading":"","text":"Slash commands The 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. Insert a block While editing text, type / on an empty line or after a space. Keep typing to filter the list — he narrows to the headings, img finds Image. Move through the matches with ↓ and ↑ , then press Enter — or click an entry. The block appears and you're already editing it — keep typing. On 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. To close the menu without inserting anything, press 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. The block list Command What you get Heading 1 Large heading Heading 2 Medium heading Heading 3 Small heading Paragraph Plain text Bulleted List Unordered list Numbered List Numbered list Blockquote Quote block Image Image Horizontal Rule Divider line Button Button element Link Anchor link Code Block Preformatted code Table Table Div Container Section Section container Filtering matches either the name or the element's short tag, so ol finds Numbered List and h1 finds Heading 1. The 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 . Next Everything else about writing on the page: Writing and formatting Other ways to add elements — the + affordance and drag and drop — in The canvas"},{"id":"docs:studio/editing/slash-commands#slash-commands","collection":"docs","slug":"studio/editing/slash-commands","url":"/docs/studio/editing/slash-commands/#slash-commands","title":"Slash commands","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.","heading":"Slash commands","text":"The 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."},{"id":"docs:studio/editing/slash-commands#insert-a-block","collection":"docs","slug":"studio/editing/slash-commands","url":"/docs/studio/editing/slash-commands/#insert-a-block","title":"Slash commands","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.","heading":"Insert a block","text":"While editing text, type / on an empty line or after a space. Keep typing to filter the list — he narrows to the headings, img finds Image. Move through the matches with ↓ and ↑ , then press Enter — or click an entry. The block appears and you're already editing it — keep typing. On 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. To close the menu without inserting anything, press 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."},{"id":"docs:studio/editing/slash-commands#the-block-list","collection":"docs","slug":"studio/editing/slash-commands","url":"/docs/studio/editing/slash-commands/#the-block-list","title":"Slash commands","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.","heading":"The block list","text":"Command What you get Heading 1 Large heading Heading 2 Medium heading Heading 3 Small heading Paragraph Plain text Bulleted List Unordered list Numbered List Numbered list Blockquote Quote block Image Image Horizontal Rule Divider line Button Button element Link Anchor link Code Block Preformatted code Table Table Div Container Section Section container Filtering matches either the name or the element's short tag, so ol finds Numbered List and h1 finds Heading 1. The 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 ."},{"id":"docs:studio/editing/slash-commands#next","collection":"docs","slug":"studio/editing/slash-commands","url":"/docs/studio/editing/slash-commands/#next","title":"Slash commands","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.","heading":"Next","text":"Everything else about writing on the page: Writing and formatting Other ways to add elements — the + affordance and drag and drop — in The canvas"},{"id":"docs:studio/editing/writing","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"","text":"Writing and formatting In 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. Moving around There is nothing to start or stop. The cursor is simply on the page, the way it is in a document editor: Click any text to put the cursor there. Arrow keys move it through the whole page, from the end of one block into the next. Home / End , word motion, and Page Up / Page Down all work as they do anywhere else. Drag — or hold Shift and move — to select, including across several blocks at once. Esc puts the cursor away. Your writing is saved into the document as you pause, so ⌘S (macOS) / Ctrl+S (Windows/Linux) always writes what is on screen, mid-sentence or not. What you can click into Anything that holds text: headings, paragraphs, list items, table cells, captions, definition lists, a disclosure's summary. Which ones those are comes from the page's own format rather than a fixed list, so a Markdown page and a component page each get the right answer. Two cases where the cursor lands somewhere you might not expect, and both are deliberate: A block quote holds paragraphs, so clicking one puts the cursor in the paragraph inside it — which is the thing you actually want to type in. A link is part of a paragraph, not a block of its own. Clicking one puts the cursor in the paragraph, so you can type through and around the link and it stays intact. Code blocks are not editable this way; their whitespace is significant, so they are left alone. Paragraphs Press 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. Shift+Enter stays in the same paragraph instead of starting a new block. Backspace at the very start of a block joins it onto the one above, and 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. To make the next block something other than a paragraph — a heading, a list, an image — type / and pick from the menu: Slash commands . The formatting toolbar The floating toolbar above the block carries the formatting buttons (the same bar described in The canvas ). Select some text first, then click a button to format it — click again to remove the format: Paragraphs, list items, and table cells offer Bold , Italic , Underline , Strikethrough , Superscript , Subscript , Code , and Link . Headings offer the shorter set: Bold , Italic , Code , and Link . With nothing selected the format buttons are disabled — they act on a range — and only Link stays clickable. The keyboard versions: ⌘B / Ctrl+B for bold, ⌘I / Ctrl+I for italic, ⌘` / Ctrl+` for code. Links Select the text to link. Click the Link button, or press ⌘K / Ctrl+K . Type the address and press Enter (or click Apply ). Put 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. Insert data The 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 . Pasting Paste 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. Text inside components Click 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: Enter finishes and keeps the change; Esc cancels it. Formatting, slash commands, and paragraph splits are off. The toolbar's name badge shows which component option you're editing (for example product-card · title ). Text 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. Behind 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. Next Insert whole blocks from the keyboard with Slash commands All the keys in one place: Keyboard shortcuts"},{"id":"docs:studio/editing/writing#writing-and-formatting","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#writing-and-formatting","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Writing and formatting","text":"In 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."},{"id":"docs:studio/editing/writing#moving-around","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#moving-around","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Moving around","text":"There is nothing to start or stop. The cursor is simply on the page, the way it is in a document editor: Click any text to put the cursor there. Arrow keys move it through the whole page, from the end of one block into the next. Home / End , word motion, and Page Up / Page Down all work as they do anywhere else. Drag — or hold Shift and move — to select, including across several blocks at once. Esc puts the cursor away. Your writing is saved into the document as you pause, so ⌘S (macOS) / Ctrl+S (Windows/Linux) always writes what is on screen, mid-sentence or not."},{"id":"docs:studio/editing/writing#what-you-can-click-into","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#what-you-can-click-into","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"What you can click into","text":"Anything that holds text: headings, paragraphs, list items, table cells, captions, definition lists, a disclosure's summary. Which ones those are comes from the page's own format rather than a fixed list, so a Markdown page and a component page each get the right answer. Two cases where the cursor lands somewhere you might not expect, and both are deliberate: A block quote holds paragraphs, so clicking one puts the cursor in the paragraph inside it — which is the thing you actually want to type in. A link is part of a paragraph, not a block of its own. Clicking one puts the cursor in the paragraph, so you can type through and around the link and it stays intact. Code blocks are not editable this way; their whitespace is significant, so they are left alone."},{"id":"docs:studio/editing/writing#paragraphs","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#paragraphs","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Paragraphs","text":"Press 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. Shift+Enter stays in the same paragraph instead of starting a new block. Backspace at the very start of a block joins it onto the one above, and 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. To make the next block something other than a paragraph — a heading, a list, an image — type / and pick from the menu: Slash commands ."},{"id":"docs:studio/editing/writing#the-formatting-toolbar","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#the-formatting-toolbar","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"The formatting toolbar","text":"The floating toolbar above the block carries the formatting buttons (the same bar described in The canvas ). Select some text first, then click a button to format it — click again to remove the format: Paragraphs, list items, and table cells offer Bold , Italic , Underline , Strikethrough , Superscript , Subscript , Code , and Link . Headings offer the shorter set: Bold , Italic , Code , and Link . With nothing selected the format buttons are disabled — they act on a range — and only Link stays clickable. The keyboard versions: ⌘B / Ctrl+B for bold, ⌘I / Ctrl+I for italic, ⌘` / Ctrl+` for code."},{"id":"docs:studio/editing/writing#links","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#links","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Links","text":"Select the text to link. Click the Link button, or press ⌘K / Ctrl+K . Type the address and press Enter (or click Apply ). Put 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."},{"id":"docs:studio/editing/writing#insert-data","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#insert-data","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Insert data","text":"The 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 ."},{"id":"docs:studio/editing/writing#pasting","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#pasting","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Pasting","text":"Paste 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."},{"id":"docs:studio/editing/writing#text-inside-components","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#text-inside-components","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Text inside components","text":"Click 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: Enter finishes and keeps the change; Esc cancels it. Formatting, slash commands, and paragraph splits are off. The toolbar's name badge shows which component option you're editing (for example product-card · title ). Text 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. Behind 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."},{"id":"docs:studio/editing/writing#next","collection":"docs","slug":"studio/editing/writing","url":"/docs/studio/editing/writing/#next","title":"Writing and formatting","description":"Write directly on the page in Jx Studio's Edit mode: paragraphs, the inline formatting toolbar, links, clean pasting, and text inside components.","heading":"Next","text":"Insert whole blocks from the keyboard with Slash commands All the keys in one place: Keyboard shortcuts"},{"id":"docs:studio/interface/canvas","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"","text":"The canvas The 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 ; this page covers the interactions shared by the visual modes. Pan and zoom In Design and Stylebook mode the canvas is an open surface you move around: Pan — scroll with the mouse wheel or trackpad. Hold Shift while scrolling to pan sideways, or drag with the middle mouse button. Zoom — hold ⌘ (macOS) or Ctrl (Windows/Linux) and scroll; the canvas zooms toward your cursor. ⌘= / Ctrl+= zooms in, ⌘- / Ctrl+- zooms out, and ⌘0 / Ctrl+0 resets to 100%. The zoom controls in the tab bar do the same, plus Fit to bring the whole canvas into view. In Edit mode the page scrolls like a normal browser page instead of panning, and Ctrl -scrolling zooms the content itself — the text reflows at the new size, like browser page zoom. Selecting elements Click 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. You can also move the selection from the keyboard: ↑ and ↓ step between siblings, ← selects the parent, → steps into the first child, and Esc clears the selection. The full list is in the shortcut reference . The block action bar A small floating toolbar appears above the selected element: A back arrow selects the parent element. 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. The ⠿ drag handle — drag it to move the element somewhere else on the page. Move up and Move down arrows swap the element with its neighbors. For a component instance, Edit Component opens the component itself; for anything else, Convert to Component turns the selection into a reusable component. While you're editing text, formatting buttons (bold, italic, and friends) and an Insert data button join the bar. See Edit mode . Inserting elements Three ways to add something to the page: 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. 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 . The Elements panel — open the Elements activity and drag an element or component card onto the canvas. Drag and drop You 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 Esc to cancel the drag with nothing changed. The right-click context menu Right-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. Editing text Click 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 . Next Style what you select in Design mode Wire up behavior in Script & logic Keep your hands on the keys with the shortcut reference"},{"id":"docs:studio/interface/canvas#the-canvas","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#the-canvas","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"The canvas","text":"The 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 ; this page covers the interactions shared by the visual modes."},{"id":"docs:studio/interface/canvas#pan-and-zoom","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#pan-and-zoom","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Pan and zoom","text":"In Design and Stylebook mode the canvas is an open surface you move around: Pan — scroll with the mouse wheel or trackpad. Hold Shift while scrolling to pan sideways, or drag with the middle mouse button. Zoom — hold ⌘ (macOS) or Ctrl (Windows/Linux) and scroll; the canvas zooms toward your cursor. ⌘= / Ctrl+= zooms in, ⌘- / Ctrl+- zooms out, and ⌘0 / Ctrl+0 resets to 100%. The zoom controls in the tab bar do the same, plus Fit to bring the whole canvas into view. In Edit mode the page scrolls like a normal browser page instead of panning, and Ctrl -scrolling zooms the content itself — the text reflows at the new size, like browser page zoom."},{"id":"docs:studio/interface/canvas#selecting-elements","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#selecting-elements","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Selecting elements","text":"Click 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. You can also move the selection from the keyboard: ↑ and ↓ step between siblings, ← selects the parent, → steps into the first child, and Esc clears the selection. The full list is in the shortcut reference ."},{"id":"docs:studio/interface/canvas#the-block-action-bar","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#the-block-action-bar","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"The block action bar","text":"A small floating toolbar appears above the selected element: A back arrow selects the parent element. 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. The ⠿ drag handle — drag it to move the element somewhere else on the page. Move up and Move down arrows swap the element with its neighbors. For a component instance, Edit Component opens the component itself; for anything else, Convert to Component turns the selection into a reusable component. While you're editing text, formatting buttons (bold, italic, and friends) and an Insert data button join the bar. See Edit mode ."},{"id":"docs:studio/interface/canvas#inserting-elements","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#inserting-elements","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Inserting elements","text":"Three ways to add something to the page: 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. 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 . The Elements panel — open the Elements activity and drag an element or component card onto the canvas."},{"id":"docs:studio/interface/canvas#drag-and-drop","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#drag-and-drop","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Drag and drop","text":"You 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 Esc to cancel the drag with nothing changed."},{"id":"docs:studio/interface/canvas#the-right-click-context-menu","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#the-right-click-context-menu","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"The right-click context menu","text":"Right-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."},{"id":"docs:studio/interface/canvas#editing-text","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#editing-text","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Editing text","text":"Click 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 ."},{"id":"docs:studio/interface/canvas#next","collection":"docs","slug":"studio/interface/canvas","url":"/docs/studio/interface/canvas/#next","title":"The canvas","description":"Working on the Jx Studio canvas — pan, zoom, selection, the block action bar, inserting elements, drag and drop, and the context menu.","heading":"Next","text":"Style what you select in Design mode Wire up behavior in Script & logic Keep your hands on the keys with the shortcut reference"},{"id":"docs:studio/interface/modes","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/","title":"Modes and the preview toggle","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.","heading":"","text":"Modes and the preview toggle The 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. Edit Edit 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 . Design Design 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 . Grid Grid 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. Code Code 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 for where code fits in Studio. Stylebook Stylebook 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 for how it fits into styling. Which files offer which modes Every file opens in its natural mode, and only sensible modes are enabled: File Opens in Also available Markdown pages and content ( .md ) Edit Design , Code , and the Preview toggle Components and pages ( .json ) Edit Design , Code , Stylebook , and the Preview toggle Spreadsheets ( .csv ) Grid Code The project file ( project.json ) Stylebook Code Installed format extensions can add their own file types with their own mode lists, so this table can grow with your project. The Preview toggle 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. Alongside the toggle, the tab bar offers: For pages with dynamic addresses (a product page, a blog post), a picker per URL parameter so you choose which real record to preview. For components, a small field per component option so you can try test values. For pages that use a layout, a Layout toggle to show or hide the elements the layout contributes. Switch Preview off to go back to editing — the mode switcher stays on Edit or Design the whole time. Next Learn the canvas itself — pan, zoom, selection, inserting — in The canvas See how each tab remembers its mode in Tabs and files"},{"id":"docs:studio/interface/modes#modes-and-the-preview-toggle","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#modes-and-the-preview-toggle","title":"Modes and the preview toggle","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.","heading":"Modes and the preview toggle","text":"The 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."},{"id":"docs:studio/interface/modes#edit","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#edit","title":"Modes and the preview toggle","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.","heading":"Edit","text":"Edit 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 ."},{"id":"docs:studio/interface/modes#design","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#design","title":"Modes and the preview toggle","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.","heading":"Design","text":"Design 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 ."},{"id":"docs:studio/interface/modes#grid","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#grid","title":"Modes and the preview toggle","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.","heading":"Grid","text":"Grid 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."},{"id":"docs:studio/interface/modes#code","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#code","title":"Modes and the preview toggle","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.","heading":"Code","text":"Code 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 for where code fits in Studio."},{"id":"docs:studio/interface/modes#stylebook","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#stylebook","title":"Modes and the preview toggle","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.","heading":"Stylebook","text":"Stylebook 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 for how it fits into styling."},{"id":"docs:studio/interface/modes#which-files-offer-which-modes","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#which-files-offer-which-modes","title":"Modes and the preview toggle","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.","heading":"Which files offer which modes","text":"Every file opens in its natural mode, and only sensible modes are enabled: File Opens in Also available Markdown pages and content ( .md ) Edit Design , Code , and the Preview toggle Components and pages ( .json ) Edit Design , Code , Stylebook , and the Preview toggle Spreadsheets ( .csv ) Grid Code The project file ( project.json ) Stylebook Code Installed format extensions can add their own file types with their own mode lists, so this table can grow with your project."},{"id":"docs:studio/interface/modes#the-preview-toggle","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#the-preview-toggle","title":"Modes and the preview toggle","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.","heading":"The Preview toggle","text":"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. Alongside the toggle, the tab bar offers: For pages with dynamic addresses (a product page, a blog post), a picker per URL parameter so you choose which real record to preview. For components, a small field per component option so you can try test values. For pages that use a layout, a Layout toggle to show or hide the elements the layout contributes. Switch Preview off to go back to editing — the mode switcher stays on Edit or Design the whole time."},{"id":"docs:studio/interface/modes#next","collection":"docs","slug":"studio/interface/modes","url":"/docs/studio/interface/modes/#next","title":"Modes and the preview toggle","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.","heading":"Next","text":"Learn the canvas itself — pan, zoom, selection, inserting — in The canvas See how each tab remembers its mode in Tabs and files"},{"id":"docs:studio/interface/quick-access","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/","title":"Quick Access","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.","heading":"","text":"Quick Access Quick 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 Enter . If you know roughly what a file is called, it beats clicking through the file tree every time. Open it Press ⌘P (macOS) or Ctrl+P (Windows/Linux) — it works from anywhere in Studio. Or click the Search files… field in the middle of the toolbar. Press Esc or click outside the palette to dismiss it. What it finds With 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. Before you type anything, the palette lists your recently opened files, so reopening the file you just closed is ⌘P , Enter . With 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. Keyboard controls ↓ and ↑ move through the results. Enter opens the highlighted result in a tab. Esc closes the palette. The mouse works too — click any row to open it. Next See how opened files behave in Tabs and files The rest of the keyboard lives in the shortcut reference"},{"id":"docs:studio/interface/quick-access#quick-access","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/#quick-access","title":"Quick Access","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.","heading":"Quick Access","text":"Quick 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 Enter . If you know roughly what a file is called, it beats clicking through the file tree every time."},{"id":"docs:studio/interface/quick-access#open-it","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/#open-it","title":"Quick Access","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.","heading":"Open it","text":"Press ⌘P (macOS) or Ctrl+P (Windows/Linux) — it works from anywhere in Studio. Or click the Search files… field in the middle of the toolbar. Press Esc or click outside the palette to dismiss it."},{"id":"docs:studio/interface/quick-access#what-it-finds","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/#what-it-finds","title":"Quick Access","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.","heading":"What it finds","text":"With 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. Before you type anything, the palette lists your recently opened files, so reopening the file you just closed is ⌘P , Enter . With 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."},{"id":"docs:studio/interface/quick-access#keyboard-controls","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/#keyboard-controls","title":"Quick Access","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.","heading":"Keyboard controls","text":"↓ and ↑ move through the results. Enter opens the highlighted result in a tab. Esc closes the palette. The mouse works too — click any row to open it."},{"id":"docs:studio/interface/quick-access#next","collection":"docs","slug":"studio/interface/quick-access","url":"/docs/studio/interface/quick-access/#next","title":"Quick Access","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.","heading":"Next","text":"See how opened files behave in Tabs and files The rest of the keyboard lives in the shortcut reference"},{"id":"docs:studio/interface/shortcuts","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"","text":"Keyboard shortcuts Every shortcut in Jx Studio, grouped by where it applies. macOS uses ⌘ where Windows and Linux use Ctrl ; everything else is the same on all platforms. Everywhere These work throughout Studio, even while typing in a field. Action macOS Windows / Linux Save the active file ⌘S Ctrl+S Open Quick Access ⌘P Ctrl+P Open a project ⌘O Ctrl+O Close the active tab ⌘W Ctrl+W Undo ⌘Z Ctrl+Z Redo ⇧⌘Z Ctrl+Shift+Z Canvas — with an element selected These apply when a block is selected but the cursor is not in its text — after picking a row in the layers panel, for instance. Once the cursor is in the text it owns these keys; see Writing below. Action macOS Windows / Linux Duplicate the element ⌘D Ctrl+D Copy the element ⌘C Ctrl+C Cut the element ⌘X Ctrl+X Paste ⌘V Ctrl+V Delete the element Delete or Backspace same Clear the selection Esc Esc Insert a paragraph after the element Enter Enter Select the previous / next sibling ↑ / ↓ ↑ / ↓ Select the parent ← ← Select the first child → → Canvas — zoom and pan Action macOS Windows / Linux Zoom in ⌘= Ctrl+= Zoom out ⌘- Ctrl+- Reset zoom to 100% ⌘0 Ctrl+0 Zoom toward the cursor ⌘ + scroll Ctrl + scroll Pan scroll scroll Pan sideways Shift + scroll Shift + scroll Pan by dragging middle-mouse drag middle-mouse drag In Edit mode the zoom keys resize the content itself (the text reflows), and the page scrolls instead of panning. Writing With the cursor in text on the canvas. Moving and selecting Action macOS Windows / Linux Put the cursor somewhere click click Move through the page, block to block ↑ ↓ ← → ↑ ↓ ← → Start / end of line Home / End Home / End Select, including across blocks Shift + move, or drag Shift + move, or drag Put the cursor away Esc Esc Changing the text Action macOS Windows / Linux Bold ⌘B Ctrl+B Italic ⌘I Ctrl+I Inline code ⌘` Ctrl+` Add a link ⌘K Ctrl+K Open the block menu / / New paragraph Enter Enter Line break, same paragraph Shift+Enter Shift+Enter Join onto the block above Backspace at the block's start same Pull the next block up Delete at the block's end same Save ⌘S Ctrl+S The block menu opens when / is typed at the start of a line or after a space. Saving always writes what is on screen — your writing reaches the document as you pause, so there is no need to finish anything first. Quick Access palette Action Key Move through the results ↑ / ↓ Open the highlighted file Enter Close the palette Esc Drag and drop Action Key Cancel the drag Esc Grid mode In Grid mode the table owns the editing keys: copy, paste, delete, arrows, and 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. While a dialog is open A dialog takes the keyboard for as long as it is up: it gets focus when it opens, 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 Delete , Enter , ⌘S and the rest cannot reach the page you can't click. Next See where these fit on the surface itself in The canvas Formatting while writing is covered in Edit mode"},{"id":"docs:studio/interface/shortcuts#keyboard-shortcuts","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#keyboard-shortcuts","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Keyboard shortcuts","text":"Every shortcut in Jx Studio, grouped by where it applies. macOS uses ⌘ where Windows and Linux use Ctrl ; everything else is the same on all platforms."},{"id":"docs:studio/interface/shortcuts#everywhere","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#everywhere","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Everywhere","text":"These work throughout Studio, even while typing in a field. Action macOS Windows / Linux Save the active file ⌘S Ctrl+S Open Quick Access ⌘P Ctrl+P Open a project ⌘O Ctrl+O Close the active tab ⌘W Ctrl+W Undo ⌘Z Ctrl+Z Redo ⇧⌘Z Ctrl+Shift+Z"},{"id":"docs:studio/interface/shortcuts#canvas-with-an-element-selected","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#canvas-with-an-element-selected","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Canvas — with an element selected","text":"These apply when a block is selected but the cursor is not in its text — after picking a row in the layers panel, for instance. Once the cursor is in the text it owns these keys; see Writing below. Action macOS Windows / Linux Duplicate the element ⌘D Ctrl+D Copy the element ⌘C Ctrl+C Cut the element ⌘X Ctrl+X Paste ⌘V Ctrl+V Delete the element Delete or Backspace same Clear the selection Esc Esc Insert a paragraph after the element Enter Enter Select the previous / next sibling ↑ / ↓ ↑ / ↓ Select the parent ← ← Select the first child → →"},{"id":"docs:studio/interface/shortcuts#canvas-zoom-and-pan","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#canvas-zoom-and-pan","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Canvas — zoom and pan","text":"Action macOS Windows / Linux Zoom in ⌘= Ctrl+= Zoom out ⌘- Ctrl+- Reset zoom to 100% ⌘0 Ctrl+0 Zoom toward the cursor ⌘ + scroll Ctrl + scroll Pan scroll scroll Pan sideways Shift + scroll Shift + scroll Pan by dragging middle-mouse drag middle-mouse drag In Edit mode the zoom keys resize the content itself (the text reflows), and the page scrolls instead of panning."},{"id":"docs:studio/interface/shortcuts#writing","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#writing","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Writing","text":"With the cursor in text on the canvas."},{"id":"docs:studio/interface/shortcuts#moving-and-selecting","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#moving-and-selecting","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Moving and selecting","text":"Action macOS Windows / Linux Put the cursor somewhere click click Move through the page, block to block ↑ ↓ ← → ↑ ↓ ← → Start / end of line Home / End Home / End Select, including across blocks Shift + move, or drag Shift + move, or drag Put the cursor away Esc Esc"},{"id":"docs:studio/interface/shortcuts#changing-the-text","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#changing-the-text","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Changing the text","text":"Action macOS Windows / Linux Bold ⌘B Ctrl+B Italic ⌘I Ctrl+I Inline code ⌘` Ctrl+` Add a link ⌘K Ctrl+K Open the block menu / / New paragraph Enter Enter Line break, same paragraph Shift+Enter Shift+Enter Join onto the block above Backspace at the block's start same Pull the next block up Delete at the block's end same Save ⌘S Ctrl+S The block menu opens when / is typed at the start of a line or after a space. Saving always writes what is on screen — your writing reaches the document as you pause, so there is no need to finish anything first."},{"id":"docs:studio/interface/shortcuts#quick-access-palette","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#quick-access-palette","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Quick Access palette","text":"Action Key Move through the results ↑ / ↓ Open the highlighted file Enter Close the palette Esc"},{"id":"docs:studio/interface/shortcuts#drag-and-drop","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#drag-and-drop","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Drag and drop","text":"Action Key Cancel the drag Esc"},{"id":"docs:studio/interface/shortcuts#grid-mode","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#grid-mode","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Grid mode","text":"In Grid mode the table owns the editing keys: copy, paste, delete, arrows, and 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."},{"id":"docs:studio/interface/shortcuts#while-a-dialog-is-open","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#while-a-dialog-is-open","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"While a dialog is open","text":"A dialog takes the keyboard for as long as it is up: it gets focus when it opens, 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 Delete , Enter , ⌘S and the rest cannot reach the page you can't click."},{"id":"docs:studio/interface/shortcuts#next","collection":"docs","slug":"studio/interface/shortcuts","url":"/docs/studio/interface/shortcuts/#next","title":"Keyboard shortcuts","description":"Every keyboard shortcut in Jx Studio, grouped by context, with macOS and Windows/Linux keys.","heading":"Next","text":"See where these fit on the surface itself in The canvas Formatting while writing is covered in Edit mode"},{"id":"docs:studio/interface/tabs","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"","text":"Tabs and files Every 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. Opening and switching Open files from the Files activity, the Manage browser, or Quick Access ( ⌘P on macOS, Ctrl+P on Windows/Linux). Click a tab to switch to it. Close a tab with its × button, by middle-clicking it, or with ⌘W / Ctrl+W . When more tabs are open than fit, scroll the mouse wheel over the strip to slide them. Studio saves only when you do Jx Studio does not auto-save. Edits live in the tab until you save: A ● dot on the tab marks unsaved changes, and the Save button in the toolbar lights up. Save with ⌘S / Ctrl+S or the Save button — the status bar confirms with \"Saved\". Closing 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. Undo and redo are per file as well: each tab keeps its own history, so ⌘Z / Ctrl+Z in one tab never unwinds work in another. Saving 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 . Each tab remembers its view A 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: its mode and Preview toggle, its zoom level and canvas position, its selected element and active right-panel tab. New files open in their natural mode — Markdown in Edit , spreadsheets in Grid , the project file in Stylebook . The tab bar Below the tab strip, a second row carries the controls for the active tab: Back and a breadcrumb trail, when you've drilled from a page into one of its components — click any crumb to jump back up. The zoom controls for the current mode, including Fit on the pannable canvases. The Preview toggle, the Layout toggle, and the preview pickers described in Modes and the preview toggle . 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 . Mode-specific actions, like Export in Code mode. Next Find any file fast with Quick Access Browse the whole project visually in Manage"},{"id":"docs:studio/interface/tabs#tabs-and-files","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#tabs-and-files","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"Tabs and files","text":"Every 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."},{"id":"docs:studio/interface/tabs#opening-and-switching","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#opening-and-switching","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"Opening and switching","text":"Open files from the Files activity, the Manage browser, or Quick Access ( ⌘P on macOS, Ctrl+P on Windows/Linux). Click a tab to switch to it. Close a tab with its × button, by middle-clicking it, or with ⌘W / Ctrl+W . When more tabs are open than fit, scroll the mouse wheel over the strip to slide them."},{"id":"docs:studio/interface/tabs#studio-saves-only-when-you-do","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#studio-saves-only-when-you-do","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"Studio saves only when you do","text":"Jx Studio does not auto-save. Edits live in the tab until you save: A ● dot on the tab marks unsaved changes, and the Save button in the toolbar lights up. Save with ⌘S / Ctrl+S or the Save button — the status bar confirms with \"Saved\". Closing 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. Undo and redo are per file as well: each tab keeps its own history, so ⌘Z / Ctrl+Z in one tab never unwinds work in another. Saving 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 ."},{"id":"docs:studio/interface/tabs#each-tab-remembers-its-view","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#each-tab-remembers-its-view","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"Each tab remembers its view","text":"A 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: its mode and Preview toggle, its zoom level and canvas position, its selected element and active right-panel tab. New files open in their natural mode — Markdown in Edit , spreadsheets in Grid , the project file in Stylebook ."},{"id":"docs:studio/interface/tabs#the-tab-bar","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#the-tab-bar","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"The tab bar","text":"Below the tab strip, a second row carries the controls for the active tab: Back and a breadcrumb trail, when you've drilled from a page into one of its components — click any crumb to jump back up. The zoom controls for the current mode, including Fit on the pannable canvases. The Preview toggle, the Layout toggle, and the preview pickers described in Modes and the preview toggle . 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 . Mode-specific actions, like Export in Code mode."},{"id":"docs:studio/interface/tabs#next","collection":"docs","slug":"studio/interface/tabs","url":"/docs/studio/interface/tabs/#next","title":"Tabs and files","description":"How tabs work in Jx Studio — opening and switching files, dirty markers, explicit saving, and the per-tab view memory.","heading":"Next","text":"Find any file fast with Quick Access Browse the whole project visually in Manage"},{"id":"docs:studio/interface/welcome-screen","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/","title":"Welcome screen","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.","heading":"","text":"Welcome screen When 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. Start a new project Click New Project… . 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. Click Next , name the project, choose the Location to create it in, and adjust the design quickstart (colors, fonts, logo). Click Create Project — Studio writes the files where you pointed it and opens the project. The full walkthrough is in Create a project . Start from an example 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 . Open an existing project Click Open Project… . Choose your project folder in the picker that appears. Studio opens the project and adds it to Recent for next time. On 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. If a repository you expect isn't listed, the App simply hasn't been given access to it — see Repository access below. Clone a git repository This entry appears when your Studio setup can run git. Click Clone Git Repository… . Paste the repository URL and click Clone . Studio clones the repository and opens it as a project. See Source control for everything git-related in Studio. Add an existing repository This entry appears when Studio is connected to your GitHub account. Click Add Existing Repository… . Type in the filter field to narrow the list of repositories your account can reach. Click a repository — Studio imports it and opens it as a project. A 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 . Repository access Studio only sees the repositories you have granted the Jx Suite GitHub App access to. There are two places to change that. If 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. Once 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: 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. Save the change on GitHub, then come back to Studio. Click Refresh — the picker re-reads your repositories and the newly granted ones appear. Projects and Recent Below the Start actions: Projects lists the projects your Studio installation knows about that you haven't opened recently. Click one to open it. 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. You don't need the mouse: press ⌘P (macOS) or Ctrl+P (Windows/Linux) on the welcome screen and Quick Access lists your recent projects to reopen. Next New to Jx? Follow Your first project end to end Once a project is open, get oriented with A tour of Jx Studio"},{"id":"docs:studio/interface/welcome-screen#welcome-screen","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#welcome-screen","title":"Welcome screen","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.","heading":"Welcome screen","text":"When 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."},{"id":"docs:studio/interface/welcome-screen#start-a-new-project","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#start-a-new-project","title":"Welcome screen","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.","heading":"Start a new project","text":"Click New Project… . 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. Click Next , name the project, choose the Location to create it in, and adjust the design quickstart (colors, fonts, logo). Click Create Project — Studio writes the files where you pointed it and opens the project. The full walkthrough is in Create a project ."},{"id":"docs:studio/interface/welcome-screen#start-from-an-example","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#start-from-an-example","title":"Welcome screen","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.","heading":"Start from an example","text":"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 ."},{"id":"docs:studio/interface/welcome-screen#open-an-existing-project","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#open-an-existing-project","title":"Welcome screen","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.","heading":"Open an existing project","text":"Click Open Project… . Choose your project folder in the picker that appears. Studio opens the project and adds it to Recent for next time. On 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. If a repository you expect isn't listed, the App simply hasn't been given access to it — see Repository access below."},{"id":"docs:studio/interface/welcome-screen#clone-a-git-repository","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#clone-a-git-repository","title":"Welcome screen","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.","heading":"Clone a git repository","text":"This entry appears when your Studio setup can run git. Click Clone Git Repository… . Paste the repository URL and click Clone . Studio clones the repository and opens it as a project. See Source control for everything git-related in Studio."},{"id":"docs:studio/interface/welcome-screen#add-an-existing-repository","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#add-an-existing-repository","title":"Welcome screen","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.","heading":"Add an existing repository","text":"This entry appears when Studio is connected to your GitHub account. Click Add Existing Repository… . Type in the filter field to narrow the list of repositories your account can reach. Click a repository — Studio imports it and opens it as a project. A 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 ."},{"id":"docs:studio/interface/welcome-screen#repository-access","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#repository-access","title":"Welcome screen","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.","heading":"Repository access","text":"Studio only sees the repositories you have granted the Jx Suite GitHub App access to. There are two places to change that. If 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. Once 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: 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. Save the change on GitHub, then come back to Studio. Click Refresh — the picker re-reads your repositories and the newly granted ones appear."},{"id":"docs:studio/interface/welcome-screen#projects-and-recent","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#projects-and-recent","title":"Welcome screen","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.","heading":"Projects and Recent","text":"Below the Start actions: Projects lists the projects your Studio installation knows about that you haven't opened recently. Click one to open it. 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. You don't need the mouse: press ⌘P (macOS) or Ctrl+P (Windows/Linux) on the welcome screen and Quick Access lists your recent projects to reopen."},{"id":"docs:studio/interface/welcome-screen#next","collection":"docs","slug":"studio/interface/welcome-screen","url":"/docs/studio/interface/welcome-screen/#next","title":"Welcome screen","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.","heading":"Next","text":"New to Jx? Follow Your first project end to end Once a project is open, get oriented with A tour of Jx Studio"},{"id":"docs:studio/logic/code","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/","title":"Code editing","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.","heading":"","text":"Code editing Everything in the Logic pages so far works without writing code. But Studio doesn't pretend code doesn't exist — when a function outgrows 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. There are two distinct code surfaces, for two different jobs. The function editor A 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. What you get: Formatting on open — the body is pretty-printed before you start. Live linting — problems are underlined as you type, with the message on hover. Completions — type state. to see every entry from the State panel (your values, data sources, and functions), and window. for the standard library ( Math , JSON , …). Named formulas carry their descriptions into the suggestions. 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. Inside 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. Sidecar files A 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 . Code mode: the whole file as source The 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 . It's the same document the visual surfaces edit, from the other side: 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. 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 Ctrl+Space completes property names. Studio uses the project.schema.json and document.schema.json that jx schema 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. Export in the tab bar saves a copy of the file elsewhere. You 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 the rules are composed for you on the server and nothing is written to your repository at all. When to drop down A handler needs a loop, error handling, or an API the structured editors don't cover. You're doing a bulk edit — renaming a state entry everywhere it's referenced is a find-and-replace in Code mode. You want to add parameters to a named formula, or make other edits the panels don't surface. You're learning the format: build something visually, then read it in Code mode. It's the fastest way to understand what Studio writes. Everything 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. Next The file format you're reading in Code mode: Components and Reactivity Prefer structure when it fits: Statements Ship your work with Source control"},{"id":"docs:studio/logic/code#code-editing","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#code-editing","title":"Code editing","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.","heading":"Code editing","text":"Everything in the Logic pages so far works without writing code. But Studio doesn't pretend code doesn't exist — when a function outgrows 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. There are two distinct code surfaces, for two different jobs."},{"id":"docs:studio/logic/code#the-function-editor","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#the-function-editor","title":"Code editing","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.","heading":"The function editor","text":"A 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. What you get: Formatting on open — the body is pretty-printed before you start. Live linting — problems are underlined as you type, with the message on hover. Completions — type state. to see every entry from the State panel (your values, data sources, and functions), and window. for the standard library ( Math , JSON , …). Named formulas carry their descriptions into the suggestions. 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. Inside 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."},{"id":"docs:studio/logic/code#sidecar-files","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#sidecar-files","title":"Code editing","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.","heading":"Sidecar files","text":"A 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 ."},{"id":"docs:studio/logic/code#code-mode-the-whole-file-as-source","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#code-mode-the-whole-file-as-source","title":"Code editing","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.","heading":"Code mode: the whole file as source","text":"The 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 . It's the same document the visual surfaces edit, from the other side: 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. 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 Ctrl+Space completes property names. Studio uses the project.schema.json and document.schema.json that jx schema 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. Export in the tab bar saves a copy of the file elsewhere. You 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 the rules are composed for you on the server and nothing is written to your repository at all."},{"id":"docs:studio/logic/code#when-to-drop-down","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#when-to-drop-down","title":"Code editing","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.","heading":"When to drop down","text":"A handler needs a loop, error handling, or an API the structured editors don't cover. You're doing a bulk edit — renaming a state entry everywhere it's referenced is a find-and-replace in Code mode. You want to add parameters to a named formula, or make other edits the panels don't surface. You're learning the format: build something visually, then read it in Code mode. It's the fastest way to understand what Studio writes. Everything 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."},{"id":"docs:studio/logic/code#next","collection":"docs","slug":"studio/logic/code","url":"/docs/studio/logic/code/#next","title":"Code editing","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.","heading":"Next","text":"The file format you're reading in Code mode: Components and Reactivity Prefer structure when it fits: Statements Ship your work with Source control"},{"id":"docs:studio/logic/data-explorer","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/","title":"Data explorer","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.","heading":"","text":"Data explorer Data is the read side of the State panel : 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. Read the values Each state entry gets a row: its kind badge, its name, and a value summary — Array(12) — a list and how many items it holds. {5} — an object and how many fields it has. string , number , boolean — a plain value's type. pending — no value yet: a data source that hasn't finished resolving (or failed to). Click 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. Refresh The 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. Test values for component options A 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 . Open a component file. The tab bar shows a field named after each option. 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. The canvas re-renders with the value, and the Data activity, template previews, and formula badges all see it. Clear the field to return that option to its authored default. Test values are a preview aid — they live with your editing session, not in the component file. Debug with it A pending Request usually means the URL is wrong or the server didn't answer — check the entry in the State panel, then Refresh . A computed value that shows the wrong result: expand the entries it depends on here first; most \"formula bugs\" are actually surprising input data. Events not visibly doing anything? Trigger them with Preview on and watch the target entry's row change. Next The entries themselves are declared in the State panel Where the data comes from: Data sources The same live values ride along in the formula workspace 's data rail"},{"id":"docs:studio/logic/data-explorer#data-explorer","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#data-explorer","title":"Data explorer","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.","heading":"Data explorer","text":"Data is the read side of the State panel : 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."},{"id":"docs:studio/logic/data-explorer#read-the-values","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#read-the-values","title":"Data explorer","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.","heading":"Read the values","text":"Each state entry gets a row: its kind badge, its name, and a value summary — Array(12) — a list and how many items it holds. {5} — an object and how many fields it has. string , number , boolean — a plain value's type. pending — no value yet: a data source that hasn't finished resolving (or failed to). Click 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."},{"id":"docs:studio/logic/data-explorer#refresh","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#refresh","title":"Data explorer","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.","heading":"Refresh","text":"The 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."},{"id":"docs:studio/logic/data-explorer#test-values-for-component-options","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#test-values-for-component-options","title":"Data explorer","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.","heading":"Test values for component options","text":"A 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 . Open a component file. The tab bar shows a field named after each option. 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. The canvas re-renders with the value, and the Data activity, template previews, and formula badges all see it. Clear the field to return that option to its authored default. Test values are a preview aid — they live with your editing session, not in the component file."},{"id":"docs:studio/logic/data-explorer#debug-with-it","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#debug-with-it","title":"Data explorer","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.","heading":"Debug with it","text":"A pending Request usually means the URL is wrong or the server didn't answer — check the entry in the State panel, then Refresh . A computed value that shows the wrong result: expand the entries it depends on here first; most \"formula bugs\" are actually surprising input data. Events not visibly doing anything? Trigger them with Preview on and watch the target entry's row change."},{"id":"docs:studio/logic/data-explorer#next","collection":"docs","slug":"studio/logic/data-explorer","url":"/docs/studio/logic/data-explorer/#next","title":"Data explorer","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.","heading":"Next","text":"The entries themselves are declared in the State panel Where the data comes from: Data sources The same live values ride along in the formula workspace 's data rail"},{"id":"docs:studio/logic/data-sources","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"","text":"Data sources A 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 '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 . Each source's editor shows just the fields that source needs. Request — fetch from a URL Fetch (Request) loads data over HTTP — a JSON API, most typically. URL — where to fetch from. Method — GET , POST , PUT , DELETE , or PATCH . 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. While the request is in flight the entry reads as pending in the Data explorer ; the resolved response then flows wherever the entry is used. LocalStorage and SessionStorage — remember in the browser Both persist a value in the visitor's browser: LocalStorage survives closing the browser, SessionStorage lasts for the visit. Their editors are identical: Key — the name the value is stored under. Default — what the entry is worth before anything has been stored. JSON here gives you a structured default. Writing 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\". Cookie — a value shared with the server Cookie — the cookie's name. Default — the value before the cookie exists. Use a Cookie instead of LocalStorage when the server needs to see the value on each request. IndexedDB — a structured browser database For larger structured data the browser stores locally: Database — the database name. Store — the object store within it. Version — the schema version number. FormData — the state of a form FormData holds a browser form-data object — the shape requests use to submit forms: Fields — a JSON object naming the fields and their starting values. The entry starts out seeded with those fields, ready to fill in and send as a request body. Set and Map Two 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. ContentCollection — query your project's content ContentCollection turns your project's content — the entries behind your 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: contentType — which content type to query, picked from the ones your project defines. 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 . sort — zero or more rules, each a field and an order ( asc or desc ). limit — the maximum number of entries to return. On 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\". Database sources Everything above reads from the browser, the network, or your project's files. With the connector extension enabled, your data tables 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: 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. 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. 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. Account sources The auth extension adds two more, both about who is looking at the page: 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 . 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. Both extensions are covered from the other side — the tables, the settings, the secrets — in Databases and Auth and secrets . External sources 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. Every 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 . The table sources are rewritten at build time into ordinary fetches against /_jx/data , so no extension code ships to the browser. Next Watch a source resolve, live, in the Data explorer Compute over fetched data with Formulas and expressions Content types themselves are managed in Content types"},{"id":"docs:studio/logic/data-sources#data-sources","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#data-sources","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Data sources","text":"A 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 '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 . Each source's editor shows just the fields that source needs."},{"id":"docs:studio/logic/data-sources#request-fetch-from-a-url","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#request-fetch-from-a-url","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Request — fetch from a URL","text":"Fetch (Request) loads data over HTTP — a JSON API, most typically. URL — where to fetch from. Method — GET , POST , PUT , DELETE , or PATCH . 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. While the request is in flight the entry reads as pending in the Data explorer ; the resolved response then flows wherever the entry is used."},{"id":"docs:studio/logic/data-sources#localstorage-and-sessionstorage-remember-in-the-browser","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#localstorage-and-sessionstorage-remember-in-the-browser","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"LocalStorage and SessionStorage — remember in the browser","text":"Both persist a value in the visitor's browser: LocalStorage survives closing the browser, SessionStorage lasts for the visit. Their editors are identical: Key — the name the value is stored under. Default — what the entry is worth before anything has been stored. JSON here gives you a structured default. Writing 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\"."},{"id":"docs:studio/logic/data-sources#cookie-a-value-shared-with-the-server","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#cookie-a-value-shared-with-the-server","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Cookie — a value shared with the server","text":"Cookie — the cookie's name. Default — the value before the cookie exists. Use a Cookie instead of LocalStorage when the server needs to see the value on each request."},{"id":"docs:studio/logic/data-sources#indexeddb-a-structured-browser-database","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#indexeddb-a-structured-browser-database","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"IndexedDB — a structured browser database","text":"For larger structured data the browser stores locally: Database — the database name. Store — the object store within it. Version — the schema version number."},{"id":"docs:studio/logic/data-sources#formdata-the-state-of-a-form","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#formdata-the-state-of-a-form","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"FormData — the state of a form","text":"FormData holds a browser form-data object — the shape requests use to submit forms: Fields — a JSON object naming the fields and their starting values. The entry starts out seeded with those fields, ready to fill in and send as a request body."},{"id":"docs:studio/logic/data-sources#set-and-map","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#set-and-map","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Set and Map","text":"Two 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."},{"id":"docs:studio/logic/data-sources#contentcollection-query-your-projects-content","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#contentcollection-query-your-projects-content","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"ContentCollection — query your project's content","text":"ContentCollection turns your project's content — the entries behind your 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: contentType — which content type to query, picked from the ones your project defines. 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 . sort — zero or more rules, each a field and an order ( asc or desc ). limit — the maximum number of entries to return. On 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\"."},{"id":"docs:studio/logic/data-sources#database-sources","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#database-sources","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Database sources","text":"Everything above reads from the browser, the network, or your project's files. With the connector extension enabled, your data tables 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: 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. 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. 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."},{"id":"docs:studio/logic/data-sources#account-sources","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#account-sources","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Account sources","text":"The auth extension adds two more, both about who is looking at the page: 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 . 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. Both extensions are covered from the other side — the tables, the settings, the secrets — in Databases and Auth and secrets ."},{"id":"docs:studio/logic/data-sources#external-sources","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#external-sources","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"External sources","text":"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. Every 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 . The table sources are rewritten at build time into ordinary fetches against /_jx/data , so no extension code ships to the browser."},{"id":"docs:studio/logic/data-sources#next","collection":"docs","slug":"studio/logic/data-sources","url":"/docs/studio/logic/data-sources/#next","title":"Data sources","description":"The reactive data sources in Jx Studio — Request, browser storage, FormData, ContentCollection — plus the database and account sources extensions add.","heading":"Next","text":"Watch a source resolve, live, in the Data explorer Compute over fetched data with Formulas and expressions Content types themselves are managed in Content types"},{"id":"docs:studio/logic/events","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"","text":"Events panel Events 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. Add a binding Click 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: The event name — pick from onclick , oninput , onchange , onsubmit , onkeydown , onkeyup , onfocus , onblur , onmouseenter , and onmouseleave . The mode — one of the three ways to respond, below. A trash button that removes the binding. Three ways to respond $ref — call a named function. A picker lists the functions declared in the State panel ; pick one and the event runs it. This is the tidiest option when the same behavior is used in more than one place. $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 . inline — a handler written on the element itself. A Statements / Code toggle picks how you write it: as visual statement cards, or as JavaScript in a small text field with an Open in editor button for the real code editor . Switching modes replaces the binding with a fresh start in the new mode; undo restores the previous one. Read values from the event Handlers 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: event#/target/value — what the visitor has typed into the field. The classic oninput pattern is one step: set $searchText to event#/target/value . event#/detail — the data a component sent along when it dispatched a custom event. An 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 ; the pickers offer the two common ones. Component events When 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 . A 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 . Next Declare the functions your events call in the State panel Watch state change as events fire, in the Data explorer Multi-step handlers read best as Statements"},{"id":"docs:studio/logic/events#events-panel","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#events-panel","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Events panel","text":"Events 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."},{"id":"docs:studio/logic/events#add-a-binding","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#add-a-binding","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Add a binding","text":"Click 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: The event name — pick from onclick , oninput , onchange , onsubmit , onkeydown , onkeyup , onfocus , onblur , onmouseenter , and onmouseleave . The mode — one of the three ways to respond, below. A trash button that removes the binding."},{"id":"docs:studio/logic/events#three-ways-to-respond","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#three-ways-to-respond","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Three ways to respond","text":"$ref — call a named function. A picker lists the functions declared in the State panel ; pick one and the event runs it. This is the tidiest option when the same behavior is used in more than one place. $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 . inline — a handler written on the element itself. A Statements / Code toggle picks how you write it: as visual statement cards, or as JavaScript in a small text field with an Open in editor button for the real code editor . Switching modes replaces the binding with a fresh start in the new mode; undo restores the previous one."},{"id":"docs:studio/logic/events#read-values-from-the-event","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#read-values-from-the-event","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Read values from the event","text":"Handlers 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: event#/target/value — what the visitor has typed into the field. The classic oninput pattern is one step: set $searchText to event#/target/value . event#/detail — the data a component sent along when it dispatched a custom event. An 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 ; the pickers offer the two common ones."},{"id":"docs:studio/logic/events#component-events","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#component-events","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Component events","text":"When 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 . A 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 ."},{"id":"docs:studio/logic/events#next","collection":"docs","slug":"studio/logic/events","url":"/docs/studio/logic/events/#next","title":"Events panel","description":"Make elements respond to clicks, typing, and form submits with the Events tab — bind functions, formulas, or inline handlers, and read event values.","heading":"Next","text":"Declare the functions your events call in the State panel Watch state change as events fire, in the Data explorer Multi-step handlers read best as Statements"},{"id":"docs:studio/logic/formula-workspace","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"","text":"Formula workspace The 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. Open it Click the full-screen icon beside any formula: On an Expression entry in the State panel . On an $expression event binding in the Events panel . The 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. The layout Header — the formula's name and kind (a state expression or an event expression), a Catalog button that opens the formula palette , and Close . Chip strip — the whole formula as a left-to-right pipeline of chips, each with its live value badge. This is the map. 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. 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 would show it. You build the formula while looking at the data it consumes. 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. Navigate by chip Click 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. Live-context previews The 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: An event formula that lives inside a repeated list is previewed with the first item's data, so $map/item references show real values. 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. Insert from the catalog The 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 . Next The editing vocabulary itself: Formulas and expressions Multi-step behavior belongs in Statements , not one giant formula Check what the page's data actually looks like in the Data explorer"},{"id":"docs:studio/logic/formula-workspace#formula-workspace","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#formula-workspace","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Formula workspace","text":"The 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."},{"id":"docs:studio/logic/formula-workspace#open-it","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#open-it","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Open it","text":"Click the full-screen icon beside any formula: On an Expression entry in the State panel . On an $expression event binding in the Events panel . The 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."},{"id":"docs:studio/logic/formula-workspace#the-layout","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#the-layout","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"The layout","text":"Header — the formula's name and kind (a state expression or an event expression), a Catalog button that opens the formula palette , and Close . Chip strip — the whole formula as a left-to-right pipeline of chips, each with its live value badge. This is the map. 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. 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 would show it. You build the formula while looking at the data it consumes. 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."},{"id":"docs:studio/logic/formula-workspace#navigate-by-chip","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#navigate-by-chip","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Navigate by chip","text":"Click 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."},{"id":"docs:studio/logic/formula-workspace#live-context-previews","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#live-context-previews","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Live-context previews","text":"The 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: An event formula that lives inside a repeated list is previewed with the first item's data, so $map/item references show real values. 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."},{"id":"docs:studio/logic/formula-workspace#insert-from-the-catalog","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#insert-from-the-catalog","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Insert from the catalog","text":"The 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 ."},{"id":"docs:studio/logic/formula-workspace#next","collection":"docs","slug":"studio/logic/formula-workspace","url":"/docs/studio/logic/formula-workspace/#next","title":"Formula workspace","description":"Edit a formula full-screen in Jx Studio — chip navigation, a live data rail, instant results, and the formula catalog side by side.","heading":"Next","text":"The editing vocabulary itself: Formulas and expressions Multi-step behavior belongs in Statements , not one giant formula Check what the page's data actually looks like in the Data explorer"},{"id":"docs:studio/logic/formulas","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/","title":"Formulas and expressions","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.","heading":"","text":"Formulas and expressions A 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. The field-mode button: any value can be dynamic Bindable 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: abc — a fixed value you type. The default. $ref — follow a state value: pick an entry from the State panel and the field always shows its current value. ${} — a template mixing fixed text and values, like Hello ${state.$name} . fx — a full formula, edited right in the row. Each 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. Formulas also appear as their own state entries ( + Add… > Expression in the State panel) and as event handlers (the $expression mode in the Events panel ). The formula editor A formula is a tree of small operations, and the editor edits one operation at a time: 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 . 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. Operators 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. Chips and live value badges Above 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 . Next 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. In the compact inline editor the chips are a summary; clicking them to navigate is what the formula workspace is for. The formula palette You 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: Type to filter by name, group, or description; ↓ and ↑ move through results and Enter inserts the highlighted entry. Formulas lists the named formulas already defined in this file. Formulas library lists ready-made formulas that ship with Jx — average , capitalize , and friends. The full generated list is the formula catalog . The remaining groups are the operators themselves, plus the blessed standard-library functions ( Math.max , JSON.stringify , …) callable from formulas. Picking an entry replaces the current step with that operation, ready for you to fill in its operands. Library 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. Named formulas An 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 . That's how you build a vocabulary: define discountedPrice once, call it everywhere. Next Give a big formula the whole canvas in the Formula workspace Every operator, defined precisely: Operator reference Every packaged formula: Formula catalog"},{"id":"docs:studio/logic/formulas#formulas-and-expressions","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#formulas-and-expressions","title":"Formulas and expressions","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.","heading":"Formulas and expressions","text":"A 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."},{"id":"docs:studio/logic/formulas#the-field-mode-button-any-value-can-be-dynamic","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#the-field-mode-button-any-value-can-be-dynamic","title":"Formulas and expressions","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.","heading":"The field-mode button: any value can be dynamic","text":"Bindable 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: abc — a fixed value you type. The default. $ref — follow a state value: pick an entry from the State panel and the field always shows its current value. ${} — a template mixing fixed text and values, like Hello ${state.$name} . fx — a full formula, edited right in the row. Each 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. Formulas also appear as their own state entries ( + Add… > Expression in the State panel) and as event handlers (the $expression mode in the Events panel )."},{"id":"docs:studio/logic/formulas#the-formula-editor","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#the-formula-editor","title":"Formulas and expressions","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.","heading":"The formula editor","text":"A formula is a tree of small operations, and the editor edits one operation at a time: 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 . 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. Operators 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."},{"id":"docs:studio/logic/formulas#chips-and-live-value-badges","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#chips-and-live-value-badges","title":"Formulas and expressions","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.","heading":"Chips and live value badges","text":"Above 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 . Next 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. In the compact inline editor the chips are a summary; clicking them to navigate is what the formula workspace is for."},{"id":"docs:studio/logic/formulas#the-formula-palette","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#the-formula-palette","title":"Formulas and expressions","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.","heading":"The formula palette","text":"You 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: Type to filter by name, group, or description; ↓ and ↑ move through results and Enter inserts the highlighted entry. Formulas lists the named formulas already defined in this file. Formulas library lists ready-made formulas that ship with Jx — average , capitalize , and friends. The full generated list is the formula catalog . The remaining groups are the operators themselves, plus the blessed standard-library functions ( Math.max , JSON.stringify , …) callable from formulas. Picking an entry replaces the current step with that operation, ready for you to fill in its operands. Library 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."},{"id":"docs:studio/logic/formulas#named-formulas","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#named-formulas","title":"Formulas and expressions","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.","heading":"Named formulas","text":"An 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 . That's how you build a vocabulary: define discountedPrice once, call it everywhere."},{"id":"docs:studio/logic/formulas#next","collection":"docs","slug":"studio/logic/formulas","url":"/docs/studio/logic/formulas/#next","title":"Formulas and expressions","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.","heading":"Next","text":"Give a big formula the whole canvas in the Formula workspace Every operator, defined precisely: Operator reference Every packaged formula: Formula catalog"},{"id":"docs:studio/logic/state","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"","text":"State panel State 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. Every entry belongs to the open file — each page or component carries its own state, saved inside its own JSON file. Read the list Entries are grouped into collapsible sections with counts — State , Computed , Data , Expressions , Functions — and only sections with entries appear. Each row shows: A colored badge for the kind of entry ( S state, C computed, E expression, F function; data sources show their kind's initial). The entry's name. A short hint — a Request's method and URL, a storage entry's key, the first line of a function. Click a row to expand its editor in place; click again to collapse it. A file with no entries yet shows \"No state defined\". Add an entry Click the + Add… picker at the bottom of the panel. Choose what to add: State Signal — a plain value the component holds (text, a number, a flag, a list). Computed — a value derived from other entries, recalculated automatically. Fetch (Request) , LocalStorage , SessionStorage , IndexedDB , Cookie , Set , Map , FormData — the built-in data sources, covered in Data sources . External Module… — a data source provided by a JavaScript module you point at. Any sources your project imports or its extensions provide (for example ContentCollection ) appear next. Expression — a named formula, covered in Formulas and expressions . Function — a reusable piece of behavior, covered in Statements and Code editing . The new entry appears with a placeholder name and its editor open — rename it first. Rename and delete To rename, edit the Name field at the top of an entry's editor. The change commits when you press 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. To delete, click the trash icon on the entry's row. Plain values A State Signal is the workhorse — the counter, the search text, the \"menu open\" flag. Its editor offers: Type — string , integer , number , boolean , array , or object . Defaults you type are converted to match. Format — for strings only: image , date , or color . An image-formatted value gets a media picker for its default. Default — the starting value. Array and object defaults are typed as JSON. Description — a note to your future self, also shown when the component is used elsewhere. Computed values A 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. Functions A Function entry is behavior you can bind to events or call from formulas. Its editor offers: Description — what the function does. Parameters — type a name and press Enter to add one as a chip; Advanced switches to full rows with a type, description, and optional flag per parameter. Body — with a Statements / Code toggle: build the body as visual steps, or write JavaScript. See Statements and Code editing . A function stored in a separate file shows Source and Export fields instead of a body — see the sidecar section of Code editing . Components: props, attributes, and events When 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: On plain values: Attribute (the HTML attribute that sets this value), Reflects , and Deprecated . 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 wherever the component is used. Name 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. Everything 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 and Reactivity . Next Watch these entries carry real values in the Data explorer Feed them from files, APIs, and the browser with Data sources Bind them to clicks and keystrokes in the Events panel"},{"id":"docs:studio/logic/state#state-panel","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#state-panel","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"State panel","text":"State 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. Every entry belongs to the open file — each page or component carries its own state, saved inside its own JSON file."},{"id":"docs:studio/logic/state#read-the-list","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#read-the-list","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Read the list","text":"Entries are grouped into collapsible sections with counts — State , Computed , Data , Expressions , Functions — and only sections with entries appear. Each row shows: A colored badge for the kind of entry ( S state, C computed, E expression, F function; data sources show their kind's initial). The entry's name. A short hint — a Request's method and URL, a storage entry's key, the first line of a function. Click a row to expand its editor in place; click again to collapse it. A file with no entries yet shows \"No state defined\"."},{"id":"docs:studio/logic/state#add-an-entry","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#add-an-entry","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Add an entry","text":"Click the + Add… picker at the bottom of the panel. Choose what to add: State Signal — a plain value the component holds (text, a number, a flag, a list). Computed — a value derived from other entries, recalculated automatically. Fetch (Request) , LocalStorage , SessionStorage , IndexedDB , Cookie , Set , Map , FormData — the built-in data sources, covered in Data sources . External Module… — a data source provided by a JavaScript module you point at. Any sources your project imports or its extensions provide (for example ContentCollection ) appear next. Expression — a named formula, covered in Formulas and expressions . Function — a reusable piece of behavior, covered in Statements and Code editing . The new entry appears with a placeholder name and its editor open — rename it first."},{"id":"docs:studio/logic/state#rename-and-delete","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#rename-and-delete","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Rename and delete","text":"To rename, edit the Name field at the top of an entry's editor. The change commits when you press 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. To delete, click the trash icon on the entry's row."},{"id":"docs:studio/logic/state#plain-values","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#plain-values","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Plain values","text":"A State Signal is the workhorse — the counter, the search text, the \"menu open\" flag. Its editor offers: Type — string , integer , number , boolean , array , or object . Defaults you type are converted to match. Format — for strings only: image , date , or color . An image-formatted value gets a media picker for its default. Default — the starting value. Array and object defaults are typed as JSON. Description — a note to your future self, also shown when the component is used elsewhere."},{"id":"docs:studio/logic/state#computed-values","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#computed-values","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Computed values","text":"A 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."},{"id":"docs:studio/logic/state#functions","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#functions","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Functions","text":"A Function entry is behavior you can bind to events or call from formulas. Its editor offers: Description — what the function does. Parameters — type a name and press Enter to add one as a chip; Advanced switches to full rows with a type, description, and optional flag per parameter. Body — with a Statements / Code toggle: build the body as visual steps, or write JavaScript. See Statements and Code editing . A function stored in a separate file shows Source and Export fields instead of a body — see the sidecar section of Code editing ."},{"id":"docs:studio/logic/state#components-props-attributes-and-events","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#components-props-attributes-and-events","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Components: props, attributes, and events","text":"When 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: On plain values: Attribute (the HTML attribute that sets this value), Reflects , and Deprecated . 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 wherever the component is used. Name 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. Everything 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 and Reactivity ."},{"id":"docs:studio/logic/state#next","collection":"docs","slug":"studio/logic/state","url":"/docs/studio/logic/state/#next","title":"State panel","description":"Declare what a page or component knows in the State panel — values, computed entries, data sources, and functions, plus rename, delete, and privacy.","heading":"Next","text":"Watch these entries carry real values in the Data explorer Feed them from files, APIs, and the browser with Data sources Bind them to clicks and keystrokes in the Events panel"},{"id":"docs:studio/logic/statements","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"","text":"Statements Statements 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. Where Studio offers it Anywhere a function body appears, a Statements / Code toggle picks the representation: A Function entry's Body in the State panel . An inline event handler in the Events panel . Statements is the structured editor described here; Code is a JavaScript text body — see Code editing . The toggle switches representations; it does not translate between them. Picking the other mode replaces the current body with an empty one — undo ( ⌘Z / Ctrl+Z ) brings the old body back. Add steps Every list of steps ends in a + Add statement picker with five kinds: 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 can produce. Call function — run another function from your state, with rows for the arguments to pass it. If / Else — run different steps depending on a condition. Switch — pick one of several branches by matching a value. Dispatch event — send an event out of a component, so the page using it can react. Each card has a header naming its kind, a delete button, and a drag handle (⠿) — drag cards to reorder them within their list. Branch with If / Else An If / Else card holds: An If row — the condition, written as an operand: a state value, or a nested comparison formula. A Then lane — an indented list of steps with its own + Add statement picker, run when the condition holds. Optionally an Else lane — click + Add else to add it, or the remove button on the lane to drop it. Lanes nest: a step inside Then can itself be an If / Else or a Switch . Branch with Switch A Switch card matches one value against several cases: Switch on — the value to examine. One lane per case, each labeled with the value it matches (edit the label field to change it). + Add case appends another. A Default lane for when nothing matches. Dispatch an event A Dispatch event card sends a custom event from a component — the counterpart of the Emits list on a function in the State panel: Event — the event's name. In a component whose functions declare emitted events, this is a combo box offering the declared names. Detail — the data to send along, as an operand (a state value, a literal, or a formula). Options — Bubbles and Composed checkboxes controlling how far the event travels. Pages using the component can then bind that event in their own Events panel and read the payload as event#/detail . Statement 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. Next Bind a statement-bodied function to a click in the Events panel When a body outgrows steps, switch to Code editing The formula vocabulary inside each step: Formulas and expressions"},{"id":"docs:studio/logic/statements#statements","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#statements","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Statements","text":"Statements 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."},{"id":"docs:studio/logic/statements#where-studio-offers-it","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#where-studio-offers-it","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Where Studio offers it","text":"Anywhere a function body appears, a Statements / Code toggle picks the representation: A Function entry's Body in the State panel . An inline event handler in the Events panel . Statements is the structured editor described here; Code is a JavaScript text body — see Code editing . The toggle switches representations; it does not translate between them. Picking the other mode replaces the current body with an empty one — undo ( ⌘Z / Ctrl+Z ) brings the old body back."},{"id":"docs:studio/logic/statements#add-steps","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#add-steps","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Add steps","text":"Every list of steps ends in a + Add statement picker with five kinds: 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 can produce. Call function — run another function from your state, with rows for the arguments to pass it. If / Else — run different steps depending on a condition. Switch — pick one of several branches by matching a value. Dispatch event — send an event out of a component, so the page using it can react. Each card has a header naming its kind, a delete button, and a drag handle (⠿) — drag cards to reorder them within their list."},{"id":"docs:studio/logic/statements#branch-with-if-else","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#branch-with-if-else","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Branch with If / Else","text":"An If / Else card holds: An If row — the condition, written as an operand: a state value, or a nested comparison formula. A Then lane — an indented list of steps with its own + Add statement picker, run when the condition holds. Optionally an Else lane — click + Add else to add it, or the remove button on the lane to drop it. Lanes nest: a step inside Then can itself be an If / Else or a Switch ."},{"id":"docs:studio/logic/statements#branch-with-switch","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#branch-with-switch","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Branch with Switch","text":"A Switch card matches one value against several cases: Switch on — the value to examine. One lane per case, each labeled with the value it matches (edit the label field to change it). + Add case appends another. A Default lane for when nothing matches."},{"id":"docs:studio/logic/statements#dispatch-an-event","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#dispatch-an-event","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Dispatch an event","text":"A Dispatch event card sends a custom event from a component — the counterpart of the Emits list on a function in the State panel: Event — the event's name. In a component whose functions declare emitted events, this is a combo box offering the declared names. Detail — the data to send along, as an operand (a state value, a literal, or a formula). Options — Bubbles and Composed checkboxes controlling how far the event travels. Pages using the component can then bind that event in their own Events panel and read the payload as event#/detail . Statement 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."},{"id":"docs:studio/logic/statements#next","collection":"docs","slug":"studio/logic/statements","url":"/docs/studio/logic/statements/#next","title":"Statements","description":"Write function bodies as visual steps — set state, call functions, branch with if and switch, and dispatch events, no JavaScript required.","heading":"Next","text":"Bind a statement-bodied function to a click in the Events panel When a body outgrows steps, switch to Code editing The formula vocabulary inside each step: Formulas and expressions"},{"id":"docs:studio/projects/browse","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"","text":"Manage Manage 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. Browse your project The 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. Right-click any file to open , rename , duplicate , or delete it. Create pages and content Use 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. Upload media Drag 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). Model your content Content 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 . Next Author content in Edit Design components in Design"},{"id":"docs:studio/projects/browse#manage","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#manage","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Manage","text":"Manage 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."},{"id":"docs:studio/projects/browse#browse-your-project","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#browse-your-project","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Browse your project","text":"The 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. Right-click any file to open , rename , duplicate , or delete it."},{"id":"docs:studio/projects/browse#create-pages-and-content","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#create-pages-and-content","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Create pages and content","text":"Use 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."},{"id":"docs:studio/projects/browse#upload-media","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#upload-media","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Upload media","text":"Drag 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)."},{"id":"docs:studio/projects/browse#model-your-content","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#model-your-content","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Model your content","text":"Content 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 ."},{"id":"docs:studio/projects/browse#next","collection":"docs","slug":"studio/projects/browse","url":"/docs/studio/projects/browse/#next","title":"Browse your project","description":"The Manage surface in Jx Studio: browse your project, create pages and content, upload media, and define content models.","heading":"Next","text":"Author content in Edit Design components in Design"},{"id":"docs:studio/projects/content-types","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/","title":"Content types","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.","heading":"","text":"Content types Content 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. Create a content type Click New Entry at the bottom of the type list. Type a name — \"Blog Posts\" becomes blog-posts — and click Create . The new type starts with a matching source folder ( content/blog-posts/ ) and an empty field schema, and is selected ready to edit. Source and format Source — the project folder entries are read from. The default matches the type's name; change it to point anywhere inside your project. 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. Build the field schema The fields you add here become the form every entry fills in. Each field row has: A name — click it to rename. A type — string (text), number, boolean (yes/no), array (a list), object (a group of sub-fields), or reference. A format for string and array fields — image , date , or color — which upgrades the field's editor: image fields get the media picker , date fields a date, and so on. A Req toggle marking the field as required. An 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. Rename or delete a type With 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. Create entries from Manage Back in the Manage view , the New menu lists an item for every content type you've defined: Click New and pick the type — for example Blog-posts . 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. Entries 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 . Studio 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 . Next Browse your project — the Manage view where entries are created Project settings — the rest of the Settings modal"},{"id":"docs:studio/projects/content-types#content-types","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#content-types","title":"Content types","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.","heading":"Content types","text":"Content 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."},{"id":"docs:studio/projects/content-types#create-a-content-type","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#create-a-content-type","title":"Content types","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.","heading":"Create a content type","text":"Click New Entry at the bottom of the type list. Type a name — \"Blog Posts\" becomes blog-posts — and click Create . The new type starts with a matching source folder ( content/blog-posts/ ) and an empty field schema, and is selected ready to edit."},{"id":"docs:studio/projects/content-types#source-and-format","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#source-and-format","title":"Content types","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.","heading":"Source and format","text":"Source — the project folder entries are read from. The default matches the type's name; change it to point anywhere inside your project. 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."},{"id":"docs:studio/projects/content-types#build-the-field-schema","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#build-the-field-schema","title":"Content types","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.","heading":"Build the field schema","text":"The fields you add here become the form every entry fills in. Each field row has: A name — click it to rename. A type — string (text), number, boolean (yes/no), array (a list), object (a group of sub-fields), or reference. A format for string and array fields — image , date , or color — which upgrades the field's editor: image fields get the media picker , date fields a date, and so on. A Req toggle marking the field as required. An 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."},{"id":"docs:studio/projects/content-types#rename-or-delete-a-type","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#rename-or-delete-a-type","title":"Content types","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.","heading":"Rename or delete a type","text":"With 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."},{"id":"docs:studio/projects/content-types#create-entries-from-manage","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#create-entries-from-manage","title":"Content types","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.","heading":"Create entries from Manage","text":"Back in the Manage view , the New menu lists an item for every content type you've defined: Click New and pick the type — for example Blog-posts . 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. Entries 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 . Studio 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 ."},{"id":"docs:studio/projects/content-types#next","collection":"docs","slug":"studio/projects/content-types","url":"/docs/studio/projects/content-types/#next","title":"Content types","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.","heading":"Next","text":"Browse your project — the Manage view where entries are created Project settings — the rest of the Settings modal"},{"id":"docs:studio/projects/create","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"","text":"Create a project The 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 with New Project… , or from the dropdown beside Open Project in the toolbar. Step 1: choose a source The first screen — Start new project from: — has up to four tabs: Template — a minimal skeleton to build on. Four options: Blank — start from scratch. Desktop First — an empty project preset with max-width breakpoints, so you design the large layout first and adapt down. Mobile First — the same, with min-width breakpoints: design the phone layout first and adapt up. Mobile App — an app shell with bottom navigation. 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 . 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. 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. Pick a source and click Next . Step 2: fill in the parameters The second screen — New Project Parameters — shows which source you picked, then the project's identity: Project Name (required) — the human-readable name, e.g. \"My Site\". 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. 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. Description — a short line about the site. Production URL — where the site will live once published, e.g. https://example.com . 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 . Under Location and Directory , Studio shows exactly where the project will land — /home/you/Sites/my-site — before you commit to it. On 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. On Jx Cloud Cloud projects are GitHub repositories, so the same step asks for a repository location instead of a folder: Owner (required) — the account the repository is created under: your personal account, or any organization you've installed the Jx Suite app on. 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. Visibility — Private (the default) or Public . The preview line shows the repository that will be created, e.g. acme/my-site . The design quickstart Below 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: Colors — pick an Accent , Background , and Text color with a swatch or by typing a value. Fonts — a Body Font and Heading Font . Logo — upload an image (SVG, PNG, JPEG, WebP, GIF, or ICO); Studio copies it into the project's public/ folder. 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. Create it Click 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. If 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. Studio 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 . There 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. Next Projects — the other ways to get a project: open a folder or clone a repository Browse your project — find your way around what was just created Starter templates — the full starter gallery"},{"id":"docs:studio/projects/create#create-a-project","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#create-a-project","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"Create a project","text":"The 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 with New Project… , or from the dropdown beside Open Project in the toolbar."},{"id":"docs:studio/projects/create#step-1-choose-a-source","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#step-1-choose-a-source","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"Step 1: choose a source","text":"The first screen — Start new project from: — has up to four tabs: Template — a minimal skeleton to build on. Four options: Blank — start from scratch. Desktop First — an empty project preset with max-width breakpoints, so you design the large layout first and adapt down. Mobile First — the same, with min-width breakpoints: design the phone layout first and adapt up. Mobile App — an app shell with bottom navigation. 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 . 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. 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. Pick a source and click Next ."},{"id":"docs:studio/projects/create#step-2-fill-in-the-parameters","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#step-2-fill-in-the-parameters","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"Step 2: fill in the parameters","text":"The second screen — New Project Parameters — shows which source you picked, then the project's identity: Project Name (required) — the human-readable name, e.g. \"My Site\". 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. 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. Description — a short line about the site. Production URL — where the site will live once published, e.g. https://example.com . 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 . Under Location and Directory , Studio shows exactly where the project will land — /home/you/Sites/my-site — before you commit to it. On 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."},{"id":"docs:studio/projects/create#on-jx-cloud","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#on-jx-cloud","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"On Jx Cloud","text":"Cloud projects are GitHub repositories, so the same step asks for a repository location instead of a folder: Owner (required) — the account the repository is created under: your personal account, or any organization you've installed the Jx Suite app on. 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. Visibility — Private (the default) or Public . The preview line shows the repository that will be created, e.g. acme/my-site ."},{"id":"docs:studio/projects/create#the-design-quickstart","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#the-design-quickstart","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"The design quickstart","text":"Below 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: Colors — pick an Accent , Background , and Text color with a swatch or by typing a value. Fonts — a Body Font and Heading Font . Logo — upload an image (SVG, PNG, JPEG, WebP, GIF, or ICO); Studio copies it into the project's public/ folder. 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."},{"id":"docs:studio/projects/create#create-it","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#create-it","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"Create it","text":"Click 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. If 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. Studio 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 . There 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."},{"id":"docs:studio/projects/create#next","collection":"docs","slug":"studio/projects/create","url":"/docs/studio/projects/create/#next","title":"Create a project","description":"Every field of the New Project modal in Jx Studio — templates, starter sites, import and agent sources, project details, and the design quickstart.","heading":"Next","text":"Projects — the other ways to get a project: open a folder or clone a repository Browse your project — find your way around what was just created Starter templates — the full starter gallery"},{"id":"docs:studio/projects/dependencies","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/","title":"Dependencies and imports","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.","heading":"","text":"Dependencies and imports Imports 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. Two contexts The panel follows whatever tab is active: A page, layout, or component open — you're managing that document's imports: which components it can place. project.json open — you're managing the whole site: packages, site-wide component availability, and class imports. Import your own components With 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. If you mostly build by placing blocks from the Elements panel , you rarely open this list — it's the same wiring, made visible. Add an npm package With project.json open: Type the package's name into Add Dependency and press Enter . Studio installs it. If the package publishes a component catalog (a custom elements manifest), a new section appears for it, listing every component inside. Packages can also be added from Settings > Dependencies — same result, different door. See Project settings . Cherry-pick components Under 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: Tick a component in the project.json context to make it available across the site. Tick it with a page, layout, or component open to enable it for that document only. The × in a package's header (site context) removes the package entirely, along with everything you'd picked from it. Class imports The 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. Stay up to date Settings > Dependencies shows each package's current and latest version, with per-row update buttons and Update all . 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. 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. Packages 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. Next Project settings — the Dependencies section of the Settings modal Pages, layouts, and components — what components are and when to make your own"},{"id":"docs:studio/projects/dependencies#dependencies-and-imports","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#dependencies-and-imports","title":"Dependencies and imports","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.","heading":"Dependencies and imports","text":"Imports 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."},{"id":"docs:studio/projects/dependencies#two-contexts","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#two-contexts","title":"Dependencies and imports","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.","heading":"Two contexts","text":"The panel follows whatever tab is active: A page, layout, or component open — you're managing that document's imports: which components it can place. project.json open — you're managing the whole site: packages, site-wide component availability, and class imports."},{"id":"docs:studio/projects/dependencies#import-your-own-components","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#import-your-own-components","title":"Dependencies and imports","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.","heading":"Import your own components","text":"With 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. If you mostly build by placing blocks from the Elements panel , you rarely open this list — it's the same wiring, made visible."},{"id":"docs:studio/projects/dependencies#add-an-npm-package","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#add-an-npm-package","title":"Dependencies and imports","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.","heading":"Add an npm package","text":"With project.json open: Type the package's name into Add Dependency and press Enter . Studio installs it. If the package publishes a component catalog (a custom elements manifest), a new section appears for it, listing every component inside. Packages can also be added from Settings > Dependencies — same result, different door. See Project settings ."},{"id":"docs:studio/projects/dependencies#cherry-pick-components","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#cherry-pick-components","title":"Dependencies and imports","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.","heading":"Cherry-pick components","text":"Under 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: Tick a component in the project.json context to make it available across the site. Tick it with a page, layout, or component open to enable it for that document only. The × in a package's header (site context) removes the package entirely, along with everything you'd picked from it."},{"id":"docs:studio/projects/dependencies#class-imports","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#class-imports","title":"Dependencies and imports","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.","heading":"Class imports","text":"The 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."},{"id":"docs:studio/projects/dependencies#stay-up-to-date","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#stay-up-to-date","title":"Dependencies and imports","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.","heading":"Stay up to date","text":"Settings > Dependencies shows each package's current and latest version, with per-row update buttons and Update all . 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. 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. Packages 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."},{"id":"docs:studio/projects/dependencies#next","collection":"docs","slug":"studio/projects/dependencies","url":"/docs/studio/projects/dependencies/#next","title":"Dependencies and imports","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.","heading":"Next","text":"Project settings — the Dependencies section of the Settings modal Pages, layouts, and components — what components are and when to make your own"},{"id":"docs:studio/projects/media","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/","title":"Media","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.","heading":"","text":"Media Your 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. Upload files 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. The new files appear in the Media section immediately, ready to use. Studio 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. Uploads 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 . Pick media in a panel Wherever 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. Click the browse (image) button beside the field. A searchable list of your project's media opens, with a thumbnail beside each image. Type to filter. Click a file to use it — Studio fills in its path for you. You can also type into the path field directly, either a path from your project or a full web address. What the build does to images You 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 . Next Browse your project — the rest of the Manage view Publish — how the optimized site goes live"},{"id":"docs:studio/projects/media#media","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/#media","title":"Media","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.","heading":"Media","text":"Your 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."},{"id":"docs:studio/projects/media#upload-files","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/#upload-files","title":"Media","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.","heading":"Upload files","text":"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. The new files appear in the Media section immediately, ready to use. Studio 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. Uploads 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 ."},{"id":"docs:studio/projects/media#pick-media-in-a-panel","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/#pick-media-in-a-panel","title":"Media","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.","heading":"Pick media in a panel","text":"Wherever 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. Click the browse (image) button beside the field. A searchable list of your project's media opens, with a thumbnail beside each image. Type to filter. Click a file to use it — Studio fills in its path for you. You can also type into the path field directly, either a path from your project or a full web address."},{"id":"docs:studio/projects/media#what-the-build-does-to-images","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/#what-the-build-does-to-images","title":"Media","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.","heading":"What the build does to images","text":"You 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 ."},{"id":"docs:studio/projects/media#next","collection":"docs","slug":"studio/projects/media","url":"/docs/studio/projects/media/#next","title":"Media","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.","heading":"Next","text":"Browse your project — the rest of the Manage view Publish — how the optimized site goes live"},{"id":"docs:studio/projects/pages-layouts-components","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/","title":"Pages, layouts, and components","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.","heading":"","text":"Pages, layouts, and components Everything 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. Pages A 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. Routing details — dynamic addresses, catch-alls — live in Site architecture . Layouts A 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. Reach for a layout when you catch yourself rebuilding the same surroundings on a second page. Components A 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. Reach 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 . Create one All three are created the same way, from the Manage view : Click Manage in the toolbar. Click New and choose Page , Layout , or Component . (The menu also lists your project's content types — see Browse your project .) 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. Studio 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. Which one do I want? A destination people should be able to visit → a page . The frame that repeats around many pages → a layout . An element that repeats within pages → a component . When 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. Next Design mode — style what you just created on the live canvas Edit mode — write content inline Site architecture — the folder-by-folder anatomy underneath"},{"id":"docs:studio/projects/pages-layouts-components#pages-layouts-and-components","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#pages-layouts-and-components","title":"Pages, layouts, and components","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.","heading":"Pages, layouts, and components","text":"Everything 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."},{"id":"docs:studio/projects/pages-layouts-components#pages","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#pages","title":"Pages, layouts, and components","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.","heading":"Pages","text":"A 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. Routing details — dynamic addresses, catch-alls — live in Site architecture ."},{"id":"docs:studio/projects/pages-layouts-components#layouts","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#layouts","title":"Pages, layouts, and components","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.","heading":"Layouts","text":"A 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. Reach for a layout when you catch yourself rebuilding the same surroundings on a second page."},{"id":"docs:studio/projects/pages-layouts-components#components","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#components","title":"Pages, layouts, and components","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.","heading":"Components","text":"A 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. Reach 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 ."},{"id":"docs:studio/projects/pages-layouts-components#create-one","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#create-one","title":"Pages, layouts, and components","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.","heading":"Create one","text":"All three are created the same way, from the Manage view : Click Manage in the toolbar. Click New and choose Page , Layout , or Component . (The menu also lists your project's content types — see Browse your project .) 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. Studio 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."},{"id":"docs:studio/projects/pages-layouts-components#which-one-do-i-want","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#which-one-do-i-want","title":"Pages, layouts, and components","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.","heading":"Which one do I want?","text":"A destination people should be able to visit → a page . The frame that repeats around many pages → a layout . An element that repeats within pages → a component . When 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."},{"id":"docs:studio/projects/pages-layouts-components#next","collection":"docs","slug":"studio/projects/pages-layouts-components","url":"/docs/studio/projects/pages-layouts-components/#next","title":"Pages, layouts, and components","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.","heading":"Next","text":"Design mode — style what you just created on the live canvas Edit mode — write content inline Site architecture — the folder-by-folder anatomy underneath"},{"id":"docs:studio/projects/settings","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"","text":"Project settings The 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. Changes save as you make them — there is no separate save button. General The basics of the site: Favicon — click Upload Favicon and pick an image; Studio copies it into your project and shows the current one beside the button. 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 . 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 for what each one writes into dist/ . 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. Global Styles — a shortcut that opens the project file where site-wide default styles live. Head What goes into every page's <head> — the invisible part of a web page that loads fonts, styles, and services: 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. 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. CSS Variables Your 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. Definitions Reusable 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 instead. Dependencies The npm packages your project uses, as a table of name, current version, and the latest available version: Type a package name and click Add to install one. A refresh icon appears on any row with an update available; Update all takes every row to its latest at once. Reinstall re-installs everything from scratch — the fix-it button if packages ever end up in a bad state. Adding packages and choosing which of their components your site uses is covered in Dependencies and imports . Sections added by extensions Extensions 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 . Every 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 . Next Content types — model your content in the Content Types section Dependencies and imports — packages and component imports in depth"},{"id":"docs:studio/projects/settings#project-settings","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#project-settings","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Project settings","text":"The 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. Changes save as you make them — there is no separate save button."},{"id":"docs:studio/projects/settings#general","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#general","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"General","text":"The basics of the site: Favicon — click Upload Favicon and pick an image; Studio copies it into your project and shows the current one beside the button. 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 . 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 for what each one writes into dist/ . 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. Global Styles — a shortcut that opens the project file where site-wide default styles live."},{"id":"docs:studio/projects/settings#head","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#head","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Head","text":"What goes into every page's <head> — the invisible part of a web page that loads fonts, styles, and services: 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. 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."},{"id":"docs:studio/projects/settings#css-variables","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#css-variables","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"CSS Variables","text":"Your 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."},{"id":"docs:studio/projects/settings#definitions","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#definitions","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Definitions","text":"Reusable 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 instead."},{"id":"docs:studio/projects/settings#dependencies","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#dependencies","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Dependencies","text":"The npm packages your project uses, as a table of name, current version, and the latest available version: Type a package name and click Add to install one. A refresh icon appears on any row with an update available; Update all takes every row to its latest at once. Reinstall re-installs everything from scratch — the fix-it button if packages ever end up in a bad state. Adding packages and choosing which of their components your site uses is covered in Dependencies and imports ."},{"id":"docs:studio/projects/settings#sections-added-by-extensions","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#sections-added-by-extensions","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Sections added by extensions","text":"Extensions 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 . Every 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 ."},{"id":"docs:studio/projects/settings#next","collection":"docs","slug":"studio/projects/settings","url":"/docs/studio/projects/settings/#next","title":"Project settings","description":"A tour of the Settings modal in Jx Studio: general options, head tags, CSS variables, definitions, dependencies, and extension-added sections.","heading":"Next","text":"Content types — model your content in the Content Types section Dependencies and imports — packages and component imports in depth"},{"id":"docs:studio/projects/starters","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"","text":"Starter templates Jx 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. Bistro & Café Restaurant & Food — A menu-driven site for a restaurant, café, or bakery. A 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. Menu content collection (Markdown) Hours & location Reservations CTA Responsive photo gallery Summit Heating & Cooling Home Services & Trades — Reliable HVAC repair and installation, backed by 24/7 service. A 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. Services grid powered by a content collection Prominent \"Get a Free Quote\" CTAs and quote form Customer testimonials and Licensed / Insured / 24-7 trust bar Rivertown Foundation Nonprofit & Community — Neighbors helping neighbors across Rivertown. A 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. Programs content collection Impact stats Prominent Donate CTA Volunteer & donation tiers Craft & Code Academy Education & Courses — Become a web developer in 14 weeks. A 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. Interactive curriculum accordion FAQ accordion Module content collection Pricing & enroll CTA Flowlark SaaS & Product — Plan, ship, and scale in one place. A 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. Interactive pricing toggle (monthly/annual) FAQ accordion island Plan comparison table & feature grid Horizon Conf 2026 Events & Conferences — A landing site for a one-day tech and design conference. A 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. Sessions content collection (Markdown) Interactive agenda-tabs island (filter by track) Save-the-date countdown strip Ticket pricing tiers The Long Field Blog & Publication — Slow essays on design, technology, and craft. A 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. Markdown posts collection Dynamic article routes (one page per post) Tags & dedicated reading layout Newsletter subscription CTA Aperture Studio Creative & Portfolio — Light, patience, and a good eye. An 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. Masonry project gallery with hover-zoom cards Dynamic per-project detail pages from a content collection Elegant image-forward editorial design Ember Yoga & Fitness Health & Wellness — Boutique yoga, pilates & strength for every body. A 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. Class schedule collection Membership pricing table & tier cards Booking/contact form Instructor profiles Meridian Advisory Professional Services — Numbers that move the business forward. A 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. Two layouts (marketing shell + article reader) Insights collection with dynamic per-article pages Team roster, services grid, and trust strip Cadence Cycles Retail & Shop — Find your next favorite ride. A 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. Filterable product collection grid Reactive category-filter island Dynamic per-product pages On-site service booking Northshore Realty Real Estate — Search homes on the north shore. A 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. CSV listings collection Split search hero + sidebar filter island Neighborhood tiles & data-forward listing cards Dynamic per-listing detail pages with gallery"},{"id":"docs:studio/projects/starters#starter-templates","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#starter-templates","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Starter templates","text":"Jx 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."},{"id":"docs:studio/projects/starters#bistro-caf","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#bistro-caf","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Bistro & Café","text":"Restaurant & Food — A menu-driven site for a restaurant, café, or bakery. A 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. Menu content collection (Markdown) Hours & location Reservations CTA Responsive photo gallery"},{"id":"docs:studio/projects/starters#summit-heating-cooling","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#summit-heating-cooling","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Summit Heating & Cooling","text":"Home Services & Trades — Reliable HVAC repair and installation, backed by 24/7 service. A 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. Services grid powered by a content collection Prominent \"Get a Free Quote\" CTAs and quote form Customer testimonials and Licensed / Insured / 24-7 trust bar"},{"id":"docs:studio/projects/starters#rivertown-foundation","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#rivertown-foundation","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Rivertown Foundation","text":"Nonprofit & Community — Neighbors helping neighbors across Rivertown. A 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. Programs content collection Impact stats Prominent Donate CTA Volunteer & donation tiers"},{"id":"docs:studio/projects/starters#craft-code-academy","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#craft-code-academy","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Craft & Code Academy","text":"Education & Courses — Become a web developer in 14 weeks. A 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. Interactive curriculum accordion FAQ accordion Module content collection Pricing & enroll CTA"},{"id":"docs:studio/projects/starters#flowlark","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#flowlark","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Flowlark","text":"SaaS & Product — Plan, ship, and scale in one place. A 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. Interactive pricing toggle (monthly/annual) FAQ accordion island Plan comparison table & feature grid"},{"id":"docs:studio/projects/starters#horizon-conf-2026","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#horizon-conf-2026","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Horizon Conf 2026","text":"Events & Conferences — A landing site for a one-day tech and design conference. A 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. Sessions content collection (Markdown) Interactive agenda-tabs island (filter by track) Save-the-date countdown strip Ticket pricing tiers"},{"id":"docs:studio/projects/starters#the-long-field","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#the-long-field","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"The Long Field","text":"Blog & Publication — Slow essays on design, technology, and craft. A 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. Markdown posts collection Dynamic article routes (one page per post) Tags & dedicated reading layout Newsletter subscription CTA"},{"id":"docs:studio/projects/starters#aperture-studio","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#aperture-studio","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Aperture Studio","text":"Creative & Portfolio — Light, patience, and a good eye. An 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. Masonry project gallery with hover-zoom cards Dynamic per-project detail pages from a content collection Elegant image-forward editorial design"},{"id":"docs:studio/projects/starters#ember-yoga-fitness","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#ember-yoga-fitness","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Ember Yoga & Fitness","text":"Health & Wellness — Boutique yoga, pilates & strength for every body. A 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. Class schedule collection Membership pricing table & tier cards Booking/contact form Instructor profiles"},{"id":"docs:studio/projects/starters#meridian-advisory","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#meridian-advisory","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Meridian Advisory","text":"Professional Services — Numbers that move the business forward. A 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. Two layouts (marketing shell + article reader) Insights collection with dynamic per-article pages Team roster, services grid, and trust strip"},{"id":"docs:studio/projects/starters#cadence-cycles","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#cadence-cycles","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Cadence Cycles","text":"Retail & Shop — Find your next favorite ride. A 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. Filterable product collection grid Reactive category-filter island Dynamic per-product pages On-site service booking"},{"id":"docs:studio/projects/starters#northshore-realty","collection":"docs","slug":"studio/projects/starters","url":"/docs/studio/projects/starters/#northshore-realty","title":"Starter templates","description":"The starter templates that ship with Jx Studio — what each includes and who it's for.","heading":"Northshore Realty","text":"Real Estate — Search homes on the north shore. A 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. CSV listings collection Split search hero + sidebar filter island Neighborhood tiles & data-forward listing cards Dynamic per-listing detail pages with gallery"},{"id":"docs:studio/publish/cloudflare","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/","title":"Publish to Cloudflare Pages","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.","heading":"","text":"Publish to Cloudflare Pages The 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. Before you start, the project needs to live on GitHub — if it doesn't yet, GitHub walks you through publishing it from Studio. Connect your Cloudflare account The first time you open the panel, it asks for a Cloudflare connection. What you see depends on your Studio platform: Connect Cloudflare — click it and sign in to Cloudflare in the window that opens. Done. 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. If 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. Create and connect a Pages project Once connected, the panel offers to create a Pages project tied to your repository: Account — pick your Cloudflare account. Pages project name — pre-filled from your project's name; this becomes part of your free site address. GitHub owner and GitHub repository — where the project lives. Production branch — the branch that publishes to your live site, normally main . Click Create & Connect . Studio 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. Watch deployments After connecting, the panel becomes a status view: The connected Pages project's name, with a link to your live site address. The latest deployment's stage and status — for example deploy: success — with a preview link to that exact build. Refresh re-checks; Disconnect removes the connection (your Pages project and site stay up — only the link from Studio is removed). There is no publish button, because publishing is automatic: every Commit and sync in Source control triggers a fresh build and deployment. Right after connecting, the panel shows No deployments yet — your next commit starts the first one. Sites with a database or sign-in Pages serves both halves of a Jx site, but a project with data tables or accounts needs three things arranged once: 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. Push the schema to the real database, from a terminal. While you develop, a D1 connection 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 covers where they come from. 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 covers the whole arrangement. Your 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. Studio 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. Next Source control — the commit-and-sync flow that triggers each deployment Other hosts — the same site on Netlify, GitHub Pages, or anywhere else"},{"id":"docs:studio/publish/cloudflare#publish-to-cloudflare-pages","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#publish-to-cloudflare-pages","title":"Publish to Cloudflare Pages","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.","heading":"Publish to Cloudflare Pages","text":"The 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. Before you start, the project needs to live on GitHub — if it doesn't yet, GitHub walks you through publishing it from Studio."},{"id":"docs:studio/publish/cloudflare#connect-your-cloudflare-account","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#connect-your-cloudflare-account","title":"Publish to Cloudflare Pages","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.","heading":"Connect your Cloudflare account","text":"The first time you open the panel, it asks for a Cloudflare connection. What you see depends on your Studio platform: Connect Cloudflare — click it and sign in to Cloudflare in the window that opens. Done. 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. If 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."},{"id":"docs:studio/publish/cloudflare#create-and-connect-a-pages-project","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#create-and-connect-a-pages-project","title":"Publish to Cloudflare Pages","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.","heading":"Create and connect a Pages project","text":"Once connected, the panel offers to create a Pages project tied to your repository: Account — pick your Cloudflare account. Pages project name — pre-filled from your project's name; this becomes part of your free site address. GitHub owner and GitHub repository — where the project lives. Production branch — the branch that publishes to your live site, normally main . Click Create & Connect . Studio 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."},{"id":"docs:studio/publish/cloudflare#watch-deployments","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#watch-deployments","title":"Publish to Cloudflare Pages","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.","heading":"Watch deployments","text":"After connecting, the panel becomes a status view: The connected Pages project's name, with a link to your live site address. The latest deployment's stage and status — for example deploy: success — with a preview link to that exact build. Refresh re-checks; Disconnect removes the connection (your Pages project and site stay up — only the link from Studio is removed). There is no publish button, because publishing is automatic: every Commit and sync in Source control triggers a fresh build and deployment. Right after connecting, the panel shows No deployments yet — your next commit starts the first one."},{"id":"docs:studio/publish/cloudflare#sites-with-a-database-or-sign-in","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#sites-with-a-database-or-sign-in","title":"Publish to Cloudflare Pages","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.","heading":"Sites with a database or sign-in","text":"Pages serves both halves of a Jx site, but a project with data tables or accounts needs three things arranged once: 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. Push the schema to the real database, from a terminal. While you develop, a D1 connection 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 covers where they come from. 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 covers the whole arrangement. Your 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. Studio 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."},{"id":"docs:studio/publish/cloudflare#next","collection":"docs","slug":"studio/publish/cloudflare","url":"/docs/studio/publish/cloudflare/#next","title":"Publish to Cloudflare Pages","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.","heading":"Next","text":"Source control — the commit-and-sync flow that triggers each deployment Other hosts — the same site on Netlify, GitHub Pages, or anywhere else"},{"id":"docs:studio/publish/collaboration","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/","title":"Real-time collaboration","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.","heading":"","text":"Real-time collaboration When 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. What you see A status pill in the toolbar — Live while the session is connected. It replaces the usual unsaved-changes dot for this tab. 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. Selections on the canvas — each peer's selected element is outlined in their color, labeled with their name, and follows them live. Cursors in Code view — in the Code mode the shared text carries every writer's caret and selection in their color, with their name on the caret. How co-editing behaves 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. Undo is yours alone. ⌘Z / Ctrl+Z steps back through your edits only — you can't undo what a teammate just did. Forms sync too. Page metadata and frontmatter fields co-edit the same way the canvas does. 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. 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. Syncing is not saving Your 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 saves open co-edited files first, so a commit always captures what the session currently sees. On 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. Which setups support it A shared dev server — the built-in case. Everyone who opens the same dev-server URL (see The 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. 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. The desktop app — always solo: it edits your local files directly and has no collaboration endpoint. Falling back to solo Collaboration degrades, never blocks: A backend without the endpoint simply gives you ordinary solo editing — no errors, no pill. If a session can't sync within a few seconds of opening, the tab proceeds solo. If the connection drops, the pill reads Offline — changes sync on reconnect : keep editing, and your changes merge when the connection returns. 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. Next Source control — turn the shared result into a commit Code — the text view that co-editing shares character by character"},{"id":"docs:studio/publish/collaboration#real-time-collaboration","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#real-time-collaboration","title":"Real-time collaboration","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.","heading":"Real-time collaboration","text":"When 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."},{"id":"docs:studio/publish/collaboration#what-you-see","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#what-you-see","title":"Real-time collaboration","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.","heading":"What you see","text":"A status pill in the toolbar — Live while the session is connected. It replaces the usual unsaved-changes dot for this tab. 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. Selections on the canvas — each peer's selected element is outlined in their color, labeled with their name, and follows them live. Cursors in Code view — in the Code mode the shared text carries every writer's caret and selection in their color, with their name on the caret."},{"id":"docs:studio/publish/collaboration#how-co-editing-behaves","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#how-co-editing-behaves","title":"Real-time collaboration","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.","heading":"How co-editing behaves","text":"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. Undo is yours alone. ⌘Z / Ctrl+Z steps back through your edits only — you can't undo what a teammate just did. Forms sync too. Page metadata and frontmatter fields co-edit the same way the canvas does. 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. 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."},{"id":"docs:studio/publish/collaboration#syncing-is-not-saving","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#syncing-is-not-saving","title":"Real-time collaboration","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.","heading":"Syncing is not saving","text":"Your 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 saves open co-edited files first, so a commit always captures what the session currently sees. On 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."},{"id":"docs:studio/publish/collaboration#which-setups-support-it","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#which-setups-support-it","title":"Real-time collaboration","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.","heading":"Which setups support it","text":"A shared dev server — the built-in case. Everyone who opens the same dev-server URL (see The 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. 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. The desktop app — always solo: it edits your local files directly and has no collaboration endpoint."},{"id":"docs:studio/publish/collaboration#falling-back-to-solo","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#falling-back-to-solo","title":"Real-time collaboration","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.","heading":"Falling back to solo","text":"Collaboration degrades, never blocks: A backend without the endpoint simply gives you ordinary solo editing — no errors, no pill. If a session can't sync within a few seconds of opening, the tab proceeds solo. If the connection drops, the pill reads Offline — changes sync on reconnect : keep editing, and your changes merge when the connection returns. 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."},{"id":"docs:studio/publish/collaboration#next","collection":"docs","slug":"studio/publish/collaboration","url":"/docs/studio/publish/collaboration/#next","title":"Real-time collaboration","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.","heading":"Next","text":"Source control — turn the shared result into a commit Code — the text view that co-editing shares character by character"},{"id":"docs:studio/publish/github","collection":"docs","slug":"studio/publish/github","url":"/docs/studio/publish/github/","title":"GitHub","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.","heading":"","text":"GitHub Studio 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 keeps the repository current, and your host can build the site from it. Authorize Studio The first time you use a GitHub feature, Studio asks you to sign in with a one-time code: A Sign in to GitHub dialog appears showing a short code. Click the link in the dialog — it opens GitHub's device-authorization page in your browser. Enter the code there and approve the request. Back in Studio, the dialog closes on its own once GitHub confirms. Studio remembers the authorization on this device, so you won't be asked again on your next publish. Publish to GitHub 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. Click Publish to GitHub . Sign in first if prompted. In the dialog, confirm the Repository name (prefilled with your project's name) and add an optional Description . Choose the visibility. Private repository is on by default — turn it off to make the code public. Click Create Repository . Studio 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. Publishing 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. Some Studio platforms connect to GitHub through the Jx Suite GitHub App instead — when that applies, the 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 . Next Source control — the day-to-day commit and sync flow Publish — how a push becomes a live site"},{"id":"docs:studio/publish/github#github","collection":"docs","slug":"studio/publish/github","url":"/docs/studio/publish/github/#github","title":"GitHub","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.","heading":"GitHub","text":"Studio 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 keeps the repository current, and your host can build the site from it."},{"id":"docs:studio/publish/github#authorize-studio","collection":"docs","slug":"studio/publish/github","url":"/docs/studio/publish/github/#authorize-studio","title":"GitHub","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.","heading":"Authorize Studio","text":"The first time you use a GitHub feature, Studio asks you to sign in with a one-time code: A Sign in to GitHub dialog appears showing a short code. Click the link in the dialog — it opens GitHub's device-authorization page in your browser. Enter the code there and approve the request. Back in Studio, the dialog closes on its own once GitHub confirms. Studio remembers the authorization on this device, so you won't be asked again on your next publish."},{"id":"docs:studio/publish/github#publish-to-github","collection":"docs","slug":"studio/publish/github","url":"/docs/studio/publish/github/#publish-to-github","title":"GitHub","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.","heading":"Publish to GitHub","text":"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. Click Publish to GitHub . Sign in first if prompted. In the dialog, confirm the Repository name (prefilled with your project's name) and add an optional Description . Choose the visibility. Private repository is on by default — turn it off to make the code public. Click Create Repository . Studio 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. Publishing 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. Some Studio platforms connect to GitHub through the Jx Suite GitHub App instead — when that applies, the 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 ."},{"id":"docs:studio/publish/github#next","collection":"docs","slug":"studio/publish/github","url":"/docs/studio/publish/github/#next","title":"GitHub","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.","heading":"Next","text":"Source control — the day-to-day commit and sync flow Publish — how a push becomes a live site"},{"id":"docs:studio/publish/other-hosts","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/","title":"Other hosts","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.","heading":"","text":"Other hosts A 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 ; for everything else, the recipe below is the whole story. The recipe 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 . Put the project on GitHub (or another repository host) so your host can see it — GitHub does this from inside Studio. Tell the host two things : the build command is bunx jx build , and the folder to publish is dist . That's it. From then on, every Commit and sync in Source control pushes your changes, the host runs the build, and the fresh site goes live — the flow described in Publish . Netlify Create a new site in Netlify and connect it to your repository. In the build settings, set: Build command : bunx jx build Publish directory : dist Netlify then builds and publishes on every push, with a free site address until you attach your own domain. GitHub Pages GitHub 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. Anywhere else The 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. With 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 . Next Publish to Cloudflare Pages — the host with a built-in Studio flow GitHub — get the project into a repository first"},{"id":"docs:studio/publish/other-hosts#other-hosts","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#other-hosts","title":"Other hosts","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.","heading":"Other hosts","text":"A 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 ; for everything else, the recipe below is the whole story."},{"id":"docs:studio/publish/other-hosts#the-recipe","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#the-recipe","title":"Other hosts","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.","heading":"The recipe","text":"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 . Put the project on GitHub (or another repository host) so your host can see it — GitHub does this from inside Studio. Tell the host two things : the build command is bunx jx build , and the folder to publish is dist . That's it. From then on, every Commit and sync in Source control pushes your changes, the host runs the build, and the fresh site goes live — the flow described in Publish ."},{"id":"docs:studio/publish/other-hosts#netlify","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#netlify","title":"Other hosts","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.","heading":"Netlify","text":"Create a new site in Netlify and connect it to your repository. In the build settings, set: Build command : bunx jx build Publish directory : dist Netlify then builds and publishes on every push, with a free site address until you attach your own domain."},{"id":"docs:studio/publish/other-hosts#github-pages","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#github-pages","title":"Other hosts","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.","heading":"GitHub Pages","text":"GitHub 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."},{"id":"docs:studio/publish/other-hosts#anywhere-else","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#anywhere-else","title":"Other hosts","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.","heading":"Anywhere else","text":"The 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. With 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 ."},{"id":"docs:studio/publish/other-hosts#next","collection":"docs","slug":"studio/publish/other-hosts","url":"/docs/studio/publish/other-hosts/#next","title":"Other hosts","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.","heading":"Next","text":"Publish to Cloudflare Pages — the host with a built-in Studio flow GitHub — get the project into a repository first"},{"id":"docs:studio/publish/source-control","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/","title":"Source control","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.","heading":"","text":"Source control Source 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. If 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 . Review your changes The 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. Click a changed file to open a diff in the canvas — what changed since your last commit. 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. Click the undo icon on a row to discard its changes. Studio asks for confirmation first. Discard permanently throws away a file's changes since the last commit — there is no undo beyond the confirmation dialog. Commit and sync Type a summary of your work in the message box. 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 . To record a commit without pushing, open the dropdown beside the button and choose Commit (don't sync) , or press Ctrl+Enter in the message box. If nothing is staged, the commit takes all changed files. Stay in sync The 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: Fetch — check the repository for news without changing your files. Pull — bring teammates' commits into your copy. Push — send your local commits up. Studio 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). A project with no remote yet shows Local only (no remote) here, with a Publish to GitHub shortcut. Branches The 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. History The History tab lists your project's recent commits — message, author, and when — so you can see how the site got to where it is. Next GitHub — put a local project on GitHub without leaving Studio Publish — how a push becomes a live site"},{"id":"docs:studio/publish/source-control#source-control","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#source-control","title":"Source control","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.","heading":"Source control","text":"Source 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. If 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 ."},{"id":"docs:studio/publish/source-control#review-your-changes","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#review-your-changes","title":"Source control","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.","heading":"Review your changes","text":"The 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. Click a changed file to open a diff in the canvas — what changed since your last commit. 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. Click the undo icon on a row to discard its changes. Studio asks for confirmation first. Discard permanently throws away a file's changes since the last commit — there is no undo beyond the confirmation dialog."},{"id":"docs:studio/publish/source-control#commit-and-sync","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#commit-and-sync","title":"Source control","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.","heading":"Commit and sync","text":"Type a summary of your work in the message box. 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 . To record a commit without pushing, open the dropdown beside the button and choose Commit (don't sync) , or press Ctrl+Enter in the message box. If nothing is staged, the commit takes all changed files."},{"id":"docs:studio/publish/source-control#stay-in-sync","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#stay-in-sync","title":"Source control","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.","heading":"Stay in sync","text":"The 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: Fetch — check the repository for news without changing your files. Pull — bring teammates' commits into your copy. Push — send your local commits up. Studio 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). A project with no remote yet shows Local only (no remote) here, with a Publish to GitHub shortcut."},{"id":"docs:studio/publish/source-control#branches","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#branches","title":"Source control","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.","heading":"Branches","text":"The 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."},{"id":"docs:studio/publish/source-control#history","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#history","title":"Source control","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.","heading":"History","text":"The History tab lists your project's recent commits — message, author, and when — so you can see how the site got to where it is."},{"id":"docs:studio/publish/source-control#next","collection":"docs","slug":"studio/publish/source-control","url":"/docs/studio/publish/source-control/#next","title":"Source control","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.","heading":"Next","text":"GitHub — put a local project on GitHub without leaving Studio Publish — how a push becomes a live site"}]}