graview-worker-view
Write a Graview worker view — one plain script, no imports and no build, that draws HTML, SVG and CSS for a kind or the home through the graview global — with a manifest the host enforces, and prove it runs rather than assuming it does.
Installed into a project by graview skills install ., and read by whichever assistant is working beside you.
The skill
A worker view
A worker view is for what the declared blocks cannot draw: a chart, a grid of cards, a front page with a figure and a ring. It is one plain script. The host runs it in a hardened worker with no network, no storage and no DOM of its own, and draws what it says into a shadow root inside a region the host owns. What it may draw is broad; what it may reach is not. Write it against one global, graview. There is nothing to import and nothing to build; the word import may not appear in it at all, even in a comment, and nor may export.
The manifest
Every view comes with a manifest the host enforces:
{ name: "packages", title: "The packages", attach: "package", cardinality: "many",
reads: { kinds: ["offer"], edges: ["includes"] },
acts: ["set-summary", { act: "set-standing", as: "recommend", constants: { standing: "recommended" } }] }nameis lower-case letters, digits and hyphens. Acts are recordedvia: "view:<name>".titlemakes it a named place on both faces, with the slugplaceSlug(title)("the-packages").attachis a kind, or"home"for the front page's body (cardinalitymany).readslists the other kinds and the edges it is shown. Nothing else is handed to it.actslists the acts it may ask for, by name. An entry withasandconstantsis another name for the same act with arguments the view cannot change.
A manifest that names a kind, an edge or an act the app does not declare is refused before the view starts.
The global
graview.style(css) // the view's one stylesheet
graview.onProps((props) => …) // every push of what it is shown (and at once)
graview.props // the last push
graview.render(markup) // draw: a string, graview.html`…`, or nodes
graview.html`<li>${x}</li>` // markup; every value put in is escaped
graview.on("click", ".card", (event) => …) // also input, change, keydown, toggle
graview.act(name, args) // ask for an act: resolves { ok, intent } or { ok: false, reason, message }
graview.navigate("offer:7") // a record; graview.navigate({ place: "the-packages" }) a placeprops holds nodes (each with its id, kind, label and fields), edges ({ kind, from, to } among them), node for a view of one, label (its title), acts, places ({ as, title }), and theme (scheme, accent, panel, ink …). render keeps what it can, by element and by data-key or id, so give repeated rows a data-key: a field being typed in keeps its text across a push. An event carries value, checked, key, and pressed for a bound press.
What it may draw
HTML's sectioning, headings, text, lists, tables, details/summary, button, input, select, textarea, label, fieldset, meter, progress and img. SVG's svg, g, shapes, path, text, gradients, clipPath, mask, marker, pattern, symbol and use href="#id". Attributes: id, class, style, title, role, aria-*, data-*, and each element's own. CSS for layout, grid, flex, colour, type, transitions, @keyframes, @media, @supports and @container.
Never drawn: script, iframe, object, embed, link, meta, base, style, form, video, canvas, SVG image and foreignObject; any href, srcset or on* attribute; src except on img, as a data: image. In CSS: url() except url(#id) on fill, stroke, clip-path or marker; @import; @font-face; image-set(), attr() and other unlisted functions; position: fixed or sticky; :host. What the host leaves out it lists in refused; graview.refused is the runtime's own early word on the last render.
A view never speaks as the app's chrome. nav, header, footer, aside and search are drawn as div, and output as span, each with what it holds. They lose their landmark or status role, and a selector for them in your stylesheet still matches. A section is never named (no aria-label or title), so it stays out of the landmarks. role takes no landmark or notice's role (navigation, region, status, alert, …).
Draw with the app's tokens, so light and dark follow the app's own toggle: var(--graview-panel), --graview-ground, --graview-ink, --graview-ink-muted, --graview-edge, --graview-accent, --graview-font-body, --graview-font-mono. Presentation attributes do not take var(); put paints in the stylesheet (.bar { fill: var(--graview-accent) }).
Links
<a data-record="offer:7"> and <a data-place="the-packages"> are links the host follows, on both faces. <a href="https://…"> is drawn as text.
Writes
An act must be in the manifest. If every member of the app sees every record, graview.act applies it as given. Otherwise only a press applies an act: a person's click on an element with data-act. Its arguments come only from the record it is bound to (data-record, on it or around it), the manifest's constants, and the fields in its fieldset, named for the act's arguments and typed by the person:
<fieldset>
<input name="summary" placeholder="What it is">
<button data-act="set-summary" data-record="package:start">Say it</button>
</fieldset>A field the view filled (value="…") is the view's until the person empties it, and a press carrying it is refused. Do not prefill. A view may empty a field after a press. A radio or a select is the person's pick, not their words: its value goes only if it is one the act declares (an enum's option) or a record the view was shown. The press is applied before the view hears it; read event.pressed.
Limits
About 256 kB of source, 5 000 drawn nodes, 120 messages a second, 1 000 ms per push, and 100 ms a second of the page's time drawing it. Past any of them the view is stopped and the plain face of its records is drawn instead, with why. Draw summaries, not every row of a huge set. Work a view schedules with timers between pushes is not timed per push. It can keep its own worker busy for just under the heartbeat's 5 s at a time; the page stays responsive, but the person's CPU does not, so never spin.
Worked examples
examples/offers-list.js is a list lens: rows linking to their records, and a note typed and pressed in. examples/front-page.js is a home: a headline, a figure, an SVG ring and cards linking to a place. Each opens with its manifest.
Registering it
import { registerWorkerView, workerHome } from "@graview/guest/host/views";
views: (schema, registry) => registerWorkerView(registry, { manifest, worker: { source } }),
pages.surface("home", workerHome({ manifest: front, worker: { source: frontSource } }));A view with attach: "home" given to registerWorkerView is the home's own view on both faces. It is the routed home's body and the landing over the scene, and the app's declared home is drawn if it fails.
Then find out whether it worked
graview checkthe app the view names: a manifest is only as sound as the declaration it reads.checkManifest(manifest, store)andcheckViewSource(source)from@graview/guest/host/viewsmust both say nothing.- Run it headless, with no network, as a member who may see less than you, over a seed: `graview view check view.js --app <app> --manifest manifest.json --seed <seed.json> --roles <role>`. It prints what the view drew, said as
graview describesays a place, or why it will not do:error(what it threw),nodes,flood,slow,act(an act its manifest does not name),manifestorsource. Run it on an empty seed too. In code,runWorkerViewHeadlessfrom@graview/guest/headlessdoes the same in an isolate you supply. - Open the place on both faces as that member. Is anything shown that they should not see? Does the region carry no
data-worker-view-failed? - Toggle the app to dark. Does the view restyle?
- Press each bound button with typed words, then again with a field the view filled. The first applies and the second is refused.
What the check cannot see
graview check judges the declaration, not the script. It cannot see a view that throws on an empty graph, draws the wrong number, or takes too long on a big one. Only running it does: graview view check on an empty app, a full one, and a member with narrow sight. A headless run is one push with timers that never fire, and nothing laid out, so it cannot see an animation or a layout that breaks at 390; the faces can. Nor can it see a view that misleads. The host keeps it from reaching or leaking anything, not from arranging what the person may see badly.