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.
Files changed (76) hide show
  1. package/README.md +1246 -0
  2. package/dist/aktion.iife.js +8431 -0
  3. package/dist/aktion.iife.js.map +1 -0
  4. package/dist/aktion.js +22594 -0
  5. package/dist/aktion.js.map +1 -0
  6. package/dist/aktion.umd.cjs +8431 -0
  7. package/dist/aktion.umd.cjs.map +1 -0
  8. package/dist/index.cjs +3 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +5 -0
  11. package/dist/system_prompt.txt +1095 -0
  12. package/dist/system_prompt_chat.txt +404 -0
  13. package/dist/types/element.d.ts +175 -0
  14. package/dist/types/icons/index.d.ts +45 -0
  15. package/dist/types/index.d.ts +15 -0
  16. package/dist/types/language/builtins.d.ts +33 -0
  17. package/dist/types/language/components.d.ts +28 -0
  18. package/dist/types/language/grammar.d.ts +121 -0
  19. package/dist/types/language/index.d.ts +41 -0
  20. package/dist/types/language/snippets.d.ts +17 -0
  21. package/dist/types/library/components/_internal.d.ts +56 -0
  22. package/dist/types/library/components/advanced-charts.d.ts +6 -0
  23. package/dist/types/library/components/advanced-data.d.ts +6 -0
  24. package/dist/types/library/components/advanced-forms.d.ts +12 -0
  25. package/dist/types/library/components/advanced-patterns.d.ts +13 -0
  26. package/dist/types/library/components/charts.d.ts +5 -0
  27. package/dist/types/library/components/chat.d.ts +6 -0
  28. package/dist/types/library/components/content.d.ts +33 -0
  29. package/dist/types/library/components/data.d.ts +9 -0
  30. package/dist/types/library/components/editors.d.ts +5 -0
  31. package/dist/types/library/components/feedback.d.ts +14 -0
  32. package/dist/types/library/components/forms-shared.d.ts +7 -0
  33. package/dist/types/library/components/forms.d.ts +21 -0
  34. package/dist/types/library/components/helpers.d.ts +33 -0
  35. package/dist/types/library/components/layout.d.ts +20 -0
  36. package/dist/types/library/components/media.d.ts +7 -0
  37. package/dist/types/library/components/menu.d.ts +5 -0
  38. package/dist/types/library/components/navigation.d.ts +6 -0
  39. package/dist/types/library/components/new-components.d.ts +13 -0
  40. package/dist/types/library/components/patterns.d.ts +39 -0
  41. package/dist/types/library/components/router.d.ts +2 -0
  42. package/dist/types/library/components/theme.d.ts +2 -0
  43. package/dist/types/library/index.d.ts +5 -0
  44. package/dist/types/library/registry.d.ts +15 -0
  45. package/dist/types/library/types.d.ts +140 -0
  46. package/dist/types/library/utils.d.ts +73 -0
  47. package/dist/types/library/validate.d.ts +27 -0
  48. package/dist/types/parser/frontier.d.ts +65 -0
  49. package/dist/types/parser/index.d.ts +4 -0
  50. package/dist/types/parser/lexer.d.ts +46 -0
  51. package/dist/types/parser/parser.d.ts +2 -0
  52. package/dist/types/parser/types.d.ts +349 -0
  53. package/dist/types/prompt/generator.d.ts +33 -0
  54. package/dist/types/prompt/index.d.ts +1 -0
  55. package/dist/types/renderer/index.d.ts +1 -0
  56. package/dist/types/renderer/morph.d.ts +42 -0
  57. package/dist/types/renderer/renderer.d.ts +73 -0
  58. package/dist/types/runtime/builtins.d.ts +27 -0
  59. package/dist/types/runtime/console.d.ts +21 -0
  60. package/dist/types/runtime/effects.d.ts +69 -0
  61. package/dist/types/runtime/evaluator.d.ts +151 -0
  62. package/dist/types/runtime/http.d.ts +85 -0
  63. package/dist/types/runtime/i18n.d.ts +40 -0
  64. package/dist/types/runtime/index.d.ts +9 -0
  65. package/dist/types/runtime/router.d.ts +105 -0
  66. package/dist/types/runtime/state.d.ts +84 -0
  67. package/dist/types/runtime/storage.d.ts +50 -0
  68. package/dist/types/theme/index.d.ts +175 -0
  69. package/dist/types/theme/styles.d.ts +9 -0
  70. package/dist/types/tooling/codemod.d.ts +36 -0
  71. package/dist/types/tooling/delta.d.ts +74 -0
  72. package/dist/types/tooling/formatter.d.ts +8 -0
  73. package/dist/types/tooling/index.d.ts +29 -0
  74. package/dist/types/tooling/inspector.d.ts +49 -0
  75. package/dist/types/tooling/language-service.d.ts +57 -0
  76. package/package.json +63 -0
package/README.md ADDED
@@ -0,0 +1,1246 @@
1
+ # aktion
2
+
3
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+ [![Docs](https://img.shields.io/badge/docs-github%20pages-6366f1)](https://asfand-dev.github.io/aktion/)
5
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-10b981.svg)](#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).