The whole model, in the order you meet it.
<row gap="8" align="center">
<image src="icon.svg" w="20" h="20">
<text grow>A row lines things up</text>
<button>Go</button>
</row>
<row> lines up, <col> stacks,
<grid cols="3"> tiles, <stack> layers,
<scroll> clips. A box measures itself from its content and
its padding unless you give it w or h. Something with
grow takes its share of what is left over — its share of the free
space, not its content plus a share, which is what makes two of them exactly
equal.
Written where the thing is, because there is no selector to write them somewhere else. Any of them can be set from a script by the same name, and read back as what it is:
card.w // 120, a number — not "120"
card.w + 10 // 130
card.bold // true, a boolean
card.bg // "#222222", a string
<style name="card" bg="#222" radius="10" border="1" border-color="#333" pad="16">
<col use="card"> … </col>
A bundle of attributes under a name. It does not cascade, it does not match, and it cannot reach anything it was not written on.
<template name="kv">
<row gap="8"><text grow>$k</text><text>$v</text></row>
</template>
<kv k="Version" v="1.0.0">
Text in, text out, while parsing. Nothing is left at runtime, and a template cannot see anything but what its call hands it — which is what stops it becoming a scope.
Four layers, in the order to reach for them:
| a look | <style> + use="" |
| markup | <template> |
| a drawing | <surface> with
<fill> and <picture> |
| behaviour | a function over ids |
<surface h="60" self="stretch">
<fill id="knob" at="20 10 40 40" radius="20" bg="#4a90e2">
</surface>
A drawn shape is a real target: give that <fill> a click
listener and it is hit-tested, hovered and given a hand cursor like any widget.
A knob, a colour wheel, a chart with tooltips, a confetti burst — all of it
without adding anything to Spark.
<picture src="sword.svg" animate="rotate 2s alternate" from="-5" to="5">
<col use="card" animate="scale 150ms" from="0.86" to="1">
One numeric attribute between two values. A one-shot animation runs again
every time its node is shown, which is what makes a dialog pop open rather than
pop open once. The clock runs at sixty a second only while something is
moving, and honours the system's reduced-motion setting without being
asked. For anything Spark has no opinion about, listen for
frame.
button.addEventListener("click", save);
document.body.addEventListener("key", function () {
if (document.body.key === "Escape") close();
});
card.addEventListener("drop", function () {
open(String(document.body.files).split("\n")[0]);
});
Spark has no event object — a listener is a function of no arguments
everywhere — so what came with an event is read off <body>
like any other property. Key names are decided once for all three systems, so
nobody has to know whether this one calls it Return or
Enter.
spark.open("settings.html"); // a second window, a second document
spark.tell("saved"); // a line to every other window
A window is a document: its own tree, its own script context, its own clock. They share a process and nothing else, so the only thing one can do to another is tell it something.
spark.file.write(spark.dir("config") + "/settings.json", text);
spark.notify("Build finished", name);
spark.tray("", "Running");
spark.hotkey("ctrl+alt+G");
spark.sound("sounds/done.wav", 0.5);
spark.openFile(); spark.run("git status"); spark.writeClipboard(text);
Dialogs, the clipboard, notifications, the status area, global shortcuts,
standard folders, running a program, opening a URL. Every one of them is the
system's own, and every one answers null where the system has no
such thing — so an app can ask and carry on.
<script src="main.js" bundle></script>
The developer's own esbuild and Babel translate the script down to es5 before it is baked in — never on the machine that runs the app. They have Node because they installed the packages; the person using the app needs neither.
JSON and nothing else — no let, no
arrow, no class, no Promise. That is why the target is es5 and why
this exists.