VisualKit

VisualKit supplies the surroundings for showing an app: desktop windows, device bezels and platform controls. It replaces the name FilmKit. The app content remains the product’s own code on sample records.

LayerContains
WindowsmacOS app windows, Safari, Google Chrome and Microsoft Edge
Device BezelsMeasured iPhone and iPad artwork with a live app viewport
AppChromeWindow controls, browser toolbars, status bars and device time
App contentThe product’s real, revision-pinned app on sample data

Open the owner preview to compare the window styles and 20 device variants. Sign in with the owner account to view the licensed artwork.

Public Lit components live in DS1/5-visual-kit/, with 2-core entry points for Sync 1 selections window, device-bezel and app-chrome. The device artwork is supplied separately and is excluded from the public package.

The default device clock is 00:01. Browser addresses are empty until supplied. Controls are presentation-only by default. Opting into interaction emits requests for the scene to handle; it does not close or navigate the host page.

macOS

Light
Dark

Safari

Light
Dark

Google Chrome

Light
Dark

Microsoft Edge

Light
Dark

ds-window wraps app content in a macOS application window or a Safari, Google Chrome or Microsoft Edge browser window.

<ds-window kind="safari" label="Orders" address="shop.example" style="height:500px">
<iframe title="Orders preview" src="/sample-app/"></iframe>
</ds-window>

Choose kind="macos|safari|chrome|edge" and appearance="light|dark". Use a real app kit in the viewport; keep the window upright and scale it as a whole. Browser controls use shared DS1 icons within platform-specific geometry. These are configurable presentation templates, not version-certified replicas of each browser release.

The viewport CSS part owns the app area. A slotted iframe fills it. Add interactive to emit chrome-action requests such as back, reload and close; the scene decides what those requests do.

A 30px title bar, multiplied by DS1’s --sf, with three muted window controls and a centred title or address. Use kind="macos-compact" and set href to make the title a normal link that opens in a new tab. Without href, the title is plain text. Set --ds-window-chrome-background and --ds-window-chrome-color to match the content.

<ds-window kind="macos-compact" label="Room shop" href="/examples/layouts/shop/" style="height:500px">
<iframe title="Shop design" src="/examples/layouts/shop/"></iframe>
</ds-window>

The catalogue includes 20 colour variants: iPhone 17 Pro Max, iPhone 17 Pro, iPhone 17, iPhone Air, iPad Pro 13, iPad Pro 11 and iPad mini. Measurements and artwork provenance follow the supplied Figma community file.

Compare every variant in the owner preview.

<ds-device-bezel
device="iphone-17-pro-max-deep-blue"
asset-base="/licensed-device-art/"
time="00:01"
browser="safari"
address="shop.example">
<iframe title="App preview" src="/sample-app/"></iframe>
</ds-device-bezel>

BEZELS exports each artwork size, screen rectangle and source node. screenGeometry scales the viewport uniformly without changing its logical app dimensions. asset-base names the folder containing the artwork filenames in the catalogue. chrome-assets optionally supplies the separately licensed status icons. appearance changes the screen surroundings; browser="safari" adds the mobile toolbar. Set the status JavaScript property to false when the app content already includes a status bar.

The inline example is public and includes the iPhone 17 Pro Max Deep Blue artwork and status icons. The remaining artwork is available in the owner preview through its authenticated asset route. Original artwork is not included in npm. Obtain and use artwork under the source terms; importing it does not grant redistribution rights. Missing artwork is reported explicitly rather than replaced with a different device.

Money’s sample screen inside the original iPhone frame. Eight seconds, with the status and Safari bars included.

Download MP4

Complete an item in a sample Weekend list. The video uses Ezo’s actual components and includes the original phone and Safari bars.

Ezo’s sample screen inside the original iPhone frame. Eight seconds, with the status and Safari bars included.

Download MP4

The downloadable video includes the complete frame. Playback cannot scroll or interact with the app. Its editable .vis source is created and rendered through the Visual CLI.

Make
a move.

Use ds-app-chrome kind="instagram" around a video, image or live animation. The Reels header, action rail, account, caption, progress line and floating navigation follow the supplied October 2026 iPhone screenshot. This is presentation chrome; it has no account connection or publishing behavior.

<ds-device-bezel edge-to-edge appearance="dark" network="wifi" asset-base="/licensed-device-art/">
<ds-app-chrome kind="instagram" username="studio" caption="A moment in motion." likes="20.7K" progress="0.6">
<video src="/film.mp4" muted playsinline></video>
</ds-app-chrome>
</ds-device-bezel>

Set username, avatar, caption, verified, likes, comments, reposts and shares from the scene. Counts default empty; progress is a fraction from 0 to 1. Slot content fills the viewport. The device’s edge-to-edge mode runs the slotted page up under its status bar and omits the separate home strip; the status bar’s ink follows appearance, black for light over a light page and white for dark over a dark one or a photograph. Set interactive on AppChrome to emit chrome-action requests; no social actions happen automatically.

macOS

Light
Dark

Compact Mac

Light
Dark

Safari

Light
Dark

Google Chrome

Light
Dark

Microsoft Edge

Light
Dark

iPhone status

Light
Dark

iPad status

Light
Dark

iPhone Safari

Light
Dark

ds-app-chrome draws the controls independently of the window or device artwork. Available kinds are macos, macos-compact, safari, chrome, edge, ios, ipados and safari-ios.

<ds-app-chrome kind="ios" time="00:01"></ds-app-chrome>
<ds-app-chrome kind="safari" label="Orders" address="shop.example"></ds-app-chrome>

Add static to a Device Bezel used as an illustration. Its content becomes inert: wheel, touch, keyboard focus and clicks cannot interact with the embedded app; the surrounding page still scrolls. Give the frame role="img" and an aria-label describing the illustration. Leave static off for live previews.

network="wifi", network="5G", network="4G" or network="LTE" chooses the connection indicator on AppChrome and Device Bezels. Single-SIM signal bars follow the Figma status component; dual-sim opts into the separately drawn dual-SIM variant. With chrome-assets (or AppChrome’s asset-base), Wi-Fi and battery use the original Figma vectors. Battery level remains adjustable with battery="80". SF Pro is the typeface; SF Symbols is Apple’s symbol library.

The time defaults to 00:01 and remains editable. The address has no default. Both light and dark appearances are supported. iOS status geometry follows the source status component; device artwork supplies the camera island. asset-base can supply status-wifi.svg and status-battery.svg from appropriately licensed assets.

For interactive scenes, set interactive and listen for chrome-action. Its detail.action identifies the requested action. Buttons stay disabled in presentation mode; the component never navigates, closes a page or mutates app records itself.

Platform chrome intentionally uses native typography and dimensions. DS1’s surrounding documentation and editor controls retain DS1’s own sizing and typography. Browser toolbars have separate macOS layouts: Safari 27 compact (52px), Google Chrome 155 (86px) and Microsoft Edge 156 (80px), with browser-specific vector controls, native tab curves and light/dark surfaces. Chrome uses site controls; Edge uses site information and a favorites toolbar; Safari groups sidebar, history and sharing controls in compact capsules. These presets show a normal webpage without profile-specific extensions or optional assistant buttons.

Set favicon to the scene’s icon URL. The backAvailable and forwardAvailable properties control history availability (defaults: true and false). Edge hides unavailable Forward; the other browsers dim it. ds-window forwards these properties. Browser updates, custom themes and OS font rendering can change the appearance; these are versioned presentation presets, not a running browser.

A product film shows the app as it is deployed today. VisualKit makes that so: with every deploy the product publishes its own app as a kit, and the film loads that kit from the product’s site. Nobody rebuilds the app inside the film, and every render records the revision it showed.

Status: first version. VisualKit is a part of the 0001 System, copied with Sync 1 like any other shared source. Any product that builds with Vite (Vite+, plain Vite or Astro) and serves static files can publish one.

The product’s real app on made-up records: the app’s own root component, every section, control and stylesheet as the app ships them, mounted on sample data. It comes as one classic script and one stylesheet.

  • No live data. The kit answers the app’s network itself from sample records. No keys, no sign-in, no requests leave the page. A kit is as public as the app’s own bundle.
  • Published with every deploy at https://<site>/kit/: latest.json, which names the current revision, and <revision>/ with kit.js, kit.css, the files the stylesheet takes from public/, and manifest.json.
  • The revision is the content’s hash. The manifest lists every file with its sha256 and the commit it was built from. An unchanged kit keeps its revision across deploys.

VisualKit has four files, each a Sync 1 export:

FileExportTaken byWhat it does
build.mjsbuildThe productBundles the kit entry with the product’s own Vite and writes dist/kit/
serve.tsserveThe product, when a Worker answers firstfilmKit(request, env.ASSETS) answers /kit/ for every origin
player.jsplayerThe film projectwindow.VisualKits: finds a kit, loads it into the film’s page and pins its revision
pull.mjspullThe film projectKeeps a checked local copy of a kit

The example is a React app served by a Worker, its kit named ledger.

Add VisualKit to sync.json. Take serve as well when a Worker answers requests before the static files.

"VisualKit": {
"build": { "to": "scripts/film-kit" },
"serve": { "to": "server/film-kit" }
}

The kit imports the real app, so nothing is cut out of the source at build time. Keep the root component in one file and the page mount in another:

// src/start.tsx: the page mount
import { createRoot } from "react-dom/client";
import { App } from "./main";
createRoot(document.getElementById("root")!).render(<App />);

src/main.tsx exports App and mounts nothing.

The entry imports the root, answers the app’s network from sample records, and sets window.VisualKit. mount(element, options) is all a film must have. Anything else the films drive, such as how many requests are still open, is the product’s own.

src/film/kit.tsx
import { createRoot, type Root } from "react-dom/client";
import { flushSync } from "react-dom";
import { App } from "../main";
import { sampleState } from "../../shared/sample";
let records = null;
let root: Root | null = null;
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
// The app's network, answered from the sample records. Nothing leaves the page.
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, "https://kit.invalid");
if (url.pathname === "/api/state") return json(records);
if (url.pathname === "/api/session") return json({ kind: "owner", plan: "plus" });
return new Response("Not in the film", { status: 404 });
}) as typeof fetch;
function mount(element: HTMLElement, options: { today: string }) {
root?.unmount();
records = sampleState(options.today);
root = createRoot(element);
flushSync(() => root!.render(<App />));
}
window.VisualKit = { name: "ledger", mount };

Answer a save with the app’s own validation and keep the result, so what the app does to its records in a film is the app’s own code.

4. Build the kit at the end of every build

Section titled “4. Build the kit at the end of every build”

Add film:kit to package.json and run it last, after the app’s own build, which empties dist:

"build": "vp run check && vp build && vp run film:kit",
"film:kit": "node scripts/film-kit/build.mjs --name ledger --entry src/film/kit.tsx"
OptionDefaultFor
--nameRequiredThe kit’s name: lowercase letters, digits and hyphens
--entryRequiredThe kit entry
--outdist/kitWhere the host serves from. An Astro site on a Worker: dist/client/kit
--publicpublicThe folder the stylesheet’s /… addresses come from
--confignoneA Vite config with the plugins the app needs, such as React’s or Tailwind’s
--headersnoneA static host’s _headers file, for step 5

The build prints the kit’s name, revision and file count, and says so when the checkout had uncommitted changes.

Films load the kit from another origin, so /kit/ answers every one: Access-Control-Allow-Origin: *, a year’s immutable caching for a revision, and no caching for latest.json.

In a Worker that runs first, call filmKit() before sign-in, so the kit never asks for a session:

import { filmKit } from "./film-kit/serve";
export default {
async fetch(request: Request, env: Env) {
const kit = await filmKit(request, env.ASSETS);
if (kit) return kit;
// …sign-in and the app's own routes
},
};

On a static host (Cloudflare Pages, Workers static assets, Amplify), pass --headers dist/_headers to the build instead. It adds the same headers for the kit’s folder.

Terminal window
curl -s https://<site>/kit/latest.json
Terminal window
curl -sI https://<site>/kit/<revision>/manifest.json

latest.json names the revision just built, and the manifest answers with Access-Control-Allow-Origin: *.

Add one line to scenes/kits/hosts.js, the list of where each product publishes its kit:

VisualKits.register('ledger', 'https://<site>/kit/');

A scene loads ../film-kit/player.js and ../kits/hosts.js, resolves the kit, and writes its tags into the app’s own page, an iframe’s srcdoc. Anything that must run before the app, such as a film clock, goes before the kit’s tags.

const kit = await VisualKits.resolve('ledger', story.kit);
frame.srcdoc = `<!doctype html><html><head><meta charset="utf-8"></head><body>
<div id="root"></div>
<script src="../my-film/clock.js"></script>
${VisualKits.tags(kit)}
</body></html>`;
frame.addEventListener('load', () => {
frame.contentWindow.VisualKit.mount(frame.contentDocument.getElementById('root'), { today: '2026-10-19' });
}, { once: true });

resolve() returns the kit’s name, revision, commit, scripts, styles and files, and where it came from: live, pinned or local.

Captions drawn beside the app take the app’s typefaces from the same kit. VisualKits.fontFaces() returns the kit’s @font-face rules for the named families, with their addresses made absolute, as CSS for the film’s own page:

style.textContent = await VisualKits.fontFaces(kit, ['GT-America-Standard-Regular', 'Iosevka']);

The page’s ?kit= comes first, then the film’s own choice, then latest. Several kits are separated by commas: ?kit=ledger@latest,notes@local.

ChoicePlays
latestWhat the product has deployed now
?kit=<name>@<revision>That revision, pinned
localThe pulled copy in scenes/kits/<name>/

Each kit resolves once per page load, so a page never mixes two revisions. When the host cannot be reached, the local copy plays instead and the console says so.

The renderer reads the revisions its first page took (VisualKits.settled(), or VisualKits.pinQuery() as a kit= query) and opens every other capture page on the same ones. The render’s manifest records them under kits.

Terminal window
vp run kit:pull ledger

kit:pull <name> [revision] downloads a revision, checks every file against the manifest’s sha256 and writes scenes/kits/<name>/. Earlier copies are removed. Pull before publishing a film that must not change.

  1. A host keeps only its current revision. Each deploy replaces the last kit. A film pinned to an older revision plays only from a pulled copy.
  2. Films find controls by their labels. A film presses the app’s controls by their accessible names ([aria-label="Show the calendar"]) and finds rows by their words. Renaming a label in the product breaks the film that uses it, from the next deploy on. Play the product’s films after changing words they use, or pin them to a pulled revision.

The former Film kit Sync 1 source and FilmKit / FilmKits globals remain compatible aliases. Existing /kit/ URLs, revision hashes and recorded films keep their meaning. New entries use VisualKit; new players use VisualKits. The source folder remains 0001/system/parts/film-kit so existing copies continue to resolve.

Browser icon source notices: Chromium and Microsoft Fluent licenses.

The iPhone Safari preset follows the supplied screenshot: a single floating row with Back, Page menu, address, Reload and Tabs overlays the page. It has no separate Share/Forward row or opaque footer. Status defaults to 00:01, dual-SIM 4G and 80% battery. Configure network (wifi, 4G, 5G), battery (0–100), and the dualSim property on ds-app-chrome or ds-device-bezel. The glass uses backdrop blur; optical refraction differs from native iOS.