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.
| Layer | Contains |
|---|---|
| Windows | macOS app windows, Safari, Google Chrome and Microsoft Edge |
| Device Bezels | Measured iPhone and iPad artwork with a live app viewport |
| AppChrome | Window controls, browser toolbars, status bars and device time |
| App content | The 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.
Windows
Section titled “Windows”macOS
Safari
Google Chrome
Microsoft Edge
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.
Compact Mac
Section titled “Compact Mac”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>Device bezels
Section titled “Device bezels”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.
The device artwork could not be loaded. Reload this page to try again.
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.
Video in the original phone frame
Section titled “Video in the original phone frame”Ezo in the original phone frame
Section titled “Ezo in the original phone frame”Complete an item in a sample Weekend list. The video uses Ezo’s actual components and includes the original phone and Safari bars.
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.
Instagram Reels
Section titled “Instagram Reels”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.
App chrome
Section titled “App chrome”macOS
Compact Mac
Safari
Google Chrome
Microsoft Edge
iPhone status
iPad status
iPhone Safari
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.
App content
Section titled “App content”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.
What a kit is
Section titled “What a kit is”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>/withkit.js,kit.css, the files the stylesheet takes frompublic/, andmanifest.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:
| File | Export | Taken by | What it does |
|---|---|---|---|
build.mjs | build | The product | Bundles the kit entry with the product’s own Vite and writes dist/kit/ |
serve.ts | serve | The product, when a Worker answers first | filmKit(request, env.ASSETS) answers /kit/ for every origin |
player.js | player | The film project | window.VisualKits: finds a kit, loads it into the film’s page and pins its revision |
pull.mjs | pull | The film project | Keeps a checked local copy of a kit |
In the product
Section titled “In the product”The example is a React app served by a Worker, its kit named ledger.
1. Take the build through Sync 1
Section titled “1. Take the build through Sync 1”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" }}2. Export the app’s root from its mount
Section titled “2. Export the app’s root from its mount”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 mountimport { createRoot } from "react-dom/client";import { App } from "./main";
createRoot(document.getElementById("root")!).render(<App />);src/main.tsx exports App and mounts nothing.
3. Write the kit entry
Section titled “3. Write the kit entry”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.
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"| Option | Default | For |
|---|---|---|
--name | Required | The kit’s name: lowercase letters, digits and hyphens |
--entry | Required | The kit entry |
--out | dist/kit | Where the host serves from. An Astro site on a Worker: dist/client/kit |
--public | public | The folder the stylesheet’s /… addresses come from |
--config | none | A Vite config with the plugins the app needs, such as React’s or Tailwind’s |
--headers | none | A 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.
5. Serve /kit/ to every origin
Section titled “5. Serve /kit/ to every origin”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.
6. Deploy, then check
Section titled “6. Deploy, then check”curl -s https://<site>/kit/latest.jsoncurl -sI https://<site>/kit/<revision>/manifest.jsonlatest.json names the revision just built, and the manifest answers with Access-Control-Allow-Origin: *.
In the film project
Section titled “In the film project”Register the host
Section titled “Register the host”Add one line to scenes/kits/hosts.js, the list of where each product publishes its kit:
VisualKits.register('ledger', 'https://<site>/kit/');Load the kit
Section titled “Load the 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.
Typefaces
Section titled “Typefaces”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']);Choose a version
Section titled “Choose a version”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.
| Choice | Plays |
|---|---|
latest | What the product has deployed now |
?kit=<name>@<revision> | That revision, pinned |
local | The 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.
Renders pin their revision
Section titled “Renders pin their revision”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.
Keep a local copy
Section titled “Keep a local copy”vp run kit:pull ledgerkit: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.
Limits
Section titled “Limits”- 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.
- 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.
Existing kits
Section titled “Existing kits”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.