Your first interface
Install it into your app:
bash
npm install sayshapeA SayShape interface needs three things: a <canvas>, the components the instance understands, and a spec.
html
<div style="max-width: 480px">
<canvas id="ui"></canvas>
</div>ts
import { SayShape, coreComponents } from "sayshape";
const canvas = document.querySelector<HTMLCanvasElement>("#ui")!;
const ui = new SayShape(canvas, { components: coreComponents });
ui.setSpec(`state count=0
card
text "Clicked {count} times"
button "Click me" on=inc:count`);The canvas fills its parent's width and grows to fit its content, so size the parent rather than the canvas. That spec renders as:
sayshape
state count=0
card
text "Clicked {count} times"
button "Click me" on=inc:countThis is the same code a clean Vite app runs against the published package in the release checks (npm run verify:pack).
Listen to it
Everything the interface does is visible to your page through events:
ts
// Any state change: a click, a keystroke, a value the spec declares.
ui.on("change", ({ key, value }) => console.log(key, value));
// Actions the spec hands to your app with emit:
ui.on("action", ({ name, value }) => {
if (name === "order") submitOrder(value);
});on() returns a function that unsubscribes. Call ui.destroy() when you remove the canvas from the page: it removes the instance's listeners and stops rendering.
Change it
ui.setSpec(text)replaces the spec. Values the person already changed are kept when the new spec declares them the same way; pass{ resetState: true }to start over.ui.state.set("count", 10)changes state from your app.ui.setTheme(theme)switches themes (Themes).
The full list of options, methods and events is in Embedding SayShape.
Add the map
The world map ships separately because its country outlines are large:
ts
import { SayShape, coreComponents } from "sayshape";
import { mapComponents } from "sayshape/map";
const ui = new SayShape(canvas, { components: [...coreComponents, ...mapComponents] });