Declare multiple plugin pages in device.projection.json. The Dock recognizes left, right, up and down swipes and renders the transition locally, both with a connected Host and in a native resident installation. Browser previews and the native simulator use the same page graph. Use an updated SDK, Studio and consumer App: old package validators reject the added fields. Firmware must advertise pages.swipe.v1; older firmware is reported as incompatible rather than silently showing every page.
device.projection.json
pages.swipe.v1
pages contains 1–16 entries. Each entry has a unique id (an ASCII letter followed by letters, digits, underscores or hyphens, at most 64 characters) and optional left, right, up, down targets. Targets refer to the direction of the finger, must name a different declared page, and may form cycles. An omitted target leaves the current page unchanged. Pages do not infer neighbors or wrap automatically.
pages
id
left
right
up
down
initialPage defaults to the first page. Put page on a primitive, hit or root widget to display it on that page. Untagged nodes appear on every page and remain stationary during transitions. Children inherit their root widget's page and must not declare their own. IDs, node/animation counts and asset budgets remain package-wide; pages do not multiply the existing quotas. All node coordinates still use the 854×480 canvas, with content below the 72px system header.
initialPage
page
This excerpt belongs inside a complete projection with its normal version, hashes, dimensions and header:
{ "pages": [ { "id": "overview", "left": "details", "up": "help" }, { "id": "details", "right": "overview" }, { "id": "help", "down": "overview" } ], "initialPage": "overview", "primitives": [ { "type": "text", "page": "overview", "x": 32, "y": 104, "w": 700, "h": 56, "font": 40, "text": "Overview" }, { "type": "text", "page": "details", "x": 32, "y": 104, "w": 700, "h": 56, "font": 40, "text": "Details" }, { "type": "text", "page": "help", "x": 32, "y": 104, "w": 700, "h": 56, "font": 40, "text": "Help" }, { "type": "text", "x": 32, "y": 400, "w": 700, "h": 40, "font": 24, "text": "Shared footer" } ], "hits": [] }
Navigation and the current page belong to the Dock renderer. A complete state publication refreshes bindings on all pages without selecting a page or waiting for the Host to animate. New device views start at initialPage: Back/reopen, reconnection that creates a new view, process restart and projection replacement may reset navigation. Page selection is not durable storage or a host-controlled state binding.
Pages alone keep deviceExecution(manifest, projection) === 'native'. Packaged static primitives/images, native media animation/fullscreen and declared device data-source bindings can span pages and run independently. Existing restrictions still apply: host permissions, hits, widgets, streams, ordinary dynamic bindings, dial handlers and pageChange require Host execution. Local navigation does not add device JavaScript or make Host integrations work offline. Device-source pages have no host controls or pageChange notification.
deviceExecution(manifest, projection) === 'native'
pageChange
Set pageChange to a declared read-only event name to observe committed transitions. The context is {from,to,direction}. The event must have write: false and no permissions. Package checks validate its context schema against every declared edge. This makes the plugin Host-dependent, even when it declares no permissions. A handler observes a completed local transition; its result cannot approve, undo or delay navigation.
{from,to,direction}
write: false
For example, events.schema.json can contain:
events.schema.json
{ "changed": { "context": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" }, "direction": { "type": "string", "enum": ["left", "right", "up", "down"] } }, "required": ["from", "to", "direction"], "additionalProperties": false }, "write": false } }
Add "pageChange": "changed" to the projection, then register:
"pageChange": "changed"
ctx.on("changed", async ({ context }) => { ctx.log.info("Page changed", context); return { ok: true }; });
Notifications use the existing bounded, view-scoped event lane. No notification is sent for initial entry, a short/cancelled drag or an absent neighbor. They may be lost when a view closes, disconnects or its event queue is full; there is no replay or initial-page query API. Keep them observational. Ordinary controls retain their original context shapes.
Only the plugin content area participates. Sliders and scrolling controls own their pointer gestures; popup dialogs and fullscreen media suspend page navigation. A swipe selects a dominant axis with a 1.25 ratio, follows the finger, commits after at least 48 device pixels and settles over 180ms. A shorter drag returns to the source page. A recognized swipe suppresses the button tap that would otherwise follow release. Input is locked during settling.
Use the bundled page-deck example in Studio, or copy dist/examples/page-deck from the standalone tools to your own source directory. Run normal check/package commands, then drag in browser/native preview to inspect every page. plugin_read.context.pageNavigation (standalone MCP/API) and plugin_context (Desktop MCP) expose the current capability and limits. plugin_event can exercise the declared notification handler; it does not change the preview's local page. Screenshot tools capture the initial page of their fresh renderer.
page-deck
dist/examples/page-deck
plugin_read.context.pageNavigation
plugin_context
plugin_event
Test all declared directions, boundaries, cycles, short-drag cancellation, button/slider/scroll priority, hidden-page controls, state refresh and new-view reset. Verify both native offline installation and a connected Host on physical hardware before claiming device latency or smoothness. Browser and SDL checks do not certify real touch hardware.