@typecad/ui 0.1.0-alpha.1

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
package/README.md ADDED
@@ -0,0 +1,1443 @@
1
+ # @typecad/ui — HTML/CSS-Driven Graphics for Microcontrollers
2
+
3
+ Write UIs in HTML and CSS. TypeCAD's `cuttlefish` transpiler lowers them to a retained-mode C++ runtime that draws on ILI9341 (and future) displays over hardware SPI. No browser, no DOM, no CSS engine on the device — everything is resolved at transpile time.
4
+
5
+ ## Install
6
+
7
+ `@typecad/ui` is the authoring API you import in source; `@typecad/cuttlefish` is the transpiler that lowers those imports to firmware at build time. You need both, plus a board package:
8
+
9
+ ```bash
10
+ npm install @typecad/ui @typecad/board-esp32-devkit
11
+ npm install --save-dev @typecad/cuttlefish
12
+ ```
13
+
14
+ `@typecad/ui` is compile-time only — none of its code is shipped to the device. The transpiler intercepts `ui.mount` / `ui.signal` / `ui.bind` / ... calls and lowers them to device variables and binding-table entries, so the package can be safely kept in `dependencies`.
15
+
16
+ ## Project layout
17
+
18
+ A TypeCAD UI project has one **entry** — the file you point `cuttlefish.config.ts` at. The entry can be either a `.ui` single-file component or a plain `.ts` module. Both intermix freely with regular cuttlefish TypeScript (HAL pin reads, `setInterval`, `console.log`, your own `.ts` modules) — the `<script>` block of a `.ui` file and a standalone `.ts` file are lowered by the same pipeline.
19
+
20
+ The transpiler accepts three entry extensions: `.ts`, `.tsx`, and `.ui`.
21
+
22
+ ### Pattern A — `.ui` single-file component (display + logic together)
23
+
24
+ A `.ui` file is a Svelte-style single-file component with three sections — `<script>`, `<style>`, and the HTML template — in one file. The blocks may appear in any order; only one `<script>` is supported, and multiple `<style>` blocks are concatenated.
25
+
26
+ The transpiler injects an implicit `import { screen } from './app.ui.html'` into the script, so the in-file template is referenceable as `screen.*` without an explicit import.
27
+
28
+ **`src/app.ui`** — markup, styling, and behavior for one screen in one file:
29
+
30
+ ```html
31
+ <script>
32
+ import { ui } from '@typecad/ui';
33
+ import { A0 } from '@typecad/board-esp32-devkit';
34
+
35
+ ui.mount(screen);
36
+
37
+ // Plain TypeScript — same lowering as a standalone .ts. HAL pin reads,
38
+ // timers, and UI writes coexist as regular statements.
39
+ const sensor = A0.asInput();
40
+
41
+ setInterval(() => {
42
+ screen.reading.value = sensor.readAnalog();
43
+ }, 500);
44
+
45
+ ui.bind(screen.lamp, 'background', () =>
46
+ screen.reading.value > 512 ? 'limegreen' : '#333'
47
+ );
48
+ </script>
49
+
50
+ <style>
51
+ screen { background: #1a1a2e; display: flex; flex-direction: column; padding: 10px; gap: 8px; }
52
+ #reading { color: dodgerblue; font-size: 24px; }
53
+ #lamp { width: 16px; height: 16px; border-radius: 4px; }
54
+ </style>
55
+
56
+ <screen>
57
+ <text id="reading">0</text>
58
+ <view id="lamp"></view>
59
+ </screen>
60
+ ```
61
+
62
+ **`cuttlefish.config.ts`** — point the entry at the `.ui` file:
63
+
64
+ ```typescript
65
+ import type { CuttlefishConfig } from '@typecad/cuttlefish/api';
66
+
67
+ const config: CuttlefishConfig = {
68
+ entry: './src/app.ui',
69
+ target: 'esp32',
70
+ mcu: '@typecad/mcu-esp32',
71
+ board: '@typecad/board-esp32-devkit',
72
+ framework: '@typecad/framework-arduino',
73
+ frameworkData: { buildTarget: 'esp32:esp32:esp32' },
74
+ output: { framework: 'arduino', optimize: 'size', outDir: './out' },
75
+ toolchain: { type: 'arduino-cli' },
76
+ console: { baudRate: 115200 },
77
+ display: { profile: 'ili9341-spi', cs: 5, dc: 21, rst: 22, backlight: 33 },
78
+ };
79
+ export default config;
80
+ ```
81
+
82
+ Use this layout when a screen's markup and behavior are tightly coupled and you want them in one place.
83
+
84
+ ### Pattern B — `main.ts` entry + `.ui.html` for display (logic factored out)
85
+
86
+ When the sensor/IO logic is substantial, factor it into plain `.ts` modules and keep the UI file purely declarative. `main.ts` is the entry; it imports `screen` from a `.ui.html` file **and** the sensor functions from a sibling `.ts`, then marries them.
87
+
88
+ The import graph resolves both directions: `main.ts → app.ui.html` (for `screen`) and `main.ts → sensors.ts` (for the reading). Cross-module function calls survive lowering into the emitted C++.
89
+
90
+ **`src/main.ts`** — the entry, wires display and sensor logic:
91
+
92
+ ```typescript
93
+ import { ui } from '@typecad/ui';
94
+ import { screen } from './app.ui.html';
95
+ import { readVolts } from './sensors.js'; // .js extension required (Node16 resolution)
96
+
97
+ ui.mount(screen, { display: 'ili9341', bus: 'SPI', cs: 10, dc: 9, rst: 8 });
98
+
99
+ setInterval(() => {
100
+ screen.meter.value = Math.round(readVolts() * 100);
101
+ }, 500);
102
+
103
+ export function main(): void { while (true) {} }
104
+ ```
105
+
106
+ **`src/sensors.ts`** — plain cuttlefish TS, owns device I/O, no UI imports:
107
+
108
+ ```typescript
109
+ import { A0 } from '@typecad/board-arduino-uno';
110
+ const adc = A0.asInput();
111
+ export function readVolts(): number {
112
+ return adc.readVoltage();
113
+ }
114
+ ```
115
+
116
+ **`src/app.ui.html`** — display only:
117
+
118
+ ```html
119
+ <screen>
120
+ <range id="meter" min="0" max="330"></range>
121
+ </screen>
122
+ ```
123
+
124
+ **`cuttlefish.config.ts`** — point the entry at `main.ts`:
125
+
126
+ ```typescript
127
+ const config: CuttlefishConfig = {
128
+ entry: './src/main.ts', // ← .ts entry instead of .ui
129
+ // ...rest identical
130
+ };
131
+ ```
132
+
133
+ The transpiler lowers the `setInterval` callback to a timer that writes the node table and marks it dirty, preserving the cross-module `readVolts()` call verbatim:
134
+
135
+ ```c
136
+ static void __tc_timer_cb_0() {
137
+ __ui_nodes[1].value = round(readVolts() * 100); // sensor call preserved
138
+ ui_mark_dirty(1); // screen.meter.value = ... lowered
139
+ }
140
+ ```
141
+
142
+ Use this layout when sensor or IO logic is large enough to deserve its own module, or when you want the UI file to stay purely declarative.
143
+
144
+ ### Choosing between the two
145
+
146
+ | Layout | Entry | Use when |
147
+ |---|---|---|
148
+ | `.ui` single-file | `entry: './src/app.ui'` | Display and behavior are tightly coupled; one screen's markup + logic belong together |
149
+ | `main.ts` + `.ui.html` split | `entry: './src/main.ts'` | Sensor/IO logic is substantial; UI file should stay declarative |
150
+
151
+ Both patterns can import the same things (`@typecad/ui`, the board package, the HAL, your own `.ts` modules), and both lower through the same pipeline. You can also mix them within a project — a `.ui` entry can import helper functions from a sibling `.ts`, and a `main.ts` entry can import `screen` from multiple `.ui.html` files.
152
+
153
+ ### A note on dynamic text bindings
154
+
155
+ Most bindings lower general expressions and work in either layout — color, background, border, value, visibility. The one exception is `ui.bind(node, 'text', ...)`, whose body in v1 only lowers three shapes: `String(<numeric>)`, a bare string literal, or a template literal with numeric interpolations. Anything else (including a cross-module function call) lowers to an empty body and emits a `ui-bind-text-unlowered` warning.
156
+
157
+ For dynamic text that doesn't fit those shapes, write to the element from `setInterval` instead:
158
+
159
+ ```typescript
160
+ // Avoid (body won't lower in v1):
161
+ ui.bind(screen.reading, 'text', () => readVolts().toFixed(2) + 'V');
162
+
163
+ // Prefer — drive from a timer:
164
+ setInterval(() => {
165
+ // write to .value on numeric elements, or use String(<expr>) in the bind
166
+ screen.meter.value = Math.round(readVolts() * 100);
167
+ }, 500);
168
+ ```
169
+
170
+ ## Quick start
171
+
172
+ The fastest path is a single `.ui` file with markup, styling, and behavior together (see [Project layout](#project-layout) for the alternative `main.ts` + `.ui.html` split).
173
+
174
+ ### 1. Create `src/app.ui`
175
+
176
+ ```html
177
+ <script>
178
+ import { ui } from '@typecad/ui';
179
+
180
+ ui.mount(screen);
181
+
182
+ // A signal carries the count; the binding recomputes the label each frame.
183
+ // Text updates via ui.bind(..., 'text', ...) — <text> elements don't expose
184
+ // a writable .text. String(<number>) is one of the supported bind shapes.
185
+ const taps = ui.signal(0);
186
+ ui.bind(screen.count, 'text', () => String(taps()));
187
+
188
+ screen.action.onClick(() => {
189
+ taps.set(taps() + 1);
190
+ });
191
+ </script>
192
+
193
+ <style>
194
+ screen {
195
+ background: #1a1a2e;
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 8px;
199
+ padding: 10px;
200
+ }
201
+ #count { color: dodgerblue; font-size: 16px; }
202
+ #action {
203
+ background: darkgreen;
204
+ color: white;
205
+ border: 2px solid limegreen;
206
+ border-radius: 4px;
207
+ padding: 8px 20px;
208
+ text-align: center;
209
+ transition: background 300ms;
210
+ }
211
+ #action:pressed { background: limegreen; }
212
+ </style>
213
+
214
+ <screen>
215
+ <text id="count">0</text>
216
+ <button id="action">Start</button>
217
+ </screen>
218
+ ```
219
+
220
+ Display hardware and wiring live in `cuttlefish.config.ts` under `display`, so the UI source stays focused on UI behavior.
221
+
222
+ ### 2. Point the entry at the `.ui` file
223
+
224
+ ```typescript
225
+ // cuttlefish.config.ts
226
+ import type { CuttlefishConfig } from '@typecad/cuttlefish/api';
227
+
228
+ const config: CuttlefishConfig = {
229
+ entry: './src/app.ui',
230
+ target: 'esp32',
231
+ mcu: '@typecad/mcu-esp32',
232
+ board: '@typecad/board-esp32-devkit',
233
+ framework: '@typecad/framework-arduino',
234
+ frameworkData: { buildTarget: 'esp32:esp32:esp32' },
235
+ output: { framework: 'arduino', optimize: 'size', outDir: './out' },
236
+ toolchain: { type: 'arduino-cli' },
237
+ console: { baudRate: 115200 },
238
+ display: { profile: 'ili9341-spi', cs: 5, dc: 21, rst: 22, backlight: 33 },
239
+ };
240
+ export default config;
241
+ ```
242
+
243
+ ### 3. Build
244
+
245
+ ```bash
246
+ npx cuttlefish build --compile
247
+ ```
248
+
249
+ ## Elements
250
+
251
+ ### `<screen>`
252
+
253
+ The root container. Required (exactly one). Its box fills the display viewport (320×240 landscape for ILI9341).
254
+
255
+ ### `<view>`
256
+
257
+ A generic container. Supports `display: flex` for layout. Has a `.value` property and `onToggle`/`onChange` for interaction.
258
+
259
+ ```html
260
+ <view id="row">
261
+ <text id="label">Status</text>
262
+ <text id="value">OK</text>
263
+ </view>
264
+ ```
265
+
266
+ ### `<text>`
267
+
268
+ Static or dynamic text. Has a `.value` property for numeric state.
269
+
270
+ ```html
271
+ <text id="counter">0</text>
272
+ ```
273
+
274
+ ### `<button>`
275
+
276
+ A clickable button with `:pressed` pseudo-state support and transition animations.
277
+
278
+ ```html
279
+ <button id="start">Start</button>
280
+ ```
281
+
282
+ ### Interactive elements
283
+
284
+ | Element | Purpose | `.value` |
285
+ |---|---|---|
286
+ | `<check>` | Checkbox (tap to toggle) | 0 / 1 |
287
+ | `<radio name="g">` | Radio (mutually exclusive within a `name` group) | 0 / 1 |
288
+ | `<select>` | Tap to cycle options; text auto-shows the current option | 0..N-1 |
289
+ | `<progress>` | Progress bar (0-100) | fill percentage |
290
+ | `<range>` | Draggable slider | between `min` and `max` |
291
+ | `<input>` | Text input (tap opens the on-screen keyboard) | — (use `.text`) |
292
+ | `<list>` | Virtualized, data-bound list (renders only visible items) | — |
293
+ | `<canvas>` | User-drawn graphics (sparklines, gauges, custom shapes) via `ui.drawCanvas` | — |
294
+
295
+ ```html
296
+ <range id="brightness" min="0" max="100"></range>
297
+ <progress id="load" value="40"></progress>
298
+ <check id="enable">Enable feature</check>
299
+ <input id="ssid" type="text" placeholder="Network name" maxlength="32"></input>
300
+ <select id="mode"><option>Auto</option><option>Manual</option></select>
301
+ ```
302
+
303
+ Slider and progress values can be driven live from `loop()` via bindings (see
304
+ [Reactive bindings](#reactive-bindings) below).
305
+
306
+ ### Images
307
+
308
+ `<img>` embeds a raw RGB565 `.img` file as a `static const uint16_t[]` array:
309
+
310
+ ```html
311
+ <img id="logo" src="assets/logo.img" width="64" height="64"></img>
312
+ ```
313
+
314
+ The `.img` file is a flat row-major RGB565 dump (width × height × 2 bytes). Use
315
+ `object-fit` (`contain`, `cover`, `fill`) to control scaling.
316
+
317
+ ### HTML tag aliases
318
+
319
+ Common HTML tags are accepted and remapped to the internal primitives, so you
320
+ can write familiar HTML:
321
+
322
+ | HTML tag | Maps to | Notes |
323
+ |---|---|---|
324
+ | `body`, `div`, `header`, `footer`, `nav`, `main`, `section`, `article`, `aside` | `<view>` | Block container |
325
+ | `span`, `p`, `h1`–`h6` | `<text>` | Inline/heading text |
326
+
327
+ ```html
328
+ <header><h1 id="title">Settings</h1></header>
329
+ <main><p id="desc">Adjust preferences.</p></main>
330
+ ```
331
+
332
+ ### Global attributes
333
+
334
+ All UI elements support the `hidden` attribute. Hidden elements and their
335
+ descendants stay in the generated node table, but they do not take space in
336
+ layout and are skipped for drawing and hit testing.
337
+
338
+ ```html
339
+ <view id="advancedPanel" hidden>
340
+ <text>Advanced settings</text>
341
+ </view>
342
+ ```
343
+
344
+ ## CSS reference
345
+
346
+ ### Supported properties
347
+
348
+ #### Box model
349
+ | Property | Values | Notes |
350
+ |---|---|---|
351
+ | `padding` | `8px`, `8px 16px` | Shorthand supported |
352
+ | `margin` | `8px`, `8px 16px` | Shorthand supported |
353
+ | `width` | `100px` | Explicit size |
354
+ | `height` | `50px` | Explicit size |
355
+ | `min-width` / `max-width` | `100px` | Yoga constraints |
356
+ | `min-height` / `max-height` | `50px` | Yoga constraints |
357
+ | `aspect-ratio` | `16 / 9`, `1 / 1`, `1.5` | Infers the missing width or height |
358
+ | `box-sizing` | `border-box` | Yoga border-box |
359
+ | `overflow` | `hidden`, `scroll` | Clips children; `scroll` enables touch-drag scrolling |
360
+
361
+ #### Length units
362
+
363
+ All length values accept `px`, bare numbers, and `rem`/`em` (× 16 root font
364
+ size). `0.625rem` resolves to `10px`. Percentages are used as-is in the contexts
365
+ that honor them (flex/position).
366
+
367
+ `calc()` evaluates simple arithmetic (`+ - * /`) on lengths after `var()`
368
+ substitution, with proper operator precedence:
369
+
370
+ ```css
371
+ :root { --radius: 10px; }
372
+ .card { border-radius: calc(var(--radius) - 4px); } /* 6px */
373
+ .a { padding: calc(0.625rem * 2); } /* 20px */
374
+ ```
375
+
376
+ #### Flexbox (via Yoga)
377
+ | Property | Values |
378
+ |---|---|
379
+ | `display` | `flex`, `none` |
380
+ | `flex-direction` | `row`, `row-reverse`, `column`, `column-reverse` |
381
+ | `gap` | `8px` (sets both row and column gap) |
382
+ | `row-gap` / `column-gap` | `8px` (per-axis; overrides uniform `gap`) |
383
+ | `flex-grow` | `1` |
384
+ | `flex-shrink` | `0` |
385
+ | `flex` (shorthand) | `1`, `1 0 auto`, `none` |
386
+ | `align-items` | `flex-start`, `center`, `flex-end`, `stretch` |
387
+ | `align-self` | `flex-start`, `center`, `flex-end`, `stretch`, `baseline` |
388
+ | `align-content` | `flex-start`, `center`, `flex-end`, `stretch`, `space-between`, `space-around`, `space-evenly` |
389
+ | `justify-content` | `flex-start`, `center`, `flex-end`, `space-between`, `space-around`, `space-evenly` |
390
+ | `flex-wrap` | `wrap`, `nowrap`, `wrap-reverse` |
391
+ | `order` | `1`, `2`, ... |
392
+ | `position` | `relative`, `absolute`, `static` |
393
+ | `top` / `right` / `bottom` / `left` | `10px` |
394
+ | `z-index` | numeric layers; inherited by descendants |
395
+
396
+ `display: none` removes the element subtree from layout, drawing, and hit
397
+ testing while preserving generated node indices.
398
+
399
+ #### Colors
400
+ All standard CSS color formats are supported:
401
+ - `#rrggbb` — `#ff0000`
402
+ - `#rgb` — `#f00`
403
+ - `#rrggbbaa` — `#ff0000ff` (alpha ignored)
404
+ - `rgb(r,g,b)` — `rgb(255, 0, 0)`
405
+ - `rgba(r,g,b,a)` — `rgba(255, 0, 0, 0.5)` (alpha ignored)
406
+ - `hsl(h, s%, l%)` / `hsla(...)` — `hsl(240, 100%, 50%)`, `hsla(0 0% 0% / 0.05)`
407
+ - Named colors — `red`, `dodgerblue`, `limegreen`, `transparent`, ... (147 CSS named colors)
408
+
409
+ Both comma (`hsl(0, 0%, 0%)`) and CSS4 space (`hsl(0 0% 0%)`) syntaxes work.
410
+ The slash-alpha form (`hsl(0 0% 0% / 0.05)`) is honored in `box-shadow` alpha;
411
+ elsewhere alpha is ignored (no runtime blending on bare metal).
412
+
413
+ #### Typography
414
+ | Property | Values | Notes |
415
+ |---|---|---|
416
+ | `color` | any color | Text foreground color |
417
+ | `font-family` | `"MyFont"` | Uses a generated font when matched by `@font-face`; otherwise the built-in bitmap font |
418
+ | `font-size` | `16px` | Generated fonts are rasterized at this pixel size; bitmap text maps to GFX text size |
419
+ | `font` | `italic bold 18px DeviceSans` | Shorthand support for style, weight, size, and family |
420
+ | `text-align` | `left`, `center`, `right` | Horizontal alignment within the box |
421
+ | `text-decoration` | `underline`, `line-through`, `none` | Both may combine: `underline line-through` |
422
+ | `text-overflow` | `ellipsis`, `clip` | Truncates overflowing single-line text with `...` |
423
+ | `text-transform` | `uppercase`, `lowercase`, `capitalize`, `none` | Applied at transpile time |
424
+ | `line-height` | `1.5`, `150%`, `24px` | Line advance for wrapped text; `normal` = font default |
425
+ | `letter-spacing` | `2px`, `-1px` | Per-character advance adjustment |
426
+ | `white-space` | `normal`, `nowrap`, `pre`, `pre-line` | Controls word-wrap behavior |
427
+ | `font-weight` | `normal`, `bold`, `400`, `700` | Selects the matching `@font-face` variant when available |
428
+ | `font-style` | `normal`, `italic`, `oblique` | Selects the matching `@font-face` variant when available |
429
+ | `font-smoothing` | `antialiased`, `none` | Overrides display-level text antialiasing |
430
+ | `font-subset` | `exact`, `fallback` | Controls generated-font glyph selection |
431
+
432
+ ### Fonts
433
+
434
+ Local TTF/OTF fonts can be referenced with `@font-face`. The transpiler does
435
+ not copy the whole font to the board. It reads the font at build time, rasterizes
436
+ only the glyphs needed by the UI, packs them as 4-bit alpha bitmap data, and
437
+ emits those tables into the firmware. On ESP32-class targets those generated
438
+ tables are `static const` data in flash/rodata; the original TTF/OTF file is not
439
+ held in RAM on the hardware.
440
+
441
+ #### Install a font in a project
442
+
443
+ Put font files somewhere inside the project, usually next to the `.ui.css` file
444
+ or under a local `fonts/` folder:
445
+
446
+ ```text
447
+ src/
448
+ app.ui.html
449
+ app.ui.css
450
+ fonts/
451
+ DeviceSans-Regular.ttf
452
+ DeviceSans-Bold.ttf
453
+ ```
454
+
455
+ Reference them from CSS with paths relative to the `.ui.css` file:
456
+
457
+ ```css
458
+ @font-face {
459
+ font-family: "DeviceSans";
460
+ src: url("fonts/DeviceSans.ttf");
461
+ }
462
+
463
+ #title {
464
+ font-family: "DeviceSans";
465
+ font-size: 24px;
466
+ font-smoothing: antialiased;
467
+ }
468
+ ```
469
+
470
+ Remote font URLs are not supported for embedded builds. Use local files so the
471
+ build is reproducible and does not depend on network access.
472
+
473
+ #### Declare variants
474
+
475
+ Declare each weight/style variant as its own `@font-face`. The UI compiler
476
+ chooses the closest matching variant for each node based on `font-family`,
477
+ `font-weight`, `font-style`, and `font-size`.
478
+
479
+ ```css
480
+ @font-face {
481
+ font-family: "DeviceSans";
482
+ src: url("fonts/DeviceSans-Regular.ttf");
483
+ font-weight: 400;
484
+ font-style: normal;
485
+ }
486
+
487
+ @font-face {
488
+ font-family: "DeviceSans";
489
+ src: url("fonts/DeviceSans-Bold.ttf");
490
+ font-weight: 700;
491
+ font-style: normal;
492
+ }
493
+
494
+ @font-face {
495
+ font-family: "DeviceSans";
496
+ src: url("fonts/DeviceSans-Italic.ttf");
497
+ font-weight: 400;
498
+ font-style: italic;
499
+ }
500
+
501
+ #title {
502
+ font-family: "DeviceSans";
503
+ font-size: 24px;
504
+ font-weight: bold;
505
+ }
506
+ ```
507
+
508
+ Every distinct `font-family` + resolved font file + `font-size` + variant becomes
509
+ one generated font asset. Reusing the same face and size across many nodes
510
+ shares one asset. Using the same face at `16px` and `24px` creates two assets
511
+ because each size is rasterized separately.
512
+
513
+ #### Exact subsetting and icon fonts
514
+
515
+ By default, generated fonts use `font-subset: exact`. Only the literal
516
+ characters found in static UI text, placeholders, and option labels are encoded.
517
+ This is useful for icon fonts and symbol fonts:
518
+
519
+ ```html
520
+ <text id="wifiIcon">✓</text>
521
+ ```
522
+
523
+ ```css
524
+ @font-face {
525
+ font-family: "DeviceIcons";
526
+ src: url("fonts/device-icons.ttf");
527
+ }
528
+
529
+ #wifiIcon {
530
+ font-family: "DeviceIcons";
531
+ font-size: 20px;
532
+ font-subset: exact;
533
+ }
534
+ ```
535
+
536
+ In this case, only the checkmark glyph is emitted for that font/size, not the
537
+ whole icon font and not the common ASCII set.
538
+
539
+ Generated font glyph lookup supports UTF-8 text for codepoints in the Basic
540
+ Multilingual Plane (`U+0000` to `U+FFFF`). Many icon fonts use Private Use Area
541
+ codepoints such as `U+E000`; those are supported. Emoji and other characters
542
+ above `U+FFFF` are not currently supported by the generated-font runtime.
543
+
544
+ Wingdings-style fonts can work, but be careful: some older symbol fonts use
545
+ legacy character mappings rather than standard Unicode symbols. Copy the exact
546
+ character/codepoint that the font maps to the glyph you want, or prefer a
547
+ Unicode icon font when possible.
548
+
549
+ #### Dynamic text and fallback glyphs
550
+
551
+ Exact subsetting can only see text known at build time. If a generated font is
552
+ used on a node whose text changes at runtime, include a fallback character set:
553
+
554
+ ```css
555
+ #counter {
556
+ font-family: "DeviceSans";
557
+ font-size: 18px;
558
+ font-subset: fallback;
559
+ }
560
+ ```
561
+
562
+ `font-subset: fallback` includes the static text plus a small common ASCII set
563
+ containing digits, letters, spaces, and punctuation. Use it for counters,
564
+ formatted numeric values, input fields, or any generated-font text binding that
565
+ can produce characters not present in the initial HTML.
566
+
567
+ If the node uses the built-in bitmap font, `font-subset` has no effect.
568
+
569
+ #### Smoothing
570
+
571
+ Generated TTF/OTF glyphs are rasterized as alpha masks. Use
572
+ `font-smoothing: antialiased` to blend edge pixels for smoother text on RGB
573
+ displays. Use `font-smoothing: none` to threshold the same glyph masks for a
574
+ sharper, more pixel-like look. On monochrome displays smoothing is disabled.
575
+
576
+ ```css
577
+ .smooth {
578
+ font-family: "DeviceSans";
579
+ font-size: 18px;
580
+ font-smoothing: antialiased;
581
+ }
582
+
583
+ .sharp {
584
+ font-family: "DeviceSans";
585
+ font-size: 18px;
586
+ font-smoothing: none;
587
+ }
588
+ ```
589
+
590
+ #### Converting fonts
591
+
592
+ Use TTF or OTF files when possible. WOFF/WOFF2 web fonts should be converted to
593
+ TTF/OTF before use.
594
+
595
+ Common conversion options:
596
+
597
+ - FontForge GUI: open the source font, then use `File -> Generate Fonts...` and
598
+ choose TrueType (`.ttf`) or OpenType (`.otf`).
599
+ - FontForge CLI:
600
+
601
+ ```bash
602
+ fontforge -lang=ff -c 'Open($1); Generate($2)' input.otf output.ttf
603
+ ```
604
+
605
+ - WOFF2 tools: use `woff2_decompress input.woff2` to produce a TTF-flavored
606
+ font when the source is a WOFF2 web font.
607
+ - fonttools can inspect and subset fonts:
608
+
609
+ ```bash
610
+ python -m pip install fonttools brotli
611
+ pyftsubset DeviceSans.ttf --text="ABC123" --unicodes=U+2713 --output-file=DeviceSans-subset.ttf
612
+ ```
613
+
614
+ Manual external subsetting is optional. TypeCAD already subsets the emitted
615
+ hardware glyphs. External subsetting is mainly useful when you need to distribute
616
+ a smaller source font file, remove unused font tables for licensing reasons, or
617
+ speed up build-time parsing of a very large font.
618
+
619
+ #### Licensing
620
+
621
+ Do not assume system fonts are redistributable. Fonts such as commercial OS
622
+ fonts may be licensed for local use but not for checking into a repository or
623
+ shipping in a firmware project. Prefer open-licensed fonts, or keep proprietary
624
+ fonts outside shared source control if your license requires it.
625
+
626
+ #### Visual
627
+ | Property | Values | Notes |
628
+ |---|---|---|
629
+ | `background` / `background-color` | any color | Fill color |
630
+ | `border` (shorthand) | `2px solid #808080` | Splits into width/style/color |
631
+ | `border-width` | `2px` | |
632
+ | `border-color` | any color | |
633
+ | `border-style` | `solid`, `dashed`, `none` | Dashed approximated with segments |
634
+ | `border-radius` | `4px` | Rounded fill/border on hardware; preview approximates |
635
+ | `outline` | `1px solid #fff`, `2px dashed red` | Drawn outside the element box |
636
+ | `visibility` | `visible`, `hidden` | Hidden elements are not drawn |
637
+ | `box-shadow` | `inset 0 1px 0 #fff`, `0 10px 0 #333` | Up to 4 rect shadows; approximated for TFT drawing |
638
+ | `transform` | `translateY(10px)`, `translate(0, 10px)` | Draw-time translate offset; no flex relayout |
639
+ | `opacity` | parsed | (Blending not supported without PSRAM framebuffer) |
640
+
641
+ #### Transitions
642
+ | Property | Values | Notes |
643
+ |---|---|---|
644
+ | `transition` | `background 300ms`, `color 120ms` | Lerps the property over the duration |
645
+ | `:pressed` | pseudo-class | Applied when `.value` is 1 (button press) |
646
+
647
+ Pressed rules may also include `top` / `left` / `right` / `bottom` or
648
+ `transform: translate(...)`. These are applied as draw-time offsets so the
649
+ element face/content can move visually without recomputing the flex layout;
650
+ outset shadows stay anchored, which is useful for raised button effects.
651
+
652
+ Small dirty paint regions for text, backgrounds, borders, outlines, shadows,
653
+ and draw-time translate offsets are composed in an offscreen RGB565 canvas and
654
+ pushed as one rectangle when memory allows. Larger regions fall back to direct
655
+ drawing.
656
+
657
+ ### Selectors
658
+ - Element: `screen { ... }`
659
+ - ID: `#title { ... }`
660
+ - Class: `.card { ... }`
661
+ - Compound: `.card.active { ... }`, `button.primary { ... }`
662
+ - Descendant: `view text { ... }`
663
+ - Child: `view > text { ... }` (direct children only)
664
+ - Adjacent sibling: `.first + .second { ... }` (immediate next sibling)
665
+ - General sibling: `.first ~ .later { ... }` (any following sibling)
666
+ - Attribute: `[disabled]`, `[type="number"]` (presence and exact-value match)
667
+ - Negation: `button:not(.disabled)`, `.a:not(.b.c)` (compound `:not()` supported)
668
+ - Pseudo-state: `#btn:pressed`, `input:disabled`, `check:checked`, `*:focus`
669
+ - Inline style: `<text style="color: red">hi</text>`
670
+ - `<style>` blocks embedded in the `.ui.html`
671
+
672
+ Pseudo-states match runtime element state: `:pressed` (button held), `:disabled`
673
+ (`disabled` attribute), `:checked` (`<check>`/`<radio>` with `.value` 1), and
674
+ `:focus` (the node currently receiving input).
675
+
676
+ ### CSS variables
677
+
678
+ Define variables in `:root` and reference them with `var()`:
679
+
680
+ ```css
681
+ :root {
682
+ --bg: #0a0a0a;
683
+ --fg: #fafafa;
684
+ --primary: #7c3aed;
685
+ }
686
+ screen { background: var(--bg); }
687
+ #title { color: var(--fg); }
688
+ ```
689
+
690
+ Variables resolve at transpile time — no runtime cost.
691
+
692
+ #### Class-scoped variables (themes)
693
+
694
+ Variables can also be defined under a class selector (e.g. `.dark`) and selected
695
+ at build time via the `themeClass` config option. This is how shadcn-style
696
+ light/dark themes work:
697
+
698
+ ```css
699
+ :root { --bg: #ffffff; --fg: #0a0a0a; }
700
+ .dark { --bg: #0a0a0a; --fg: #fafafa; }
701
+ screen { background: var(--bg); color: var(--fg); }
702
+ ```
703
+
704
+ ```typescript
705
+ // cuttlefish.config.ts — or the display config in ui.mount
706
+ display: {
707
+ themeClass: 'dark', // resolves var(--x) using the .dark overrides
708
+ }
709
+ ```
710
+
711
+ When `themeClass` is set, `var()` substitution prefers that class's variables
712
+ over `:root`. This is **transpile-time selection** — one theme per firmware
713
+ build (there is no runtime theme switch on a fixed-screen device).
714
+
715
+ #### `@media` (compile-time variant selection)
716
+
717
+ `@media` rules are evaluated against the resolved display profile at transpile
718
+ time. Since each build targets one fixed screen size, this acts as a
719
+ compile-time variant selector, not responsive design:
720
+
721
+ ```css
722
+ /* Applied only when the display is ≤ 240px wide */
723
+ @media (max-width: 240px) {
724
+ #title { font-size: 12px; }
725
+ }
726
+ ```
727
+
728
+ Supported conditions: `min-width`, `max-width`, `min-height`, `max-height`
729
+ (in `px`). Unsupported conditions (e.g. `orientation`) emit a warning and the
730
+ rule is skipped. `@import` and `@supports` are not supported (warned + skipped).
731
+
732
+ ### Theming
733
+
734
+ Themes are **compile-time**. There are two complementary mechanisms:
735
+
736
+ **1. Theme file (`themeCss`)** — swap the entire CSS file:
737
+
738
+ ```typescript
739
+ // cuttlefish.config.ts
740
+ display: {
741
+ themeCss: './src/hello.dark.css', // relative to .ui.html dir
742
+ // or: themeCss: '/absolute/path/to/theme.css',
743
+ }
744
+ ```
745
+
746
+ When `themeCss` is set, that file replaces the default sibling `.ui.css`. Use CSS variables to define a palette once, then swap the variable file for different themes:
747
+
748
+ ```
749
+ src/
750
+ hello.ui.html ← layout (shared)
751
+ hello.ui.css ← default theme (no themeCss set)
752
+ hello.dark.css ← dark theme
753
+ hello.shadcn.css ← shadcn palette
754
+ ```
755
+
756
+ **2. Theme class (`themeClass`)** — select a class-scoped variable block
757
+ within a single CSS file (see [Class-scoped variables](#class-scoped-variables-themes) above):
758
+
759
+ ```typescript
760
+ display: {
761
+ themeClass: 'dark', // resolves var(--x) from .dark { ... } overrides
762
+ }
763
+ ```
764
+
765
+ The two can be combined: `themeCss` picks the file, `themeClass` picks the
766
+ variable scope within it.
767
+
768
+ The `.ui.html` file defines the structure (elements, IDs, layout); the CSS file defines the appearance (colors, fonts, borders, shadows). Swap either in config without touching the HTML.
769
+
770
+ ### Unsupported (and why)
771
+ - `display: grid` — needs a GridLayoutEngine
772
+ - Full inline rich text — basic wrapping, `line-height`, `white-space`, and `<br>` are supported; mixed inline spans are not
773
+ - `background-image` / sprites — use `<img>` for embedded images; CSS `background: url(...)` is unsupported (only solid colors and `linear-gradient`)
774
+ - `position: fixed` — viewport-fixed positioning is not implemented
775
+ - `:after` / `:before` pseudo-elements — no generated content
776
+ - `text-shadow` on built-in font — needs sub-pixel font data (works with custom fonts)
777
+ - Per-corner `border-radius` — only a uniform radius is supported (Adafruit_GFX draws one corner value)
778
+ - Runtime theme switching — themes are compile-time only (one `themeClass` per build; swap in config and rebuild)
779
+
780
+ ## State and interaction
781
+
782
+ ### The `.value` property
783
+
784
+ Every interactive element has a `.value` property — a number that is both readable and writable:
785
+
786
+ ```typescript
787
+ // Read
788
+ const count = screen.counter.value;
789
+
790
+ // Write (updates the display immediately)
791
+ screen.counter.value = 42;
792
+ ```
793
+
794
+ ### Pin input
795
+
796
+ ```typescript
797
+ // Watch a pin for falling edges — runs in the frame loop (no ISR)
798
+ ui.watchPin(4, () => {
799
+ screen.counter.value = screen.counter.value + 1;
800
+ });
801
+
802
+ // Toggle an element's .value on pin press (0 ↔ 1)
803
+ screen.ledBox.onToggle(5);
804
+
805
+ // Cycle through options (0 → 1 → 2 → 0 → ...)
806
+ screen.modeSelect.onChange(15, 3);
807
+ ```
808
+
809
+ ### Reactive bindings
810
+
811
+ Bindings compute a display property from `.value` or signals each frame:
812
+
813
+ ```typescript
814
+ // Color binding
815
+ ui.bind(screen.counter, 'color', () =>
816
+ (screen.counter.value % 2 === 0 ? 'limegreen' : 'orange')
817
+ );
818
+
819
+ // Background binding
820
+ ui.bind(screen.btn, 'background', () =>
821
+ (screen.btn.value > 0 ? 'limegreen' : 'darkgreen')
822
+ );
823
+
824
+ // Border color binding
825
+ ui.bind(screen.ledBox, 'borderColor', () =>
826
+ (screen.ledBox.value ? 'limegreen' : '#808080')
827
+ );
828
+
829
+ // Value binding — drive a progress/range node live from loop()
830
+ ui.bind(screen.progress, 'value', () => sensorPercent);
831
+
832
+ // Visibility binding - preallocate both branches and toggle which one draws
833
+ ui.bind(screen.enteredBranch, 'visible', () => screen.input.value > 0);
834
+ ui.bind(screen.emptyBranch, 'visible', () => screen.input.value === 0);
835
+
836
+ // Text binding — number to string
837
+ ui.bind(screen.counter, 'text', () => String(screen.counter.value));
838
+
839
+ // Text binding — ternary chain (for selectors)
840
+ ui.bind(screen.modeValue, 'text', () => (
841
+ screen.modeValue.value === 0 ? 'Auto' :
842
+ screen.modeValue.value === 1 ? 'Manual' : 'Off'
843
+ ));
844
+ ```
845
+
846
+ `visible` bindings are for fixed-layout conditional rendering. The nodes stay in
847
+ the retained UI tree; hidden branches are skipped for drawing and hit testing,
848
+ and shown branches repaint their subtree.
849
+
850
+ ### Two-way input binding
851
+
852
+ When the user types into an `<input>` via the on-screen keyboard, push the text
853
+ back into app state with `ui.bindInput`:
854
+
855
+ ```typescript
856
+ let ssid = '';
857
+ ui.bindInput(screen.ssid, (text) => { ssid = text; });
858
+ ```
859
+
860
+ The callback fires whenever the input's text changes (after the keyboard commits).
861
+
862
+ ### Slider change callbacks
863
+
864
+ A `<range>` fires `onChange` on every value change while dragging — read
865
+ `.value` inside the callback for the new value:
866
+
867
+ ```typescript
868
+ screen.brightness.onChange(() => {
869
+ // Fires continuously during the drag.
870
+ ledPwm = screen.brightness.value;
871
+ });
872
+ ```
873
+
874
+ ### Data-bound lists
875
+
876
+ `<list>` is a virtualized, callback-driven list — it renders only the visible
877
+ items to a dedicated scroll canvas, so a thousand-item list has the same memory
878
+ footprint as a ten-item one. Bind it with `ui.bindList`:
879
+
880
+ ```html
881
+ <list id="networks" item-height="28px"></list>
882
+ ```
883
+
884
+ ```typescript
885
+ ui.bindList(
886
+ screen.networks,
887
+ () => scanResults.length, // count
888
+ (i) => `${scanResults[i].ssid} (${scanResults[i].rssi} dBm)`, // item text
889
+ (i) => { connectTo(scanResults[i].ssid); }, // optional tap handler
890
+ );
891
+ ```
892
+
893
+ The count function re-evaluates each frame; if it changes, the list recomputes
894
+ its content height and repaints. Drag to scroll; tap an item to fire the
895
+ optional third callback.
896
+
897
+ ### User-drawn canvas (`<canvas>`)
898
+
899
+ `<canvas>` is an element whose contents you draw yourself, every frame, using
900
+ the display graphics primitives. It follows all CSS rules (layout, borders,
901
+ transforms, z-index) like any other element, but its pixels come from your
902
+ callback. Use it for sparkline graphs, analog gauges, or custom-shaped controls.
903
+
904
+ ```html
905
+ <canvas id="spark" width="120" height="40"></canvas>
906
+ ```
907
+
908
+ `width`/`height` set the **drawing buffer** size (px). The CSS box is the
909
+ **layout** size — size them to match unless you want clipping.
910
+
911
+ ```typescript
912
+ ui.drawCanvas(screen.spark, (ctx) => {
913
+ ctx.fillScreen('black');
914
+ ctx.line(0, 30, ctx.width, 30, 'limegreen'); // baseline
915
+ ctx.rect(2, 2, ctx.width - 4, ctx.height - 4, '#333');
916
+ ctx.fillCircle(needleX, 30, 3, 'red');
917
+ ctx.text(4, 12, `${temp}°`, 'white'); // optional color arg
918
+ });
919
+ ```
920
+
921
+ Coordinates are **canvas-relative** (`(0,0)` = element top-left) and drawing is
922
+ **auto-clipped** to the buffer — you cannot accidentally paint over neighbors.
923
+ Color arguments are CSS color strings resolved to RGB565 at build time.
924
+
925
+ The callback runs **every frame**; to animate, mutate state in a `setInterval`
926
+ or signal and the canvas picks it up next frame. Taps hit-test as the full CSS
927
+ box, so `screen.spark.onClick(...)` works for interactive canvases.
928
+
929
+ #### `ctx` methods (the display graphics primitives)
930
+
931
+ | Method | Notes |
932
+ |---|---|
933
+ | `ctx.fillRect(x,y,w,h,color)` / `ctx.rect(...)` | Filled / outline rectangle |
934
+ | `ctx.fillRoundRect(x,y,w,h,r,color)` / `ctx.roundRect(...)` | Rounded variant |
935
+ | `ctx.line(x0,y0,x1,y1,color)` | Arbitrary line |
936
+ | `ctx.hline(x,y,w,color)` / `ctx.vline(x,y,h,color)` | Fast horizontal / vertical line |
937
+ | `ctx.fillCircle(x,y,r,color)` / `ctx.circle(...)` | Filled / outline circle |
938
+ | `ctx.drawPixel(x,y,color)` | Single pixel |
939
+ | `ctx.text(x,y,str,color?)` | Bitmap text (built-in font) |
940
+ | `ctx.fillScreen(color)` | Clear the whole buffer |
941
+ | `ctx.width` / `ctx.height` | Read-only buffer dimensions |
942
+
943
+ ### Signals
944
+
945
+ For reactive state not tied to an element:
946
+
947
+ ```typescript
948
+ const temperature = ui.signal(22);
949
+
950
+ // Read
951
+ const t = temperature();
952
+
953
+ // Write
954
+ temperature.set(25);
955
+ ```
956
+
957
+ `ui.signal()` accepts `number`, `string`, or `boolean` literals — these lower to
958
+ `int`/`double`, `const char*`, and `bool` on the device. Other initializers
959
+ (objects, arrays, `null`, identifiers) are rejected at type-check time
960
+ (`Signal<T extends SignalValue>`) and at build time with a `ui-signal-initializer`
961
+ warning that defaults the signal to `0` (`int`).
962
+
963
+ ## Element id errors
964
+
965
+ If a binding or event handler references an element id that doesn't exist in the
966
+ screen, the build fails with a `ui-unknown-element` error rather than silently
967
+ re-targeting the wrong node. This applies to every call that takes a screen
968
+ element:
969
+
970
+ ```typescript
971
+ ui.bind(screen.typo, 'color', ...) // ✗ error: element "typo" not found
972
+ ui.bindInput(screen.typo, ...) // ✗ error
973
+ ui.bindList(screen.typo, ...) // ✗ error
974
+ ui.drawCanvas(screen.typo, ...) // ✗ error
975
+ await ui.onTap(screen.typo) // ✗ error (no silent fallback to any-tap)
976
+ screen.typo.onClick(...) // ✗ error
977
+ screen.typo.onToggle(...) // ✗ error
978
+ ```
979
+
980
+ Fix the typo in your `.ui.html` / `.ui` file's `id` attribute and rebuild.
981
+
982
+ ## Composing custom elements
983
+
984
+ Don't see the element you need? Build it from `<view>` + `<text>` + bindings:
985
+
986
+ ### Custom checkbox
987
+
988
+ ```html
989
+ <view id="ledRow">
990
+ <view id="ledBox"></view>
991
+ <text id="ledLabel">Enable LED</text>
992
+ </view>
993
+ ```
994
+ ```css
995
+ #ledBox {
996
+ width: 16px;
997
+ height: 16px;
998
+ border: 2px solid #808080;
999
+ }
1000
+ ```
1001
+ ```typescript
1002
+ screen.ledBox.onToggle(5); // toggles .value 0↔1
1003
+
1004
+ ui.bind(screen.ledBox, 'background', () =>
1005
+ (screen.ledBox.value ? 'limegreen' : 'transparent')
1006
+ );
1007
+ ui.bind(screen.ledBox, 'borderColor', () =>
1008
+ (screen.ledBox.value ? 'limegreen' : '#808080')
1009
+ );
1010
+ ```
1011
+
1012
+ ### Custom selector
1013
+
1014
+ ```html
1015
+ <view id="modeRow">
1016
+ <text id="modeLabel">Mode:</text>
1017
+ <text id="modeValue">Auto</text>
1018
+ </view>
1019
+ ```
1020
+ ```typescript
1021
+ screen.modeValue.onChange(15, 3); // cycles 0→1→2→0
1022
+
1023
+ ui.bind(screen.modeValue, 'text', () => (
1024
+ screen.modeValue.value === 0 ? 'Auto' :
1025
+ screen.modeValue.value === 1 ? 'Manual' : 'Off'
1026
+ ));
1027
+ ```
1028
+
1029
+ ### Custom progress bar
1030
+
1031
+ ```html
1032
+ <view id="barContainer">
1033
+ <view id="barFill"></view>
1034
+ </view>
1035
+ ```
1036
+ ```css
1037
+ #barContainer { width: 200px; height: 20px; border: 1px solid #808080; }
1038
+ #barFill { background: limegreen; height: 100%; }
1039
+ ```
1040
+ ```typescript
1041
+ ui.bind(screen.barFill, 'background', () =>
1042
+ (screen.barFill.value > 50 ? 'limegreen' : 'orange')
1043
+ );
1044
+ ```
1045
+
1046
+ ## Timers
1047
+
1048
+ ```typescript
1049
+ // Auto-update every 2 seconds
1050
+ setInterval(() => {
1051
+ screen.counter.value = screen.counter.value + 1;
1052
+ }, 2000);
1053
+ ```
1054
+
1055
+ ## Architecture
1056
+
1057
+ ```
1058
+ .ui.html / .ui.css → parse (linkedom + css-tree) → resolve styles
1059
+
1060
+ layout (Yoga flexbox)
1061
+
1062
+ lower to C++ UINode[] table
1063
+
1064
+ ui_mount → display.init (Adafruit_ILI9341)
1065
+ ui_tick → poll inputs → eval bindings → transitions → draw
1066
+ ui_init → mark all dirty for first frame
1067
+ ```
1068
+
1069
+ The runtime is a retained-mode tree: the HTML/CSS is fully resolved at transpile time. The device only sees static tables + a tiny draw loop. No DOM, no CSS engine, no HTML parser on the MCU.
1070
+
1071
+ ### Rendering & performance
1072
+
1073
+ Each frame, `ui_tick` re-evaluates bindings, advances transitions/animations,
1074
+ and redraws only the nodes marked dirty (most frames touch a handful of nodes,
1075
+ not the whole screen). Dirty paint regions are composed in an offscreen RGB565
1076
+ canvas and pushed as one rectangle when memory allows.
1077
+
1078
+ **Framebuffer (PSRAM-gated).** When the board has PSRAM (`BOARD_HAS_PSRAM`
1079
+ defined + `psramFound()`), the runtime allocates a full-screen `GFXcanvas16`
1080
+ framebuffer and renders the entire dirty-node pass into it, then pushes once
1081
+ via a single SPI transaction. This eliminates the per-primitive transaction
1082
+ storm that otherwise limits redraw rate on ILI9341 over SPI. Without PSRAM the
1083
+ runtime falls back to direct per-node drawing (no behavior change). The
1084
+ framebuffer activates automatically — no config needed beyond enabling PSRAM in
1085
+ the Arduino build flags.
1086
+
1087
+ ### Diagnostics (warnings)
1088
+
1089
+ Unknown HTML tags and unknown CSS properties are **reported as warnings**, not
1090
+ silently dropped. They appear in the build output (yellow, to stderr) and do
1091
+ not abort the build:
1092
+
1093
+ ```
1094
+ ⚠ Unknown CSS property "bogus-prop" — ignored.
1095
+ ⚠ Unknown HTML tag <marquee> — ignored.
1096
+ ⚠ Unsupported @media (orientation: portrait) has an unsupported condition — rule ignored.
1097
+ ```
1098
+
1099
+ This surfaces typos and unsupported features early instead of leaving styles
1100
+ mysteriously unapplied.
1101
+
1102
+ ## Display profiles
1103
+
1104
+ The display hardware is described in `cuttlefish.config.ts` under the `display` field. This drives all transpile-time decisions: dimensions, color format, rotation, SPI pins, backlight, and touch.
1105
+
1106
+ ### Config reference
1107
+
1108
+ ```typescript
1109
+ // cuttlefish.config.ts
1110
+ display: {
1111
+ // Either reference a built-in profile by name:
1112
+ profile: 'ili9341-spi',
1113
+
1114
+ // Or inline everything:
1115
+ // driver: 'ili9341',
1116
+ // width: 320, height: 240,
1117
+ // colorFormat: 'rgb565',
1118
+ // rotation: 1,
1119
+
1120
+ // Wiring (always project-specific)
1121
+ cs: 5,
1122
+ dc: 21,
1123
+ rst: 22,
1124
+ backlight: 17, // optional — pin number for backlight
1125
+
1126
+ // Antialiasing (optional)
1127
+ antialias: true, // smooths shapes and text; text can opt out with font-smoothing:none
1128
+
1129
+ // Theming (optional — compile-time)
1130
+ themeCss: './src/hello.dark.css', // override the sibling .ui.css file
1131
+ themeClass: 'dark', // select a class-scoped variable block (.dark { ... })
1132
+
1133
+ // Touch (optional)
1134
+ touch: {
1135
+ library: 'XPT2046_Touchscreen',
1136
+ cs: 14, // touch controller CS pin
1137
+ irq: 2, // optional — interrupt pin
1138
+ calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
1139
+ minPressure: 10,
1140
+ },
1141
+ }
1142
+ ```
1143
+
1144
+ ### Profile fields
1145
+
1146
+ | Field | Type | Description |
1147
+ |---|---|---|
1148
+ | `profile` | string | Built-in profile name (e.g. `"ili9341-spi"`) |
1149
+ | `driver` | string | Display driver id (e.g. `"ili9341"`) |
1150
+ | `width` | number | Display width in pixels (after rotation) |
1151
+ | `height` | number | Display height in pixels (after rotation) |
1152
+ | `colorFormat` | `"rgb565"` \| `"mono"` | Color depth |
1153
+ | `rotation` | number | 0=portrait, 1=landscape, 2-3=inverted |
1154
+ | `backlight` | number | Backlight pin (optional) |
1155
+ | `cs` / `dc` / `rst` | number | Display wiring pins |
1156
+
1157
+ ### Built-in profiles
1158
+
1159
+ | Name | Display | Dimensions | Color | Touch |
1160
+ |---|---|---|---|---|
1161
+ | `ili9341-spi` | ILI9341 (SPI) | 320×240 | RGB565 | Add via `touch` config |
1162
+ | `ssd1309-i2c` | SSD1309 OLED (I2C) | 128×64 | Mono | None |
1163
+
1164
+ ### Adding a new display
1165
+
1166
+ Adding a new display driver requires two parts: a **display profile** (the hardware config) and a **display adapter** (the generated C++ code that drives it).
1167
+
1168
+ #### 1. Register a display adapter
1169
+
1170
+ A display adapter is a TypeScript function that generates C++ code for a specific driver. Register it in a module that runs before the build:
1171
+
1172
+ ```typescript
1173
+ // my-project/display-adapters.ts
1174
+ import { registerDisplayAdapter } from '@typecad/cuttlefish/api/shared/display-adapter';
1175
+
1176
+ registerDisplayAdapter('ssd1306', (display) => {
1177
+ return {
1178
+ includes: `#include <Adafruit_GFX.h>\n#include <Adafruit_SSD1306.h>\n#include <Wire.h>`,
1179
+ declaration: `Adafruit_SSD1306 __tc_display(128, 64, &Wire, -1);`,
1180
+ functions: [
1181
+ 'static int16_t __addrX = 0, __addrY = 0, __addrW = 0, __addrH = 0;',
1182
+ 'static uint32_t __addrCursor = 0;',
1183
+ 'static inline void display_init() {',
1184
+ ' __tc_display.begin(SSD1306_SWITCHCAPVCC, 0x3C);',
1185
+ ' __tc_display.clearDisplay();',
1186
+ ' __tc_display.display();',
1187
+ '}',
1188
+ 'static inline void display_fillScreen(uint16_t color) {',
1189
+ ' __tc_display.fillScreen(color ? 1 : 0);',
1190
+ '}',
1191
+ 'static inline void display_startWrite() { }',
1192
+ 'static inline void display_endWrite() { }',
1193
+ 'static inline void display_setAddrWindow(int16_t x, int16_t y, int16_t w, int16_t h) {',
1194
+ ' __addrX = x; __addrY = y; __addrW = w; __addrH = h; __addrCursor = 0;',
1195
+ '}',
1196
+ 'static inline void display_writePixels(uint16_t* pixels, uint32_t count) {',
1197
+ ' if (__addrW <= 0 || __addrH <= 0) return;',
1198
+ ' uint32_t total = (uint32_t)__addrW * (uint32_t)__addrH;',
1199
+ ' for (uint32_t i = 0; i < count; i++) {',
1200
+ ' if (__addrCursor >= total) break;',
1201
+ ' uint32_t pos = __addrCursor++;',
1202
+ ' __tc_display.drawPixel(__addrX + (pos % __addrW), __addrY + (pos / __addrW), pixels[i] ? 1 : 0);',
1203
+ ' }',
1204
+ '}',
1205
+ 'static inline void display_partial_refresh(int16_t x, int16_t y, int16_t w, int16_t h) {',
1206
+ ' (void)x; (void)y; (void)w; (void)h;',
1207
+ ' __tc_display.display();',
1208
+ '}',
1209
+ ].join('\\n'),
1210
+ };
1211
+ });
1212
+ ```
1213
+
1214
+ The runtime calls these core display functions:
1215
+
1216
+ | Function | Purpose |
1217
+ |----------|---------|
1218
+ | `display_init()` | Initialize the display (begin, rotation, clear) |
1219
+ | `display_fillScreen(color)` | Fill the entire screen with a color |
1220
+ | `display_startWrite()` | Begin an SPI transaction (no-op for I2C) |
1221
+ | `display_endWrite()` | End a transaction (often a no-op for page-buffered I2C) |
1222
+ | `display_setAddrWindow(x, y, w, h)` | Set the active write region |
1223
+ | `display_writePixels(pixels, count)` | Write pixels into the active region |
1224
+ | `display_partial_refresh(x, y, w, h)` | Publish the dirty region on deferred displays |
1225
+
1226
+ For monochrome displays, the adapter wraps each color argument with a conversion function. For deferred displays, the current runtime publishes from `display_partial_refresh()`.
1227
+
1228
+ #### 2. Create a display profile
1229
+
1230
+ ```typescript
1231
+ // displays/my-display.ts
1232
+ import type { DisplayProfile } from '@typecad/cuttlefish/api/shared';
1233
+
1234
+ export const MY_DISPLAY: DisplayProfile = {
1235
+ driver: 'ssd1306', // must match the adapter name
1236
+ width: 128,
1237
+ height: 64,
1238
+ colorFormat: 'mono', // 'rgb565' or 'mono'
1239
+ rotation: 0,
1240
+ };
1241
+ ```
1242
+
1243
+ #### 3. Reference it in config
1244
+
1245
+ ```typescript
1246
+ display: {
1247
+ driver: 'ssd1306',
1248
+ width: 128, height: 64,
1249
+ colorFormat: 'mono',
1250
+ rotation: 0,
1251
+ }
1252
+ ```
1253
+
1254
+ #### 4. Import the adapter module before building
1255
+
1256
+ Make sure your adapter module is imported (side-effect import) so the registration runs:
1257
+
1258
+ ```typescript
1259
+ // cuttlefish.config.ts or main.ts
1260
+ import './display-adapters'; // registers the 'ssd1306' adapter
1261
+ ```
1262
+
1263
+ #### Built-in adapters
1264
+
1265
+ | Driver | Display | Color | Notes |
1266
+ |--------|---------|-------|-------|
1267
+ | `ili9341` | ILI9341 (320×240) | RGB565 | Default, hardware SPI |
1268
+
1269
+ To add more built-in adapters, contribute a file to `packages/framework-arduino/src/graphics/` and register it.
1270
+
1271
+ ## Touch input
1272
+
1273
+ Touch is configured via the `touch` field in the display profile. The system uses an adapter pattern: built-in libraries generate C++ automatically; custom libraries use a TypeScript adapter file.
1274
+
1275
+ ### Built-in touch libraries
1276
+
1277
+ | Library | Controllers | Interface | Config |
1278
+ |---|---|---|---|
1279
+ | `XPT2046_Touchscreen` | XPT2046 (common ILI9341 shields) | SPI (shared with display) | `{ library, cs, irq? }` |
1280
+ | `Adafruit_TouchScreen` | Resistive 4-wire | Analog (no SPI) | `{ library, analogPins: { xp, yp, xm, ym, rx } }` |
1281
+ | `Adafruit_STMPE610` | STMPE610 (capacitive) | SPI or I2C | `{ library, cs }` |
1282
+
1283
+ ### Config examples
1284
+
1285
+ **XPT2046 (most common with ILI9341 TFT shields):**
1286
+
1287
+ ```typescript
1288
+ display: {
1289
+ profile: 'ili9341-spi',
1290
+ cs: 5, dc: 21, rst: 22,
1291
+ touch: {
1292
+ library: 'XPT2046_Touchscreen',
1293
+ cs: 14, // touch CS pin (separate from display CS)
1294
+ irq: 2, // optional
1295
+ calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
1296
+ minPressure: 10,
1297
+ },
1298
+ }
1299
+ ```
1300
+
1301
+ **Adafruit resistive 4-wire:**
1302
+
1303
+ ```typescript
1304
+ touch: {
1305
+ library: 'Adafruit_TouchScreen',
1306
+ analogPins: { xp: 'A3', yp: 'A2', xm: 8, ym: 9, rx: 300 },
1307
+ calibration: { xMin: 100, xMax: 900, yMin: 100, yMax: 900 },
1308
+ minPressure: 10,
1309
+ }
1310
+ ```
1311
+
1312
+ ### Calibration
1313
+
1314
+ Calibration maps the touch controller's raw ADC values to display pixel coordinates. To calibrate your panel:
1315
+
1316
+ 1. Add `Serial.printf("raw=(%d,%d,%d)\n", p.x, p.y, p.z)` to the touch poll
1317
+ 2. Touch the four corners of the screen and note the raw values
1318
+ 3. Set `xMin`/`xMax` from the left/right edges, `yMin`/`yMax` from the top/bottom
1319
+
1320
+ The transpiler handles rotation (axis swap + inversion) automatically based on the `rotation` field in the display profile.
1321
+
1322
+ ### Touch events (onClick, onHold, onRelease)
1323
+
1324
+ ```typescript
1325
+ // Short tap (finger down + up within 600ms)
1326
+ screen.btn.onClick(() => {
1327
+ console.log("tapped");
1328
+ screen.counter.value = screen.counter.value + 1;
1329
+ });
1330
+
1331
+ // Long press (finger held ≥600ms)
1332
+ screen.btn.onHold(() => {
1333
+ console.log("held");
1334
+ });
1335
+
1336
+ // Finger lift (always fires after click or hold)
1337
+ screen.btn.onRelease(() => {
1338
+ console.log("released");
1339
+ });
1340
+ ```
1341
+
1342
+ The touch system implements a state machine:
1343
+ - **50ms debounce** — prevents rapid re-triggering
1344
+ - **Click** — touch down + up within 600ms
1345
+ - **Hold** — touch held ≥600ms (fires once)
1346
+ - **Release** — finger lifts (clears `.value` to 0)
1347
+ - **Visual feedback** — `.value` set to 1 on touch down, 0 on release
1348
+
1349
+ Hit-testing walks nodes topmost-first and skips containers without click handlers.
1350
+
1351
+ ### Awaitable tap notifications (`ui.onTap`)
1352
+
1353
+ `ui.onTap()` is an **awaitable** tap signal — use it inside an `async` function
1354
+ to suspend until the next tap. It's the building block for display-sleep /
1355
+ screensaver behavior and custom flow control ("tap to continue"):
1356
+
1357
+ ```typescript
1358
+ // Display-sleep: wake on ANY touch, dim again after 10s of inactivity.
1359
+ async function screensaver() {
1360
+ while (true) {
1361
+ backlightOff();
1362
+ await ui.onTap(); // resume on the next tap, anywhere on the screen
1363
+ backlightOn();
1364
+ await delay(10000); // keep the display awake for 10 seconds
1365
+ }
1366
+ }
1367
+ ```
1368
+
1369
+ With no argument it resumes on the next tap **anywhere** — including empty
1370
+ space, which is what makes "wake on any touch" work even when the finger lands
1371
+ on no element. Pass an element to resume only when that element is tapped:
1372
+
1373
+ ```typescript
1374
+ // Tap-to-continue wizard: wait for the Start button specifically.
1375
+ async function setupWizard() {
1376
+ await ui.onTap(screen.start);
1377
+ beginSetup();
1378
+ }
1379
+ ```
1380
+
1381
+ A tap fires **both** the tapped element's `onClick` handler **and** resumes any
1382
+ `await ui.onTap()` awaiter — they don't compete. `ui.onTap()` is a resume signal,
1383
+ not a value: there is nothing to read from it (it returns `Promise<void>`).
1384
+
1385
+ | Call | Resumes on |
1386
+ |---|---|
1387
+ | `await ui.onTap()` | the next tap anywhere (including empty space) |
1388
+ | `await ui.onTap(screen.elem)` | the next tap on that specific element |
1389
+
1390
+ Under the hood this lowers to a cooperative state-machine state that polls a
1391
+ tap counter incremented by the touch driver each frame — no ISRs, natural
1392
+ debounce from the ~16ms tick, same model as `await delay()`.
1393
+
1394
+ ### Custom touch adapters
1395
+
1396
+ For libraries not in the built-in list, write a TypeScript adapter:
1397
+
1398
+ ```typescript
1399
+ // my-touch-adapter.ts
1400
+ import { SomeTouchLib } from '../lib/SomeTouchLib/SomeTouchLib';
1401
+
1402
+ const ts = new SomeTouchLib(14, 2);
1403
+ ts.begin();
1404
+
1405
+ export const touch = {
1406
+ isTouched: () => ts.touched(),
1407
+ read: () => {
1408
+ const p = ts.getPoint();
1409
+ return { x: p.x, y: p.y, z: p.z };
1410
+ },
1411
+ };
1412
+ ```
1413
+
1414
+ Reference it in config:
1415
+
1416
+ ```typescript
1417
+ touch: {
1418
+ adapter: './my-touch-adapter',
1419
+ calibration: { xMin: 100, xMax: 4000, yMin: 100, yMax: 4000 },
1420
+ minPressure: 10,
1421
+ }
1422
+ ```
1423
+
1424
+ The adapter only provides raw `{x, y, z}` — the transpiler handles calibration, rotation, and coordinate mapping.
1425
+
1426
+ ## GPIO input (buttons without touch)
1427
+
1428
+ For physical buttons on GPIO pins (no touchscreen required):
1429
+
1430
+ ```typescript
1431
+ // Watch a pin for falling edges — runs in the frame loop
1432
+ ui.watchPin(4, () => {
1433
+ screen.counter.value = screen.counter.value + 1;
1434
+ });
1435
+
1436
+ // Toggle an element's .value on pin press
1437
+ screen.ledBox.onToggle(5);
1438
+
1439
+ // Cycle through options
1440
+ screen.modeValue.onChange(15, 3); // 3 options: 0→1→2→0
1441
+ ```
1442
+
1443
+ Natural debounce from the ~16ms frame rate — no ISR, no `volatile`.