aktion-runtime 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1246 -0
- package/dist/aktion.iife.js +8431 -0
- package/dist/aktion.iife.js.map +1 -0
- package/dist/aktion.js +22594 -0
- package/dist/aktion.js.map +1 -0
- package/dist/aktion.umd.cjs +8431 -0
- package/dist/aktion.umd.cjs.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/system_prompt.txt +1095 -0
- package/dist/system_prompt_chat.txt +404 -0
- package/dist/types/element.d.ts +175 -0
- package/dist/types/icons/index.d.ts +45 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/language/builtins.d.ts +33 -0
- package/dist/types/language/components.d.ts +28 -0
- package/dist/types/language/grammar.d.ts +121 -0
- package/dist/types/language/index.d.ts +41 -0
- package/dist/types/language/snippets.d.ts +17 -0
- package/dist/types/library/components/_internal.d.ts +56 -0
- package/dist/types/library/components/advanced-charts.d.ts +6 -0
- package/dist/types/library/components/advanced-data.d.ts +6 -0
- package/dist/types/library/components/advanced-forms.d.ts +12 -0
- package/dist/types/library/components/advanced-patterns.d.ts +13 -0
- package/dist/types/library/components/charts.d.ts +5 -0
- package/dist/types/library/components/chat.d.ts +6 -0
- package/dist/types/library/components/content.d.ts +33 -0
- package/dist/types/library/components/data.d.ts +9 -0
- package/dist/types/library/components/editors.d.ts +5 -0
- package/dist/types/library/components/feedback.d.ts +14 -0
- package/dist/types/library/components/forms-shared.d.ts +7 -0
- package/dist/types/library/components/forms.d.ts +21 -0
- package/dist/types/library/components/helpers.d.ts +33 -0
- package/dist/types/library/components/layout.d.ts +20 -0
- package/dist/types/library/components/media.d.ts +7 -0
- package/dist/types/library/components/menu.d.ts +5 -0
- package/dist/types/library/components/navigation.d.ts +6 -0
- package/dist/types/library/components/new-components.d.ts +13 -0
- package/dist/types/library/components/patterns.d.ts +39 -0
- package/dist/types/library/components/router.d.ts +2 -0
- package/dist/types/library/components/theme.d.ts +2 -0
- package/dist/types/library/index.d.ts +5 -0
- package/dist/types/library/registry.d.ts +15 -0
- package/dist/types/library/types.d.ts +140 -0
- package/dist/types/library/utils.d.ts +73 -0
- package/dist/types/library/validate.d.ts +27 -0
- package/dist/types/parser/frontier.d.ts +65 -0
- package/dist/types/parser/index.d.ts +4 -0
- package/dist/types/parser/lexer.d.ts +46 -0
- package/dist/types/parser/parser.d.ts +2 -0
- package/dist/types/parser/types.d.ts +349 -0
- package/dist/types/prompt/generator.d.ts +33 -0
- package/dist/types/prompt/index.d.ts +1 -0
- package/dist/types/renderer/index.d.ts +1 -0
- package/dist/types/renderer/morph.d.ts +42 -0
- package/dist/types/renderer/renderer.d.ts +73 -0
- package/dist/types/runtime/builtins.d.ts +27 -0
- package/dist/types/runtime/console.d.ts +21 -0
- package/dist/types/runtime/effects.d.ts +69 -0
- package/dist/types/runtime/evaluator.d.ts +151 -0
- package/dist/types/runtime/http.d.ts +85 -0
- package/dist/types/runtime/i18n.d.ts +40 -0
- package/dist/types/runtime/index.d.ts +9 -0
- package/dist/types/runtime/router.d.ts +105 -0
- package/dist/types/runtime/state.d.ts +84 -0
- package/dist/types/runtime/storage.d.ts +50 -0
- package/dist/types/theme/index.d.ts +175 -0
- package/dist/types/theme/styles.d.ts +9 -0
- package/dist/types/tooling/codemod.d.ts +36 -0
- package/dist/types/tooling/delta.d.ts +74 -0
- package/dist/types/tooling/formatter.d.ts +8 -0
- package/dist/types/tooling/index.d.ts +29 -0
- package/dist/types/tooling/inspector.d.ts +49 -0
- package/dist/types/tooling/language-service.d.ts +57 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
# aktion
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://asfand-dev.github.io/aktion/)
|
|
5
|
+
[](#contributing)
|
|
6
|
+
|
|
7
|
+
A framework-agnostic web component that renders LLM-generated UI from
|
|
8
|
+
**Aktion** — a compact, declarative language designed for chat
|
|
9
|
+
assistants. Drop one `<script>` tag and one `<aktion-app>` tag into
|
|
10
|
+
any HTML page and you have a streaming, interactive renderer for an LLM's
|
|
11
|
+
response.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>
|
|
15
|
+
<aktion-app theme="light">
|
|
16
|
+
_app_ = Card([
|
|
17
|
+
CardHeader("Hello", subtitle: "Generative UI in plain HTML"),
|
|
18
|
+
Markdown("This card was streamed in as **plain text**.")
|
|
19
|
+
])
|
|
20
|
+
</aktion-app>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That is the whole integration. Works in React, Vue, Angular, Svelte, plain
|
|
24
|
+
HTML, or no framework at all.
|
|
25
|
+
|
|
26
|
+
- **Docs site:** <https://asfand-dev.github.io/aktion/>
|
|
27
|
+
- **Live examples:** <https://asfand-dev.github.io/aktion/live-examples.html>
|
|
28
|
+
- **CDN bundle (ESM):** <https://asfand-dev.github.io/aktion/dist/aktion.js>
|
|
29
|
+
- **System prompt (full):** <https://asfand-dev.github.io/aktion/dist/system_prompt.txt>
|
|
30
|
+
- **System prompt (chat):** <https://asfand-dev.github.io/aktion/dist/system_prompt_chat.txt>
|
|
31
|
+
- **Deep authoring guide:** [`coding-gen-skill.md`](./coding-gen-skill.md)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Table of contents
|
|
36
|
+
|
|
37
|
+
- [What's in the box](#whats-in-the-box)
|
|
38
|
+
- [Quick start](#quick-start)
|
|
39
|
+
- [Public API](#public-api)
|
|
40
|
+
- [Aktion — the language](#aktion--the-language)
|
|
41
|
+
- [Component library](#component-library)
|
|
42
|
+
- [Themes](#themes)
|
|
43
|
+
- [Icons](#icons)
|
|
44
|
+
- [Routing](#routing)
|
|
45
|
+
- [JavaScript escape hatch](#javascript-escape-hatch)
|
|
46
|
+
- [Built-in globals (`storage`, `console`)](#built-in-globals)
|
|
47
|
+
- [Internationalization (`i18n`)](#internationalization)
|
|
48
|
+
- [System prompt generator](#system-prompt-generator)
|
|
49
|
+
- [Tooling](#tooling)
|
|
50
|
+
- [Documentation site](#documentation-site)
|
|
51
|
+
- [Live examples](#live-examples)
|
|
52
|
+
- [Project layout](#project-layout)
|
|
53
|
+
- [Run it locally](#run-it-locally)
|
|
54
|
+
- [Security](#security)
|
|
55
|
+
- [Contributing](#contributing)
|
|
56
|
+
- [License](#license)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## What's in the box
|
|
61
|
+
|
|
62
|
+
Everything you need at runtime ships in a single bundle:
|
|
63
|
+
|
|
64
|
+
- **A streaming-first parser.** Line-oriented, error-tolerant. Each
|
|
65
|
+
statement commits to the DOM as soon as it arrives. Single, double, and
|
|
66
|
+
backtick-quoted strings (with `${expression}` interpolation). `js{ … }`
|
|
67
|
+
opaque blocks and `{ … }` declaration bodies for `component` / `effect` /
|
|
68
|
+
`action`.
|
|
69
|
+
- **One reactive atom kind.** Declare any reactive state with `$name = value`
|
|
70
|
+
and read or write it with `$name`. The runtime tracks dependencies
|
|
71
|
+
automatically. Template literals, spread, bracket access, optional
|
|
72
|
+
chaining, nullish coalescing, expression-form `if` / `match` / `for`,
|
|
73
|
+
lambdas (`(p) => …`), `bind:prop: $atom` two-way binding, and **30+ pure
|
|
74
|
+
`@`-functions** (`@Filter`, `@Sort`, `@Find`, `@GroupBy`, `@Format`,
|
|
75
|
+
`@FormatDate`, `@Plural`, `@Case`, `@Range`, `@Pick`, …).
|
|
76
|
+
- **One HTTP primitive.** `http({ url, method, headers, body, query, ... })`
|
|
77
|
+
is the only network call. It returns a reactive resource bag exposing
|
|
78
|
+
`data | error | status | loading | headers | lastUpdated`, plus the
|
|
79
|
+
callables `refetch()` and `cancel()`. Re-runs automatically when any
|
|
80
|
+
reactive input in the options object changes.
|
|
81
|
+
- **`storage` and `console` globals.** Always in scope. `storage.set/get`
|
|
82
|
+
(localStorage by default), `storage.session.*`, `storage.cookies.*` with
|
|
83
|
+
named-arg options, and `console.log/error/warn/info/debug`. No `js{}`
|
|
84
|
+
escape hatch needed.
|
|
85
|
+
- **A React-like DOM reconciler.** Diffs each re-render against the live
|
|
86
|
+
DOM. Text-input value, selection, IME state, scroll positions,
|
|
87
|
+
`<details>.open`, and stateful primitives like `Tabs` are all preserved
|
|
88
|
+
across renders. Components that need to hold UI state get a
|
|
89
|
+
`helpers.useInstanceState(...)` slot keyed by their position in the tree.
|
|
90
|
+
- **A rich component library** of **130+ components** spanning layout,
|
|
91
|
+
forms, charts, data, feedback, navigation, patterns, app-shell composites,
|
|
92
|
+
editors, advanced UI, and standard helpers. See [Component library](#component-library).
|
|
93
|
+
- **Declarative side effects.** `effect [ ...deps ] { … }` for background
|
|
94
|
+
work — anonymous blocks where the dependency list mixes state triggers
|
|
95
|
+
(`$atom`), lifecycle triggers (`on:mount`, `on:unmount`, `on:every(N)`),
|
|
96
|
+
and rate-limit modifiers (`debounce(N)`, `throttle(N)`). `effect { … }`
|
|
97
|
+
with an empty list is equivalent to `effect [on:mount] { … }`.
|
|
98
|
+
`action Name(args) { … }` declares click-driven mutations and may
|
|
99
|
+
optionally `return` a value.
|
|
100
|
+
- **A built-in router.** `pages = _router_({ "/path": Component(), "/users/:id": UserPage(id: params.id), default: NotFound() })` plus
|
|
101
|
+
`NavLink(label, to)` and a reserved `_route_` handle that exposes
|
|
102
|
+
`_route_.path`, `_route_.params`, `_route_.query`, `_route_.pattern`,
|
|
103
|
+
and `_route_.navigate("/path")`. Hash-based, framework-agnostic,
|
|
104
|
+
always wired up.
|
|
105
|
+
- **Seven built-in themes** (`light`, `dark`, `neon`, `pastel`, `glass`,
|
|
106
|
+
`brutalist`, `skyline`) plus full custom-token support via CSS custom
|
|
107
|
+
properties. **50+ design tokens** organised into `colors`, `radius`,
|
|
108
|
+
`font`, `motion`, and `elevation` groups. Brand the UI from inside the
|
|
109
|
+
script with `theme = Theme({...})`.
|
|
110
|
+
- **`i18n` runtime.** `$i18n = i18n({ locale, messages, fallback })` plus
|
|
111
|
+
a global `t("key", vars?)` builtin and a `Locale()` helper that feeds
|
|
112
|
+
the active locale into `@Format` / `@FormatDate`.
|
|
113
|
+
- **Font Awesome 6.7.2** auto-loaded — every `icon` prop accepts a Free
|
|
114
|
+
Font Awesome name (no `fa-` prefix). Use `Icon(name, variant?, size?)`
|
|
115
|
+
for standalone glyphs. Variant prefixes supported: `"regular:star"`,
|
|
116
|
+
`"brands:github"`.
|
|
117
|
+
- **A system prompt generator.** Emits a clean, ordered prompt teaching
|
|
118
|
+
the LLM exactly which components, builtins, and tools are available.
|
|
119
|
+
Two flavours ship: `system_prompt.txt` (full — every feature) and
|
|
120
|
+
`system_prompt_chat.txt` (compact — read-only UI conversion).
|
|
121
|
+
- **Host-side tooling.** A canonical formatter, structured-edit delta
|
|
122
|
+
protocol, AST inspector, and LSP-ready language service all exported
|
|
123
|
+
from `aktion/tooling`.
|
|
124
|
+
|
|
125
|
+
Everything lives inside a Shadow DOM, so the renderer's styles never leak
|
|
126
|
+
into your application — and your application's styles never leak into the
|
|
127
|
+
renderer.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Quick start
|
|
132
|
+
|
|
133
|
+
### 1. Load the bundle
|
|
134
|
+
|
|
135
|
+
Use the CDN build (no install, just a script tag):
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For non-module setups (older bundlers, embedded contexts) use the IIFE build:
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<script src="https://asfand-dev.github.io/aktion/dist/aktion.iife.js" defer></script>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
…or install from npm and import once from your client-side entry point:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm install @aktion/runtime
|
|
151
|
+
# yarn add @aktion/runtime
|
|
152
|
+
# pnpm add @aktion/runtime
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import "@aktion/runtime";
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The package is published as
|
|
160
|
+
[`@aktion/runtime`](https://www.npmjs.com/package/@aktion/runtime). The
|
|
161
|
+
npm tarball ships only the compiled `dist/` output (ESM + CJS + UMD +
|
|
162
|
+
IIFE bundles, type declarations, the stylesheet, and the two
|
|
163
|
+
`system_prompt*.txt` files), so installs stay small. Subpath imports
|
|
164
|
+
are available for convenience:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import "@aktion/runtime/style.css";
|
|
168
|
+
const SYSTEM_PROMPT = await fetch(
|
|
169
|
+
new URL("@aktion/runtime/system_prompt.txt", import.meta.url),
|
|
170
|
+
).then((r) => r.text());
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The CSS is bundled inside the JS and injected into each instance's shadow
|
|
174
|
+
root, so you do **not** need a separate stylesheet.
|
|
175
|
+
|
|
176
|
+
### 2. Mount the tag
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<aktion-app id="reply" theme="light"></aktion-app>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. Render a response
|
|
183
|
+
|
|
184
|
+
Three equivalent ways:
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<!-- as an attribute -->
|
|
188
|
+
<aktion-app response='_app_ = Card([CardHeader("Hi")])'></aktion-app>
|
|
189
|
+
|
|
190
|
+
<!-- as inner text (rendered on connect) -->
|
|
191
|
+
<aktion-app>
|
|
192
|
+
_app_ = Card([CardHeader("Hi")])
|
|
193
|
+
</aktion-app>
|
|
194
|
+
|
|
195
|
+
<!-- as a property/method -->
|
|
196
|
+
<script>
|
|
197
|
+
const el = document.querySelector("aktion-app");
|
|
198
|
+
el.setResponse(`
|
|
199
|
+
_app_ = Stack([greeting])
|
|
200
|
+
greeting = Card([CardHeader("Hello", subtitle: "Generative UI in plain HTML")])
|
|
201
|
+
`);
|
|
202
|
+
</script>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 4. Stream from your LLM
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const response = await fetch("/api/chat", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify({ system: systemPrompt, messages }),
|
|
211
|
+
});
|
|
212
|
+
const reader = response.body.getReader();
|
|
213
|
+
const decoder = new TextDecoder();
|
|
214
|
+
|
|
215
|
+
el.streaming = true;
|
|
216
|
+
el.clear();
|
|
217
|
+
while (true) {
|
|
218
|
+
const { value, done } = await reader.read();
|
|
219
|
+
if (done) break;
|
|
220
|
+
el.appendChunk(decoder.decode(value, { stream: true }));
|
|
221
|
+
}
|
|
222
|
+
el.streaming = false;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 5. Send the system prompt
|
|
226
|
+
|
|
227
|
+
Either fetch the auto-generated `system_prompt.txt` from the CDN:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
const systemPrompt = await fetch(
|
|
231
|
+
"https://asfand-dev.github.io/aktion/dist/system_prompt.txt",
|
|
232
|
+
).then((r) => r.text());
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
…or build a richer prompt programmatically:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const prompt = el.getSystemPrompt({
|
|
239
|
+
mode: "full", // or "chat" for the compact read-only prompt
|
|
240
|
+
preamble: "You are an analytics assistant.",
|
|
241
|
+
additionalRules: ["Always end with a FollowUpBlock of 2 prompts."],
|
|
242
|
+
tools: [{ name: "list_orders", description: "Return recent orders.", argsExample: { limit: 10 } }],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 6. (Optional) Provide tools
|
|
247
|
+
|
|
248
|
+
Register host-side async functions exposed to `js{}` bodies as
|
|
249
|
+
`ctx.tools.<name>(args)`:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
el.setTools({
|
|
253
|
+
list_orders: async ({ limit }) => fetch(`/api/orders?limit=${limit}`).then(r => r.json()),
|
|
254
|
+
update_order: async ({ id, status }) =>
|
|
255
|
+
fetch(`/api/orders/${id}`, { method: "PATCH", body: JSON.stringify({ status }) }).then(r => r.json()),
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 7. (Optional) Listen for assistant messages
|
|
260
|
+
|
|
261
|
+
Wire LLM-driven follow-ups back into your chat loop:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
el.addEventListener("assistant-message", (event) => {
|
|
265
|
+
appendUserMessageToChat(event.detail.message);
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Public API
|
|
272
|
+
|
|
273
|
+
All members live on the `<aktion-app>` element.
|
|
274
|
+
|
|
275
|
+
### Attributes
|
|
276
|
+
|
|
277
|
+
| Attribute | Values | Description |
|
|
278
|
+
| --------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
279
|
+
| `theme` | Theme name or JSON token map | Switches the theme. JSON objects are merged on top of the default `light` tokens. |
|
|
280
|
+
| `streaming` | `true` / unset | Hint that text is still being appended. The error banner is suppressed while set. |
|
|
281
|
+
| `response` | Aktion text | Sets the program declaratively. Re-renders whenever the attribute changes. |
|
|
282
|
+
| `showerrors` | `true` / unset | If present and `true`, displays parse errors in the rendered UI. Defaults to off. |
|
|
283
|
+
|
|
284
|
+
Routing and the JavaScript escape hatch (`js{ … }` inside `effect` /
|
|
285
|
+
`action` bodies) are always available — no host attribute, no allow-list.
|
|
286
|
+
To omit those surfaces from the *generated prompt*, build it via
|
|
287
|
+
`getSystemPrompt({ mode: "chat" })`.
|
|
288
|
+
|
|
289
|
+
### Properties
|
|
290
|
+
|
|
291
|
+
| Property | Type | Description |
|
|
292
|
+
| ------------- | ----------------------------- | -------------------------------------------------------------------------------------- |
|
|
293
|
+
| `response` | `string` | Get or set the current program text. Setter is equivalent to `setResponse(text)`. |
|
|
294
|
+
| `streaming` | `boolean` | Reflects the `streaming` attribute. |
|
|
295
|
+
| `showErrors` | `boolean` | Reflects the `showerrors` attribute. |
|
|
296
|
+
| `route` | `string` (read-only) | Current path tracked by the router (e.g. `"/users/42"`). |
|
|
297
|
+
|
|
298
|
+
### Methods
|
|
299
|
+
|
|
300
|
+
| Method | Description |
|
|
301
|
+
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
302
|
+
| `setResponse(text)` | Replace the program (one-shot rendering). Resets state and queries. |
|
|
303
|
+
| `appendChunk(chunk)` | Append a streaming chunk and re-render. |
|
|
304
|
+
| `clear()` | Reset state, queries, and the rendered output. |
|
|
305
|
+
| `setTheme(name \| tokens)` | Apply a built-in theme by name or a partial token map. |
|
|
306
|
+
| `setTools(tools)` | Register host async tools exposed to `js{}` blocks as `ctx.tools.<name>(args)`. Replaces previously-registered tools. |
|
|
307
|
+
| `registerComponents(specs, root?)` | Extend the built-in library with your own components. |
|
|
308
|
+
| `getSystemPrompt(options?)` | Build a system prompt that matches the current library and tools. Pass `{ mode: "chat" }` for the compact variant. |
|
|
309
|
+
| `navigate(path)` | Programmatically navigate. Updates `window.location.hash`. |
|
|
310
|
+
| `registerHttpInterceptors({ onRequest?, onResponse?, onError? })` | Install interceptors for the `http({...})` layer. `onResponse` receives a `retry()` one-shot for e.g. 401 refresh flows. |
|
|
311
|
+
| `serializeState()` | Return every reactive atom as a plain JSON-friendly object (for SSR / resumption). |
|
|
312
|
+
| `hydrateState(snapshot)` | Apply a snapshot to the live store and schedule a re-render. Atoms not in the snapshot are untouched. |
|
|
313
|
+
| `loadSnapshot({ programText, state })` | Atomic program + state load. The next render plans the program with the hydrated state already in place. |
|
|
314
|
+
| `applyDelta(ops)` | Apply a structured delta (`patch` / `replace` / `append` / `new` / `delete`). User `$state` is preserved across the diff. |
|
|
315
|
+
|
|
316
|
+
### Events
|
|
317
|
+
|
|
318
|
+
| Event | Detail | When it fires |
|
|
319
|
+
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
320
|
+
| `assistant-message` | `{ message: string }` | When an action or lambda emits `emit "assistant-message" { message: "..." }`. |
|
|
321
|
+
| `error` | `{ errors: ParseError[] }` | After each render whose source had parse errors. |
|
|
322
|
+
| `route-change` | `{ path, previousPath, source }` | When the current hash path changes. `source` is `"init" \| "hashchange" \| "navigate" \| "external"`. |
|
|
323
|
+
| `<custom-name>` | User-defined `{ ... }` | When script emits `emit "name" { ... }` inside an `action` / `effect` body. |
|
|
324
|
+
|
|
325
|
+
The `error` event always fires regardless of `showerrors`, so host apps
|
|
326
|
+
can log or report errors even when the in-page banner is suppressed.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Aktion — the language
|
|
331
|
+
|
|
332
|
+
A program is a flat list of `name = expression` statements. The renderer
|
|
333
|
+
commits each line as soon as it streams in, so the user sees the page
|
|
334
|
+
shell before the leaves arrive.
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
$count = 0
|
|
338
|
+
$theme = "dark"
|
|
339
|
+
|
|
340
|
+
component Counter(label = "Count") {
|
|
341
|
+
return Stack([
|
|
342
|
+
SectionHeader(label),
|
|
343
|
+
Button("Inc", onClick: () => $count = $count + 1),
|
|
344
|
+
Text(`Current: ${$count}`)
|
|
345
|
+
])
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
action loadOrders() {
|
|
349
|
+
$orders = http({ url: "/api/orders", method: "GET" })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
effect [$draft, debounce(500)] {
|
|
353
|
+
$save = http({ url: "/api/draft", method: "PUT", body: $draft })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
$orders = http({
|
|
357
|
+
url: "/api/users/42/orders",
|
|
358
|
+
method: "GET",
|
|
359
|
+
query: { limit: 5 }
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
pages = _router_({
|
|
363
|
+
"/": Counter(),
|
|
364
|
+
"/orders": Async($orders, loading: Spinner(), data: OrderTable($orders.data)),
|
|
365
|
+
default: NotFound()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
_app_ = pages
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Key constructs
|
|
372
|
+
|
|
373
|
+
- `_app_ = …` — the reserved entry point. Every program starts with it.
|
|
374
|
+
- `$name = value` — reactive state. One kind. Read or write with the
|
|
375
|
+
same sigil. Inside `action` / `effect` / lambda bodies, assignment
|
|
376
|
+
operators (`= += -= *= /= ??= ++ --`) are all allowed.
|
|
377
|
+
- `component Name(p = default) { return Expression }` — first-class
|
|
378
|
+
declarations with default expressions, lexical scope, and per-instance
|
|
379
|
+
state. **Always** end with an explicit `return`.
|
|
380
|
+
- `action Name(args) { body }` — callable effects with optional
|
|
381
|
+
`return`. Used as event handlers (`onClick: save`) or as expressions
|
|
382
|
+
(`$result = greet("Ada")`).
|
|
383
|
+
- `effect [ ...deps ] { body }` — declarative, anonymous side effects.
|
|
384
|
+
The bracketed dependency list mixes state triggers (`$atom`),
|
|
385
|
+
lifecycle / interval triggers (`on:mount`, `on:unmount`,
|
|
386
|
+
`on:every(N)`), and rate-limit modifiers (`debounce(N)`,
|
|
387
|
+
`throttle(N)`). `effect { ... }` (no brackets) is equivalent to
|
|
388
|
+
`effect [on:mount] { ... }`.
|
|
389
|
+
- Expression-form control flow: `if cond { … } else { … }`,
|
|
390
|
+
`match expr { "a": A() default: Else() }`, `for x in xs { Row(x) }`.
|
|
391
|
+
Match and router arms use `:` and `default:` (not `->` / `_`).
|
|
392
|
+
- `http({ url, method, headers, body, query, ... })` — the only network
|
|
393
|
+
primitive. Returns a reactive resource with `.data`, `.error`,
|
|
394
|
+
`.status`, `.loading`, `.headers`, `.lastUpdated`, `.refetch()`,
|
|
395
|
+
`.cancel()`.
|
|
396
|
+
- `pages = _router_({ "/path": Component(), default: NotFound() })` —
|
|
397
|
+
function-call router. The reserved `_route_` handle exposes the
|
|
398
|
+
reactive surface and a `navigate("/path")` method; each arm body
|
|
399
|
+
additionally receives a scoped `params` loop var with its captures.
|
|
400
|
+
- `bind:value: $atom` — two-way binding sugar on inputs.
|
|
401
|
+
- Lambdas `(args) => expr` and opaque `js{ … }` blocks placed inside
|
|
402
|
+
`effect` / `action` bodies.
|
|
403
|
+
- `emit "name" { detail }` — dispatch an outbound `CustomEvent` on the
|
|
404
|
+
host element.
|
|
405
|
+
- Comments: `// line`, `# line`, and `/* block */` — all stripped silently.
|
|
406
|
+
|
|
407
|
+
### The 60-second pitch
|
|
408
|
+
|
|
409
|
+
```text
|
|
410
|
+
$days = "7"
|
|
411
|
+
$data = http({ url: "/api/metrics", method: "GET", query: { days: $days } })
|
|
412
|
+
|
|
413
|
+
filter = FormControl("Range", control: Select("days",
|
|
414
|
+
items: [SelectItem("7", "7d"), SelectItem("30", "30d")],
|
|
415
|
+
value: $days))
|
|
416
|
+
kpi = StatCard("Events", value: `${$data.data?.events ?? 0}`, trend: "up")
|
|
417
|
+
chart = LineChart(
|
|
418
|
+
labels: $data.data?.daily?.day ?? [],
|
|
419
|
+
series: [Series("Events", $data.data?.daily?.events ?? [])])
|
|
420
|
+
|
|
421
|
+
_app_ = Stack([CardHeader("Analytics"), filter, kpi, chart])
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Highlights:
|
|
425
|
+
|
|
426
|
+
- One statement per line.
|
|
427
|
+
- Three string flavours: `"double"`, `'single'`, and `` `backtick` `` with
|
|
428
|
+
`${expression}` interpolation.
|
|
429
|
+
- Optional chaining (`obj?.prop`) and nullish coalescing (`a ?? b`).
|
|
430
|
+
- Spread in arrays (`[...$pinned, ...$todos]`) and objects
|
|
431
|
+
(`{...$current, status: "done"}`).
|
|
432
|
+
- Array shortcuts: `$rows.length`, `$rows.first`, `$rows.last`,
|
|
433
|
+
plus pluck (`$rows.title` → `[title1, title2, …]`).
|
|
434
|
+
- Responsive prop maps on layout components:
|
|
435
|
+
`Grid(items, columns: { sm: 1, md: 2, lg: 4 }, gap: "l")`.
|
|
436
|
+
- Forward references are allowed — list `_app_ = Stack([...])` first
|
|
437
|
+
and let the children stream in beneath it.
|
|
438
|
+
|
|
439
|
+
### Declarative todo app (no JS required)
|
|
440
|
+
|
|
441
|
+
```text
|
|
442
|
+
$todos = [{ id: 1, text: "Welcome — try editing", done: false }]
|
|
443
|
+
$draft = ""
|
|
444
|
+
|
|
445
|
+
action add() {
|
|
446
|
+
$todos = [...$todos, { id: $todos.length + 1, text: $draft, done: false }]
|
|
447
|
+
$draft = ""
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
action remove(id) {
|
|
451
|
+
$todos = @Filter($todos, "id", "!=", id)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
row = (t) => Card([Stack([
|
|
455
|
+
Text(t.text),
|
|
456
|
+
Button("Delete", onClick: () => remove(t.id), variant: "ghost")
|
|
457
|
+
])])
|
|
458
|
+
|
|
459
|
+
list = for t in $todos { row(t) }
|
|
460
|
+
_app_ = Stack([
|
|
461
|
+
Input("draft-input", placeholder: "What needs doing?", value: $draft),
|
|
462
|
+
Button("Add", onClick: add, variant: "primary"),
|
|
463
|
+
list
|
|
464
|
+
])
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Per-instance state & content-addressed identity
|
|
468
|
+
|
|
469
|
+
```text
|
|
470
|
+
component Counter(label) {
|
|
471
|
+
$n = 0
|
|
472
|
+
return Stack([
|
|
473
|
+
Text(`${label}: ${$n}`),
|
|
474
|
+
Button("inc", onClick: () => $n = $n + 1)
|
|
475
|
+
])
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Two independent counters — each holds its own atom.
|
|
479
|
+
_app_ = Stack([Counter("A"), Counter("B")])
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Every call site accepts a universal `key:` named argument. The renderer
|
|
483
|
+
uses it as the instance suffix instead of source location, so reordering
|
|
484
|
+
siblings keeps per-instance state attached to the right element:
|
|
485
|
+
|
|
486
|
+
```text
|
|
487
|
+
component TaskRow(task) {
|
|
488
|
+
return Stack([Text(task.title)], key: task.id)
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Schema-as-truth diagnostics
|
|
493
|
+
|
|
494
|
+
`validateProgramSchema(program, library)` (exported from
|
|
495
|
+
[`src/library/index.js`](./src/library/index.ts)) emits **hard errors**
|
|
496
|
+
for:
|
|
497
|
+
|
|
498
|
+
- Closed-token enum mismatches (`Button("Save", variant: "magic")`).
|
|
499
|
+
- Unknown named args (`Stack(junk: 1)`).
|
|
500
|
+
- One-positional-max violations (`Button("Save", "primary", true)` →
|
|
501
|
+
"use `variant: "primary"`, `loading: true`").
|
|
502
|
+
|
|
503
|
+
The host element merges these into `program.errors` so the on-screen
|
|
504
|
+
banner surfaces every violation.
|
|
505
|
+
|
|
506
|
+
### Anticipatory skeletons
|
|
507
|
+
|
|
508
|
+
A reference to a component that hasn't been declared yet (and isn't in
|
|
509
|
+
the library) renders a `Skeleton` placeholder instead of
|
|
510
|
+
`[unknown component: …]`. Mid-stream forward references just shimmer
|
|
511
|
+
until the next render pass picks the declaration up.
|
|
512
|
+
|
|
513
|
+
For the complete language reference see
|
|
514
|
+
[`docs/language.html`](./docs/language.html) or, for full apps, the
|
|
515
|
+
deep authoring guide [`coding-gen-skill.md`](./coding-gen-skill.md).
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Component library
|
|
520
|
+
|
|
521
|
+
The bundle ships **130+ components** grouped by domain. Reach for **pattern composites**
|
|
522
|
+
(`Hero`, `PageHeader`, `Stats`, `Toolbar`, `EmptyState`, `Timeline`,
|
|
523
|
+
`KanbanBoard`, `DescriptionList`, `PricingTable`, …) before hand-rolling
|
|
524
|
+
the equivalent with `Card` + `Stack` — they're tuned to produce dense,
|
|
525
|
+
production-quality SaaS UI in a single line.
|
|
526
|
+
|
|
527
|
+
| Group | Components |
|
|
528
|
+
| ------------------ | ---------- |
|
|
529
|
+
| **Layout** | `Stack`, `StackItem`, `Grid`, `GridItem`, `Container`, `Box`, `Spacer`, `Card`, `CardHeader`, `CardFooter`, `Separator`, `Tabs`, `TabItem`, `Accordion`, `AccordionItem`, `Modal`, `Drawer`, `Steps`, `AspectRatio`, `ScrollArea`, `Sticky`, `ResizablePanels`, `MasonryGrid` |
|
|
530
|
+
| **Content** | `Text`, `Image`, `Icon`, `Link`, `Badge`, `BadgeList`, `Callout`, `Quote`, `CodeBlock`, `Skeleton`, `Spinner`, `Markdown`, `Kbd` |
|
|
531
|
+
| **Forms** | `Form`, `FormControl`, `FormSection`, `FieldSet`, `ValidationSummary`, `Input`, `TextArea`, `PasswordInput`, `MaskedInput`, `MentionInput`, `TagInput`, `Select`, `SelectItem`, `Combobox`, `MultiSelect`, `Checkbox`, `CheckBoxGroup`, `CheckBoxItem`, `Radio`, `Switch`, `ToggleGroup`, `Button`, `Buttons`, `SearchBar`, `Slider`, `NumberInput`, `ColorPicker`, `DatePicker`, `DateRangePicker`, `TimePicker`, `DateTimePicker`, `FileUpload`, `PinInput`, `MultiStepForm` |
|
|
532
|
+
| **Data** | `Table`, `Col`, `DataGrid`, `List`, `ListItem`, `StatCard`, `Stats`, `Sparkline`, `Tile`, `Progress`, `ProgressRing`, `Pagination`, `Tree`, `TreeNode`, `CalendarView`, `ComparisonTable`, `InfiniteList` |
|
|
533
|
+
| **Charts** | `BarChart`, `LineChart`, `PieChart`, `RadarChart`, `ScatterChart`, `Histogram`, `Heatmap`, `Gauge`, `Series` |
|
|
534
|
+
| **Feedback & Media** | `Avatar`, `AvatarGroup`, `PersonChip`, `Tooltip`, `HoverCard`, `Popover`, `Rating`, `Toast`, `VideoPlayer`, `AudioPlayer`, `Carousel`, `Gallery`, `Lightbox`, `Map` |
|
|
535
|
+
| **Navigation** | `Breadcrumb`, `BreadcrumbItem`, `Navbar`, `NavbarItem`, `TopBar`, `NavLink` (router-aware) |
|
|
536
|
+
| **Menus** | `DropdownMenu`, `MenuItem`, `MenuSeparator`, `MenuLabel`, `ContextMenu` |
|
|
537
|
+
| **Editors** | `RichTextEditor`, `CodeEditor` |
|
|
538
|
+
| **Chat** | `SectionBlock`, `ListBlock`, `FollowUpBlock`, `FollowUpItem`, `ActionLink`, `ChatBubble` |
|
|
539
|
+
| **Patterns** | `Hero`, `PageHeader`, `SectionHeader`, `Toolbar`, `EmptyState`, `Timeline`, `TimelineItem`, `ActivityLog`, `FeatureGrid`, `FeatureItem`, `MediaCard`, `Testimonial`, `ProfileCard`, `Comment`, `Banner`, `Notification`, `InboxPanel`, `OnboardingChecklist`, `KanbanBoard`, `KanbanColumn`, `KanbanCard`, `DescriptionList`, `DescriptionItem`, `StatusDot`, `PricingTable`, `PricingCard`, `LoadingState`, `ErrorState`, `SuccessState`, `Tour`, `Spotlight` |
|
|
540
|
+
| **App shell** | `AppShell`, `Sidebar`, `SidebarSection`, `SidebarItem`, `SplitView` |
|
|
541
|
+
| **Advanced UI** | `IconButton`, `CommandPalette`, `FilterChips`, `FieldRepeater`, `VirtualList`, `QueryBuilder`, `DiffViewer`, `JsonTree`, `Gantt`, `Truncate`, `InlineEdit`, `NotificationBell` |
|
|
542
|
+
| **Helpers** | `Async`, `Show`, `Portal`, `Redirect`, `Lazy`, `ErrorBoundary` |
|
|
543
|
+
| **Theming** | `Theme` |
|
|
544
|
+
| **Routing** | `_router_({ … })`, `NavLink` |
|
|
545
|
+
|
|
546
|
+
The full catalog with positional signatures, prop tables, enum values, and
|
|
547
|
+
live previews is at
|
|
548
|
+
[`docs/components.html`](https://asfand-dev.github.io/aktion/components.html).
|
|
549
|
+
|
|
550
|
+
### Rich pattern composites
|
|
551
|
+
|
|
552
|
+
```text
|
|
553
|
+
action export_q3() { $exp = http({ url: "/exports/q3", method: "POST" }) }
|
|
554
|
+
action new_project() { _route_.navigate("/projects/new") }
|
|
555
|
+
|
|
556
|
+
dashHeader = PageHeader("Engineering Q3", subtitle: "12 active · 4 at risk", breadcrumbs: ["Workspace", "Engineering"], actions: dashActions, status: Badge("On track", "success"))
|
|
557
|
+
dashActions = [Button("Export", onClick: export_q3, variant: "secondary"), Button("New project", onClick: new_project, variant: "primary")]
|
|
558
|
+
kpis = Stats([
|
|
559
|
+
StatCard("Active", value: "12", trend: "flat"),
|
|
560
|
+
StatCard("At risk", value: "4", trend: "up", delta: "+2"),
|
|
561
|
+
StatCard("Shipped", value: "8", trend: "up", delta: "+3"),
|
|
562
|
+
StatCard("On-time", value: "87%", trend: "down", delta: "-3%")
|
|
563
|
+
])
|
|
564
|
+
board = KanbanBoard([
|
|
565
|
+
KanbanColumn("To do", items: [KanbanCard("Migrate auth", description: "Roll out new SDK.", tags: ["auth"], assignee: "Asha")]),
|
|
566
|
+
KanbanColumn("Doing", items: [KanbanCard("Streaming UI v2", description: "20 new components.", tags: ["frontend"], assignee: "Alex", tone: "primary")]),
|
|
567
|
+
KanbanColumn("Review", items: [KanbanCard("Mobile onboarding", description: "Awaiting design.", tags: ["mobile"], assignee: "Wren", tone: "warning")]),
|
|
568
|
+
KanbanColumn("Done", items: [KanbanCard("Activity timeline", description: "Shipped to 100%.", tags: ["shipped"], assignee: "Mira", tone: "success")])
|
|
569
|
+
])
|
|
570
|
+
follow = FollowUpBlock(["Show at-risk projects", "Compare to Q2", "Who needs help?"])
|
|
571
|
+
|
|
572
|
+
_app_ = Stack([dashHeader, kpis, board, follow])
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Adding your own components
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
const ProductCard = {
|
|
579
|
+
name: "ProductCard",
|
|
580
|
+
description: "Product tile with title and price.",
|
|
581
|
+
props: [
|
|
582
|
+
{ name: "title", type: "string" },
|
|
583
|
+
{ name: "price", type: "number" },
|
|
584
|
+
],
|
|
585
|
+
render: (_node, props) => {
|
|
586
|
+
const div = document.createElement("div");
|
|
587
|
+
div.textContent = `${props.title} — $${props.price}`;
|
|
588
|
+
return div;
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
el.registerComponents([ProductCard]);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
The next call to `getSystemPrompt()` automatically includes the new component.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Themes
|
|
600
|
+
|
|
601
|
+
Seven themes are built in. Pick one with `theme="..."` or pass a custom token map.
|
|
602
|
+
|
|
603
|
+
| Theme | Vibe |
|
|
604
|
+
| ------------ | ------------------------------------------------------------------------------------------------- |
|
|
605
|
+
| `light` | Crisp default, indigo accent. |
|
|
606
|
+
| `dark` | Standard dark surface, indigo accent. |
|
|
607
|
+
| `neon` | Cyberpunk-inspired dark mode with magenta/cyan glow, monospace headings, sharp corners. |
|
|
608
|
+
| `pastel` | Soft, friendly, light & rounded. Lavender + mint palette, generous radii, gentle shadows. |
|
|
609
|
+
| `glass` | Modern glassmorphism — vivid gradient backdrop, frosted translucent surfaces, indigo→cyan accent. |
|
|
610
|
+
| `brutalist` | Neo-brutalism — hard 2px black borders, chunky offset shadows, loud primary, zero gradients. |
|
|
611
|
+
| `skyline` | Enterprise cloud-console aesthetic — deep navy primary, cyan accents, calm pale blue bg. |
|
|
612
|
+
|
|
613
|
+
### Token groups
|
|
614
|
+
|
|
615
|
+
Themes are flat maps of CSS-valued strings, grouped by domain:
|
|
616
|
+
|
|
617
|
+
| Group | Sample tokens |
|
|
618
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
619
|
+
| Surface | `colorBg`, `colorBgSubtle`, `colorSurface`, `colorSurfaceMuted`, `colorBorder`, `colorText`, `colorTextMuted` |
|
|
620
|
+
| Brand | `colorPrimary`, `colorPrimaryHover`, `colorPrimaryText`, `colorAccent`, `colorAccentHover`, `colorFocusRing` |
|
|
621
|
+
| Semantic | `colorSuccess`, `colorWarning`, `colorDanger`, `colorInfo` |
|
|
622
|
+
| Typography | `fontFamily`, `fontFamilyHeading`, `fontFamilyMono`, `fontSizeBase`, `fontSizeHeading`, `fontSizeTitle`, `fontWeightBody`, `fontWeightHeading`, `letterSpacingHeading`, `headingTextTransform` |
|
|
623
|
+
| Shape | `radiusXs`, `radiusSm`, `radiusMd`, `radiusLg`, `radiusPill`, `radiusButton`, `radiusInput`, `borderWidth`, `shadowSm`, `shadowMd`, `shadowLg` |
|
|
624
|
+
| Spacing | `spacingXs`, `spacingS`, `spacingM`, `spacingL`, `spacingXl` |
|
|
625
|
+
| Buttons | `buttonFontWeight`, `buttonTextTransform`, `buttonLetterSpacing`, `buttonPaddingY`, `buttonPaddingX` |
|
|
626
|
+
| Motion | `transitionDuration` |
|
|
627
|
+
| Charts | `chart1`–`chart6` |
|
|
628
|
+
|
|
629
|
+
### Custom token map from the host
|
|
630
|
+
|
|
631
|
+
```js
|
|
632
|
+
el.setTheme({
|
|
633
|
+
colorPrimary: "#16a34a",
|
|
634
|
+
colorPrimaryHover: "#15803d",
|
|
635
|
+
colorBg: "#f0fdf4",
|
|
636
|
+
fontFamilyHeading: "'Inter', system-ui, sans-serif",
|
|
637
|
+
radiusButton: "14px",
|
|
638
|
+
buttonFontWeight: "600",
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### `Theme({...})` from inside a response
|
|
643
|
+
|
|
644
|
+
A response can brand itself by assigning a `Theme({...})` call to the
|
|
645
|
+
reserved `theme` binding. The tokens land on the host as CSS variables on
|
|
646
|
+
top of the base theme.
|
|
647
|
+
|
|
648
|
+
```text
|
|
649
|
+
theme = Theme({
|
|
650
|
+
colors: {
|
|
651
|
+
primary: "#0969da",
|
|
652
|
+
border: "#d0d7de",
|
|
653
|
+
text: "#1f2328"
|
|
654
|
+
},
|
|
655
|
+
font: {
|
|
656
|
+
family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
657
|
+
familyHeading: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
658
|
+
weightHeading: "500"
|
|
659
|
+
},
|
|
660
|
+
radius: { button: "6px", input: "6px" }
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
_app_ = Stack([CardHeader("GitHub-style page"), Buttons([Button("New repository")])])
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`Theme` expects the **structured** form — top-level groups `colors` /
|
|
667
|
+
`radius` / `font` / `motion` / `elevation` (plus metadata keys `name`
|
|
668
|
+
and `direction`). Removing the `Theme(...)` line snaps the UI back to
|
|
669
|
+
the base theme. Unknown keys are ignored silently, so typos in an
|
|
670
|
+
LLM-emitted token map can never break the page.
|
|
671
|
+
|
|
672
|
+
### Host-page CSS variable override
|
|
673
|
+
|
|
674
|
+
```css
|
|
675
|
+
aktion-app {
|
|
676
|
+
--rui-color-primary: #16a34a;
|
|
677
|
+
--rui-radius-button: 14px;
|
|
678
|
+
--rui-font-family-heading: 'Inter', system-ui, sans-serif;
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
A full token reference lives in
|
|
683
|
+
[`docs/themes.html`](https://asfand-dev.github.io/aktion/themes.html),
|
|
684
|
+
and the
|
|
685
|
+
[brand themes live example](https://asfand-dev.github.io/aktion/brand-themes.html)
|
|
686
|
+
ships ready-made GitHub / Apple / Stripe / IONOS / Notion / Vercel token
|
|
687
|
+
maps to copy.
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Icons
|
|
692
|
+
|
|
693
|
+
The runtime auto-loads
|
|
694
|
+
[Font Awesome 6.7.2](https://fontawesome.com/v6/search?o=r&m=free) from
|
|
695
|
+
the public CDN — once into `document.head` and once into each instance's
|
|
696
|
+
shadow root. Host apps do **not** need to add a stylesheet.
|
|
697
|
+
|
|
698
|
+
- Icon strings are Font Awesome names **without** the `fa-` prefix:
|
|
699
|
+
`"house"`, `"chart-line"`, `"star"`, `"cart-shopping"`,
|
|
700
|
+
`"circle-check"`, `"triangle-exclamation"`, `"sack-dollar"`.
|
|
701
|
+
- Optional variant prefix: `"regular:star"`, `"brands:github"`. The
|
|
702
|
+
default variant is `solid`.
|
|
703
|
+
- Use the dedicated `Icon(name, variant?, size?)` component to render a
|
|
704
|
+
standalone glyph (`size` ∈ `xs`, `sm`, `md`, `lg`, `xl`).
|
|
705
|
+
- Every component prop named `icon` — `NavLink`, `SidebarItem`, `Banner`,
|
|
706
|
+
`Notification`, `FeatureItem`, `Badge`, `StatCard`, `ListItem`,
|
|
707
|
+
`TimelineItem`, `DescriptionItem`, `Tile`, `EmptyState`, … — expects a
|
|
708
|
+
Font Awesome name.
|
|
709
|
+
- Invisible Unicode glyph modifiers (variation selectors, ZWJ) are
|
|
710
|
+
stripped silently so legacy emoji leftovers still resolve to the
|
|
711
|
+
proper icon.
|
|
712
|
+
|
|
713
|
+
```text
|
|
714
|
+
brandIcon = Icon("rocket", "solid", "lg")
|
|
715
|
+
homeIcon = Icon("house")
|
|
716
|
+
profileTab = NavLink("Profile", to: "/profile", variant: "ghost", icon: "user")
|
|
717
|
+
kpis = Stats([
|
|
718
|
+
StatCard("Revenue", value: "$48k", trend: "up", delta: "+12%", icon: "sack-dollar"),
|
|
719
|
+
StatCard("Orders", value: "1,284", trend: "up", delta: "+8%", icon: "cart-shopping"),
|
|
720
|
+
StatCard("Refunds", value: "12", trend: "down", delta: "-3", icon: "rotate-left")
|
|
721
|
+
])
|
|
722
|
+
_app_ = Stack([brandIcon, kpis, profileTab])
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Routing
|
|
728
|
+
|
|
729
|
+
Hash-based routing is built into the runtime. The LLM emits routes that
|
|
730
|
+
stay in sync with the URL (`#/dashboard`, `#/users/42`). Browser
|
|
731
|
+
back/forward, bookmarks, and deep links all work — and the host page
|
|
732
|
+
never reloads.
|
|
733
|
+
|
|
734
|
+
```text
|
|
735
|
+
pages = _router_({
|
|
736
|
+
"/": homePage,
|
|
737
|
+
"/dashboard": dashboardPage,
|
|
738
|
+
"/users/:id": userPage(id: params.id),
|
|
739
|
+
default: notFoundPage
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
nav = Stack([
|
|
743
|
+
NavLink("Home", to: "/", exact: true),
|
|
744
|
+
NavLink("Dashboard", to: "/dashboard"),
|
|
745
|
+
NavLink("Users", to: "/users")
|
|
746
|
+
], direction: "row", gap: "s")
|
|
747
|
+
|
|
748
|
+
_app_ = Stack([nav, pages])
|
|
749
|
+
|
|
750
|
+
homePage = Card([CardHeader("Welcome")])
|
|
751
|
+
dashboardPage = Card([CardHeader("Dashboard")])
|
|
752
|
+
userPage = (id) => Card([CardHeader(`User ${id}`)])
|
|
753
|
+
notFoundPage = Callout("Not found", description: `We couldn't find ${_route_.path}.`, variant: "warning")
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
- `pages = _router_({ "/path": Component(), default: Fallback() })` picks
|
|
757
|
+
the matching arm based on the current hash path. First match wins;
|
|
758
|
+
`default:` is the fallback.
|
|
759
|
+
- Route patterns support literal segments (`"/about"`), parameter
|
|
760
|
+
segments (`"/users/:id"` → `params.id`), and trailing wildcards
|
|
761
|
+
(`"/docs/*"` → `params._`).
|
|
762
|
+
- `NavLink(label, to:, variant?, exact?, icon?)` is a router-aware anchor
|
|
763
|
+
that intercepts clicks and reflects `data-active="true"` for the
|
|
764
|
+
current path.
|
|
765
|
+
- The reactive `_route_` handle exposes `_route_.path`, `_route_.params`,
|
|
766
|
+
`_route_.query`, and `_route_.pattern`. Call `_route_.navigate("/path")`
|
|
767
|
+
from inside the script, or `el.navigate("/path")` from the host.
|
|
768
|
+
|
|
769
|
+
The default ("full") system prompt teaches the LLM about routing. The
|
|
770
|
+
chat-flavoured prompt omits it. See the
|
|
771
|
+
[routing guide](https://asfand-dev.github.io/aktion/routing.html)
|
|
772
|
+
for the full walkthrough.
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## JavaScript escape hatch
|
|
777
|
+
|
|
778
|
+
`js{ … }` blocks live inside `effect` or `action` bodies — no host
|
|
779
|
+
attribute, no allow-list. Reach for them only when no declarative path
|
|
780
|
+
captures the behaviour (timers, clipboard, audio, complex computations,
|
|
781
|
+
host-tool calls).
|
|
782
|
+
|
|
783
|
+
```text
|
|
784
|
+
$todos = []
|
|
785
|
+
|
|
786
|
+
action toggle(id) {
|
|
787
|
+
js {
|
|
788
|
+
const todos = ctx.state.get("todos") || []
|
|
789
|
+
ctx.state.set("todos", todos.map(t => t.id === ctx.args.id ? { ...t, done: !t.done } : t))
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
row = (t) => Card([Stack([
|
|
794
|
+
Text(t.text),
|
|
795
|
+
Button("Toggle", onClick: () => toggle(t.id))
|
|
796
|
+
])])
|
|
797
|
+
|
|
798
|
+
list = for t in $todos { row(t) }
|
|
799
|
+
_app_ = Stack([list])
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Inside the `js{ … }` block:
|
|
803
|
+
|
|
804
|
+
- `ctx.state.get(name)` / `ctx.state.set(name, value)` read and write
|
|
805
|
+
reactive atoms.
|
|
806
|
+
- `ctx.args` exposes the action's positional parameters keyed by name.
|
|
807
|
+
- `ctx.cleanup(fn)` (effects only) registers a teardown to fire on
|
|
808
|
+
re-run and unmount.
|
|
809
|
+
- `ctx.host` is the host element, for DOM-observing effects.
|
|
810
|
+
- `ctx.tools` is a host-registered async tool registry (see `el.setTools(...)`).
|
|
811
|
+
|
|
812
|
+
The body runs inside `(async () => { … })()` so `await` is free. Errors
|
|
813
|
+
are caught and logged — a broken body never crashes the host page.
|
|
814
|
+
|
|
815
|
+
For long-lived behaviour, prefer an `effect`:
|
|
816
|
+
|
|
817
|
+
```text
|
|
818
|
+
effect [$draft, debounce(500)] {
|
|
819
|
+
js {
|
|
820
|
+
await fetch("/save", { method: "POST", body: JSON.stringify({ draft: ctx.state.get("draft") }) })
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
The default (full) system prompt teaches `effect` / `action` / `js{}`;
|
|
826
|
+
the chat-flavoured prompt (`getSystemPrompt({ mode: "chat" })`) omits
|
|
827
|
+
the JS section entirely.
|
|
828
|
+
|
|
829
|
+
See the
|
|
830
|
+
[JavaScript interactions guide](https://asfand-dev.github.io/aktion/javascript-interactions.html)
|
|
831
|
+
or the deep
|
|
832
|
+
[`coding-gen-skill.md`](./coding-gen-skill.md)
|
|
833
|
+
for a full walkthrough.
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## Built-in globals
|
|
838
|
+
|
|
839
|
+
Two namespace globals are always in scope inside a Aktion
|
|
840
|
+
program — no import, no `js{}` block required. Both follow the standard
|
|
841
|
+
`obj.method(args)` method-call syntax and accept named-arg options.
|
|
842
|
+
|
|
843
|
+
```text
|
|
844
|
+
# localStorage is the default; `storage.local` is its alias.
|
|
845
|
+
storage.set("name", "John")
|
|
846
|
+
$name = storage.get("name")
|
|
847
|
+
|
|
848
|
+
# Per-tab sessionStorage.
|
|
849
|
+
storage.session.set("draft", $draft)
|
|
850
|
+
$draft = storage.session.get("draft")
|
|
851
|
+
|
|
852
|
+
# Cookies — named-arg options become a single options object.
|
|
853
|
+
storage.cookies.set("user", "John", expires: 7, path: "/", sameSite: "Lax")
|
|
854
|
+
$user = storage.cookies.get("user")
|
|
855
|
+
storage.cookies.remove("user", path: "/")
|
|
856
|
+
|
|
857
|
+
# Forwards to the host console.
|
|
858
|
+
console.log("Hello", $user)
|
|
859
|
+
console.error("Something failed", $error)
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
- Non-string values round-trip through `JSON.stringify` / `JSON.parse`;
|
|
863
|
+
missing keys return `null`.
|
|
864
|
+
- Cookie options: `expires` (days, `Date`, or ISO string), `maxAge`
|
|
865
|
+
(seconds), `path`, `domain`, `secure`, `sameSite`.
|
|
866
|
+
- Failures (quota exceeded, disabled storage, malformed JSON) are
|
|
867
|
+
swallowed — perfect for partial-stream renders in privacy / SSR
|
|
868
|
+
contexts.
|
|
869
|
+
|
|
870
|
+
See the
|
|
871
|
+
[language reference](https://asfand-dev.github.io/aktion/language.html#globals)
|
|
872
|
+
for the full surface.
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## Internationalization
|
|
877
|
+
|
|
878
|
+
The `$i18n = i18n({...})` declaration configures the active locale,
|
|
879
|
+
message bundles, and fallback. A global `t(key, vars?)` builtin and a
|
|
880
|
+
`Locale()` helper feed the active locale into `@Format` / `@FormatDate`.
|
|
881
|
+
|
|
882
|
+
```text
|
|
883
|
+
$i18n = i18n({
|
|
884
|
+
locale: "fr-FR",
|
|
885
|
+
fallback: "en",
|
|
886
|
+
messages: {
|
|
887
|
+
greeting: "Bonjour, ${name}!",
|
|
888
|
+
orders: { title: "Commandes récentes" }
|
|
889
|
+
}
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
welcome = Text(t("greeting", { name: $user.name }))
|
|
893
|
+
sectionTitle = SectionHeader(t("orders.title"))
|
|
894
|
+
formatted = Text(@Format(1234.5, "currency", "EUR", Locale()))
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Keys support dot paths. Variables are interpolated using `${name}`
|
|
898
|
+
placeholders. Missing keys fall back to the fallback locale's bundle,
|
|
899
|
+
then to the bare key as a literal string.
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## System prompt generator
|
|
904
|
+
|
|
905
|
+
The bundle ships a tiny generator that walks the registered component
|
|
906
|
+
library, builtin catalog, and (optionally) host-registered tools, then
|
|
907
|
+
emits a clean, ordered prompt teaching the LLM exactly what's available.
|
|
908
|
+
|
|
909
|
+
Two flavours:
|
|
910
|
+
|
|
911
|
+
| Variant | Built-in path | API | Use when |
|
|
912
|
+
| ------------- | -------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
913
|
+
| **Full** | `dist/system_prompt.txt` | `el.getSystemPrompt()` or `{ mode: "full" }` | Generating full applications — dashboards, multi-page websites, settings consoles, admin apps. |
|
|
914
|
+
| **Chat** | `dist/system_prompt_chat.txt` | `el.getSystemPrompt({ mode: "chat" })` | Converting an LLM's prose answer into a rich, read-only UI surface (cards, tables, charts). |
|
|
915
|
+
|
|
916
|
+
`PromptOptions`:
|
|
917
|
+
|
|
918
|
+
```ts
|
|
919
|
+
interface PromptOptions {
|
|
920
|
+
mode?: "full" | "chat";
|
|
921
|
+
preamble?: string; // Replace the opening sentence
|
|
922
|
+
additionalRules?: string[]; // Bullets under "## Additional rules"
|
|
923
|
+
examples?: string[]; // Worked-example snippets
|
|
924
|
+
tools?: ToolSpec[]; // Surfaced under "## Available endpoints"
|
|
925
|
+
toolExamples?: string[]; // Worked tool examples
|
|
926
|
+
toolCalls?: boolean; // Force-include HTTP / tool sections
|
|
927
|
+
bindings?: boolean; // Force-include reactive state + builtins
|
|
928
|
+
inlineMode?: boolean; // Permit fenced ```aktion blocks
|
|
929
|
+
editMode?: boolean; // Emit only changed statements
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Both prompts are kept in lock-step with the library by `npm run build`.
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## Tooling
|
|
938
|
+
|
|
939
|
+
[`src/tooling/index.ts`](./src/tooling/index.ts) exports the full
|
|
940
|
+
host-side helper surface:
|
|
941
|
+
|
|
942
|
+
```ts
|
|
943
|
+
import {
|
|
944
|
+
formatProgram, // canonical pretty-printer (idempotent)
|
|
945
|
+
applyDelta, // structured-edit protocol
|
|
946
|
+
inspectAST, // structured Committed + Drafting AST snapshot
|
|
947
|
+
getDiagnostics, // merged parse + schema errors (LSP-ready)
|
|
948
|
+
getCompletions, // context-aware completions
|
|
949
|
+
getHoverInfo, // hover docs for symbols
|
|
950
|
+
} from "@aktion/runtime";
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
- `formatProgram` projects the parsed AST back to canonical source —
|
|
954
|
+
`prop: value` named args, double-quoted strings, two-space block
|
|
955
|
+
indentation, `bind:` preserved, template literals intact.
|
|
956
|
+
- `inspectAST(source)` returns a JSON-friendly view of the Committed +
|
|
957
|
+
Drafting ASTs at the current byte position — bindings (with
|
|
958
|
+
kind / line / column / summary), in-flight names, and any parse errors.
|
|
959
|
+
- `applyDelta(programText, ops)` patches a program with a structured
|
|
960
|
+
sequence of operations and returns the new text plus any advisory
|
|
961
|
+
warnings. Used by the element-level `el.applyDelta(...)` method.
|
|
962
|
+
- `getDiagnostics`, `getCompletions`, and `getHoverInfo` are the data
|
|
963
|
+
layer a real LSP server would wrap. The
|
|
964
|
+
[playground](https://asfand-dev.github.io/aktion/playground.html)
|
|
965
|
+
uses them under the hood.
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
## Documentation site
|
|
970
|
+
|
|
971
|
+
The `docs/` folder is the source for the live documentation site at
|
|
972
|
+
<https://asfand-dev.github.io/aktion/>. Every page is a
|
|
973
|
+
static HTML file that loads the same bundle the rest of the world
|
|
974
|
+
consumes from the CDN.
|
|
975
|
+
|
|
976
|
+
| Page | What's on it |
|
|
977
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------- |
|
|
978
|
+
| `index.html` | Overview, drop-in install, live theme picker. |
|
|
979
|
+
| `get-started.html` | Step-by-step integration walkthrough. |
|
|
980
|
+
| `frameworks.html` | Integration recipes for React, Next.js, Vue, Angular, Svelte, plain HTML. |
|
|
981
|
+
| `language.html` | Full Aktion language reference. |
|
|
982
|
+
| `components.html` | Every built-in component with a live preview, positional signatures, prop tables, and enum values. |
|
|
983
|
+
| `actions.html` | `action Name() { … }` guide — declarative state mutations, optimistic snapshot/rollback, lambda-based click handlers, navigation, and end-to-end examples. |
|
|
984
|
+
| `side-effects.html` | `effect [ ...deps ] { … }` guide — anonymous side effects, dependency entries (state, lifecycle, intervals, debounce/throttle), cleanup, and effect vs. action. |
|
|
985
|
+
| `javascript-interactions.html` | `effect [ ...deps ] { js { … } }` + action `js{}` bodies — the JS escape hatch. |
|
|
986
|
+
| `routing.html` | Hash-based routing guide — always available at runtime. |
|
|
987
|
+
| `themes.html` | Built-in themes gallery, live picker, side-by-side compare, and the token customization studio. |
|
|
988
|
+
| `examples.html` | Curated showcase of real-world block UIs (auth, products, FAQ, cart, todos, …). |
|
|
989
|
+
| `playground.html` | CodeMirror 6 editor with custom highlighting / autocomplete, live preview, share links, hover-over component info, and an inspection mode. |
|
|
990
|
+
| `chat-bot.html` | OpenRouter-powered streaming chat with four generation modes (Chat Compact, Chat Full, Website Builder, App Builder), image / PDF attachment support, and download-as-standalone-HTML. |
|
|
991
|
+
| `live-examples.html` | Catalog page that links every demo into the shared `live-example.html?example=<slug>` shell. |
|
|
992
|
+
| `live-example.html` | Shared shell for the bundled live examples — picks the demo from the `?example=<slug>` query parameter. |
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
## Live examples
|
|
997
|
+
|
|
998
|
+
Every standalone demo is served by a single shell page
|
|
999
|
+
(`docs/live-example.html`) and a single JS bundle
|
|
1000
|
+
(`docs/assets/live-example.js`) that ships every demo's UI Script source
|
|
1001
|
+
and setup code together. Open any example with
|
|
1002
|
+
`live-example.html?example=<slug>` — the shell renders the original
|
|
1003
|
+
hero / source / output layout, so each demo doubles as an integration
|
|
1004
|
+
recipe for `setResponse`, `appendChunk`, `setTools`, and `setTheme`.
|
|
1005
|
+
|
|
1006
|
+
| Demo slug | Highlights |
|
|
1007
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
1008
|
+
| `routing-demo` | A four-page app driven by `pages = _router_({ … })` + `NavLink`, deep links, browser back/forward. |
|
|
1009
|
+
| `settings-app` | Tabs, `Switch`, `ToggleGroup`, `Progress`, `Kbd`, danger-zone confirmation `Drawer`. |
|
|
1010
|
+
| `data-explorer` | Analytics surface: sortable `DataGrid` + bulk toolbar, `Gauge` SLA dials, `LineChart`, `Heatmap`, `RadarChart`, `ScatterChart`, `Histogram`, `InfiniteList`, `ActivityLog`. |
|
|
1011
|
+
| `media-gallery` | Travel magazine: `Carousel` hero, `Gallery` + click-to-zoom `Lightbox`, `VideoPlayer`, `AudioPlayer`, Leaflet-backed `Map`. |
|
|
1012
|
+
| `content-studio` | CMS-style authoring surface: `RichTextEditor`, `CodeEditor`, `MultiStepForm`, `ColorPicker`, `TagInput`, `MentionInput`, `PinInput`, `ValidationSummary`, `TopBar`. |
|
|
1013
|
+
| `brand-themes.html` | Same UI reskinned with `Theme({...})` for **GitHub**, **Apple**, **Stripe**, **IONOS**, **Notion**, **Vercel** (bespoke UI on its own page). |
|
|
1014
|
+
|
|
1015
|
+
The full catalog with tag filters lives at
|
|
1016
|
+
[`docs/live-examples.html`](https://asfand-dev.github.io/aktion/live-examples.html).
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## Project layout
|
|
1021
|
+
|
|
1022
|
+
```
|
|
1023
|
+
.
|
|
1024
|
+
├── src/ # Library source
|
|
1025
|
+
│ ├── parser/ # Lexer, parser, AST types
|
|
1026
|
+
│ ├── runtime/ # Evaluator, reactive state, effects, HTTP, i18n
|
|
1027
|
+
│ │ ├── builtins.ts # pure @-function helpers
|
|
1028
|
+
│ │ ├── evaluator.ts # program planner + binding resolver
|
|
1029
|
+
│ │ ├── state.ts # reactive store — `$name = value`
|
|
1030
|
+
│ │ ├── effects.ts # EffectRunner + ActionDeclRunner + js{} executor
|
|
1031
|
+
│ │ ├── http.ts # http({...}) reactive HTTP primitive + interceptors
|
|
1032
|
+
│ │ ├── i18n.ts # $i18n runtime + t() / Locale() builtins
|
|
1033
|
+
│ │ ├── storage.ts # storage.local / .session / .cookies bridge
|
|
1034
|
+
│ │ ├── console.ts # console.* host bridge
|
|
1035
|
+
│ │ └── router.ts # Hash-based router for _router_({…}) calls and NavLink
|
|
1036
|
+
│ ├── library/ # Component specs and registry
|
|
1037
|
+
│ │ └── components/ # layout / content / forms / data / charts / chat /
|
|
1038
|
+
│ │ # feedback / navigation / menu / patterns / helpers / router
|
|
1039
|
+
│ ├── renderer/ # Tree → DOM
|
|
1040
|
+
│ │ ├── renderer.ts # walks the tree, calls component renderers
|
|
1041
|
+
│ │ └── morph.ts # React-like DOM reconciler — keeps focus, selection, scroll, <details>.open
|
|
1042
|
+
│ ├── theme/ # Token system + injected stylesheet
|
|
1043
|
+
│ ├── prompt/ # System prompt generator
|
|
1044
|
+
│ ├── tooling/ # Host-side helpers (formatter, inspector, language service)
|
|
1045
|
+
│ ├── language/ # Reusable language-support module
|
|
1046
|
+
│ ├── icons/ # Font Awesome CDN loader
|
|
1047
|
+
│ ├── element.ts # The custom element
|
|
1048
|
+
│ └── index.ts # Public entry point
|
|
1049
|
+
├── docs/ # Static documentation site (HTML + CSS + JS)
|
|
1050
|
+
│ ├── _examples/ # Author-facing source for every bundled live example
|
|
1051
|
+
│ └── assets/live-example.js # GENERATED single-bundle for live-example.html
|
|
1052
|
+
├── _docs/ # Internal design notes and inspirations (not shipped)
|
|
1053
|
+
├── scripts/
|
|
1054
|
+
│ ├── emit-prompt.mjs # Writes dist/system_prompt*.txt from the bundle
|
|
1055
|
+
│ └── build-docs.mjs # Assembles ./site/ from docs/ + dist/
|
|
1056
|
+
├── tests/ # Vitest unit + element regression tests
|
|
1057
|
+
├── dist/ # Built artifacts (created by `npm run build`)
|
|
1058
|
+
├── site/ # Deployable static docs (created by `npm run build:docs`)
|
|
1059
|
+
├── .github/workflows/ # GitHub Pages deploy pipeline
|
|
1060
|
+
├── README.md # This file
|
|
1061
|
+
└── coding-gen-skill.md # Deep authoring knowledge base
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Run it locally
|
|
1067
|
+
|
|
1068
|
+
Requirements: **Node ≥ 18** and **npm ≥ 9** (pnpm/yarn work too).
|
|
1069
|
+
|
|
1070
|
+
### Install
|
|
1071
|
+
|
|
1072
|
+
```bash
|
|
1073
|
+
git clone https://github.com/asfand-dev/aktion.git
|
|
1074
|
+
cd aktion
|
|
1075
|
+
npm install
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### Build the library and system prompt
|
|
1079
|
+
|
|
1080
|
+
```bash
|
|
1081
|
+
npm run build
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
Produces:
|
|
1085
|
+
|
|
1086
|
+
```
|
|
1087
|
+
dist/aktion.js # ESM bundle (CDN entry)
|
|
1088
|
+
dist/aktion.umd.cjs # UMD bundle for older bundlers
|
|
1089
|
+
dist/aktion.iife.js # IIFE for non-module <script> tags
|
|
1090
|
+
dist/aktion.css # Stylesheet (also inlined into the JS bundles)
|
|
1091
|
+
dist/index.js # ESM npm entry — re-exports aktion.js
|
|
1092
|
+
dist/index.cjs # CommonJS npm entry — wraps aktion.umd.cjs
|
|
1093
|
+
dist/index.d.ts # TypeScript types entry
|
|
1094
|
+
dist/types/ # Per-module .d.ts declarations
|
|
1095
|
+
dist/system_prompt.txt # Full prompt — every feature
|
|
1096
|
+
dist/system_prompt_chat.txt # Compact chat-focused prompt
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Publish to npm
|
|
1100
|
+
|
|
1101
|
+
The package is published as `@aktion/runtime`. The `files` field
|
|
1102
|
+
restricts the tarball to `dist/` only, and `prepublishOnly` runs the
|
|
1103
|
+
full build, so a release is:
|
|
1104
|
+
|
|
1105
|
+
```bash
|
|
1106
|
+
npm publish
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
Run `npm pack --dry-run` first to confirm the tarball contains only the
|
|
1110
|
+
expected `dist/` artefacts.
|
|
1111
|
+
|
|
1112
|
+
The two prompt variants exist so host apps can pick the right flavour
|
|
1113
|
+
up front. Both are kept in lock-step with the library by the build
|
|
1114
|
+
script.
|
|
1115
|
+
|
|
1116
|
+
### Run the test suite
|
|
1117
|
+
|
|
1118
|
+
```bash
|
|
1119
|
+
npm test
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
The suite covers:
|
|
1123
|
+
|
|
1124
|
+
- Parser / lexer correctness (`tests/parser.test.ts`).
|
|
1125
|
+
- Runtime evaluator + reactive state + `http({...})` (`tests/runtime.test.ts`).
|
|
1126
|
+
- `effect` / `action` declarations + `js{}` execution (`tests/javascript-integration.test.ts`).
|
|
1127
|
+
- Hash-based router + `NavLink` (`tests/router.test.ts`).
|
|
1128
|
+
- Theme resolution and token application (`tests/theme.test.ts`).
|
|
1129
|
+
- In-script `Theme(...)` overrides (`tests/in-script-theme.test.ts`).
|
|
1130
|
+
- Component library smoke tests (`tests/library.test.ts`).
|
|
1131
|
+
- Element-level integration via happy-dom (`tests/element.test.ts`).
|
|
1132
|
+
- System prompt generator output (`tests/prompt.test.ts`).
|
|
1133
|
+
- Storage + console globals (`tests/storage-console.test.ts`).
|
|
1134
|
+
- End-to-end programs (`tests/suis2-end-to-end.test.ts`).
|
|
1135
|
+
- Language support spec for editor / tooling integrations (`tests/language.test.ts`).
|
|
1136
|
+
- One-positional-max enforcement (`tests/suis2-one-positional.test.ts`).
|
|
1137
|
+
- Component prop aliases (`tests/suis2-prop-aliases.test.ts`).
|
|
1138
|
+
- Icon rendering (`tests/icons.test.ts`).
|
|
1139
|
+
|
|
1140
|
+
### Build the documentation site
|
|
1141
|
+
|
|
1142
|
+
```bash
|
|
1143
|
+
npm run build:docs
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Assembles `./site/` from `./docs/` + `./dist/`. Serve it with anything
|
|
1147
|
+
static:
|
|
1148
|
+
|
|
1149
|
+
```bash
|
|
1150
|
+
npx http-server site -p 4321
|
|
1151
|
+
# or
|
|
1152
|
+
npx serve site
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
Then open <http://localhost:4321/index.html>.
|
|
1156
|
+
|
|
1157
|
+
---
|
|
1158
|
+
|
|
1159
|
+
## Security
|
|
1160
|
+
|
|
1161
|
+
The library treats every LLM-supplied attribute as untrusted and runs
|
|
1162
|
+
it through a small set of sanitisers before it lands on the DOM. HTTP
|
|
1163
|
+
requests issued by the LLM through `http({...})` flow through your host's
|
|
1164
|
+
`registerHttpInterceptors({ onRequest, onResponse, onError })` chain so
|
|
1165
|
+
auth headers, CORS workarounds, and refresh-token retries stay under
|
|
1166
|
+
host control.
|
|
1167
|
+
|
|
1168
|
+
| Sink | Helper | Effect |
|
|
1169
|
+
| -------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
1170
|
+
| Anchor `href` (`Link`, `BreadcrumbItem`, `NavbarItem`, Markdown links) | `sanitiseHref` | Allow-lists `http(s):`, `mailto:`, `tel:`, fragments, root-relative paths. Rejects `javascript:`, `vbscript:`, `data:text/html`, control-char bypasses (`java\tscript:`), protocol-relative `//host/...`. Unsafe URLs collapse to `#`. |
|
|
1171
|
+
| Image `src` (`Image`, `Avatar`, `MediaCard`, `Hero`, `Testimonial`, `ChatBubble`) | `sanitiseImageSrc` | Allow-lists `http(s):`, `data:image/*`, `blob:`, plus relative paths. Anything else falls back to an empty string so callers render a placeholder. |
|
|
1172
|
+
| Inline `style` lengths (`Container.maxWidth`, `Skeleton.height`, …) | `sanitiseCssLength` | Restricts the alphabet so semicolons / quotes cannot inject extra declarations. |
|
|
1173
|
+
| `background-image: url(...)` (`Hero.imageSrc`) | `sanitiseCssUrl` | Drops characters that would close the `url()` literal. |
|
|
1174
|
+
| `helpers.openUrl(...)` from an action body | `sanitiseHref` (renderer) | The renderer sanitises the URL before calling `window.open`. External windows open with `noopener,noreferrer`. |
|
|
1175
|
+
|
|
1176
|
+
External links rendered by `Link`, `NavbarItem`, and the Markdown
|
|
1177
|
+
renderer get `rel="noopener noreferrer"` so the destination cannot
|
|
1178
|
+
read the opener's `document.referrer`.
|
|
1179
|
+
|
|
1180
|
+
If you embed `<aktion-app>` behind a CSP, the bundle does not
|
|
1181
|
+
use `eval`. `js{}` bodies inside `effect` / `action` declarations are
|
|
1182
|
+
evaluated with `new Function(...)` which requires `'unsafe-eval'` if
|
|
1183
|
+
you want them to work; if you cannot relax CSP, simply avoid emitting
|
|
1184
|
+
`js{}` blocks from the LLM — every other part of the runtime keeps
|
|
1185
|
+
working without the JS escape hatch.
|
|
1186
|
+
|
|
1187
|
+
---
|
|
1188
|
+
|
|
1189
|
+
## CDN deployment
|
|
1190
|
+
|
|
1191
|
+
This repository ships its own copy of the bundle on GitHub Pages, so
|
|
1192
|
+
most users do not need to host anything themselves:
|
|
1193
|
+
|
|
1194
|
+
```html
|
|
1195
|
+
<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>
|
|
1196
|
+
<aktion-app theme="dark"></aktion-app>
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
…plus a fetch of `system_prompt.txt` server-side to build LLM messages:
|
|
1200
|
+
|
|
1201
|
+
```bash
|
|
1202
|
+
curl https://asfand-dev.github.io/aktion/dist/system_prompt.txt
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
To ship your own copy, run `npm run build` and serve the `dist/` folder
|
|
1206
|
+
from any static host — every artifact in `dist/` is self-contained.
|
|
1207
|
+
|
|
1208
|
+
GitHub Pages deployment for this repo is automated via
|
|
1209
|
+
[`.github/workflows/deploy-pages.yml`](.github/workflows/deploy-pages.yml).
|
|
1210
|
+
Push to `main` and the workflow will build, test, assemble `site/`, and
|
|
1211
|
+
publish.
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## Contributing
|
|
1216
|
+
|
|
1217
|
+
Contributions are very welcome. The fastest path is:
|
|
1218
|
+
|
|
1219
|
+
1. Fork and clone the repo.
|
|
1220
|
+
2. `npm install && npm test` — make sure the suite is green on `main` first.
|
|
1221
|
+
3. Make your change in a focused branch (e.g. `feat/inline-charts`).
|
|
1222
|
+
4. Add or update tests in `tests/`. Aim for good edge-case coverage.
|
|
1223
|
+
5. Run `npm run build` to confirm the bundle and the system prompt still build.
|
|
1224
|
+
6. Open a pull request describing the motivation and any user-visible changes.
|
|
1225
|
+
|
|
1226
|
+
Two cursor rules keep documentation in sync with the code:
|
|
1227
|
+
|
|
1228
|
+
- [`.cursor/rules/readme-sync.mdc`](.cursor/rules/readme-sync.mdc) — when
|
|
1229
|
+
you change the public API, attribute set, component list, theme list,
|
|
1230
|
+
or build outputs, update this README in the same commit.
|
|
1231
|
+
- [`.cursor/rules/coding-gen-skill-sync.mdc`](.cursor/rules/coding-gen-skill-sync.mdc) —
|
|
1232
|
+
when you add or change a component, builtin, action step, theme, or
|
|
1233
|
+
authoring rule, update `coding-gen-skill.md` so LLMs consuming this
|
|
1234
|
+
library don't generate broken code.
|
|
1235
|
+
|
|
1236
|
+
Issues, design discussions, and bug reports are tracked at
|
|
1237
|
+
<https://github.com/asfand-dev/aktion/issues>.
|
|
1238
|
+
|
|
1239
|
+
By contributing you agree that your work will be released under the
|
|
1240
|
+
project's MIT license.
|
|
1241
|
+
|
|
1242
|
+
---
|
|
1243
|
+
|
|
1244
|
+
## License
|
|
1245
|
+
|
|
1246
|
+
MIT — see [LICENSE](LICENSE).
|