@typecad/ui 1.0.0-alpha.13 → 1.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1457 -1469
- package/dist/cli.js +22 -19
- package/dist/preview/host-ui-runtime.js +0 -1
- package/dist/ui-engine/model.js +7 -1
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
- package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
- package/dist/ui-engine/runtime-header/keyboard.js +676 -676
- package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1656
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
- package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
- package/dist/ui-engine/runtime-header/structs.js +228 -228
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
- package/dist/ui-engine/runtime-header/types-defines.js +110 -110
- package/dist/ui-engine/shadcn-kit.d.ts +1 -1
- package/dist/ui-engine/shadcn-kit.js +4 -0
- package/dist/ui-engine/ui-lowering.d.ts +1 -1
- package/dist/ui-engine/ui-lowering.js +14 -5
- package/dist/ui-engine/ui-registry.js +9 -1
- package/dist/wizard/display-catalog.d.ts +0 -3
- package/dist/wizard/display-catalog.js +3 -12
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.js +1 -0
- package/dist/wizard/integration-wizard.d.ts +3 -0
- package/dist/wizard/integration-wizard.js +69 -20
- package/dist/wizard/package-writer.d.ts +20 -0
- package/dist/wizard/package-writer.js +79 -0
- package/package.json +4 -4
- package/src/cli.ts +90 -87
- package/src/preview/host-ui-runtime.ts +0 -1
- package/src/ui-engine/model.ts +6 -1
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
- package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
- package/src/ui-engine/runtime-header/keyboard.ts +689 -689
- package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1681
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
- package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
- package/src/ui-engine/runtime-header/structs.ts +234 -234
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
- package/src/ui-engine/runtime-header/types-defines.ts +116 -116
- package/src/ui-engine/shadcn-kit.ts +4 -0
- package/src/ui-engine/ui-lowering.ts +12 -4
- package/src/ui-engine/ui-registry.ts +9 -1
- package/src/wizard/display-catalog.ts +261 -273
- package/src/wizard/index.ts +46 -38
- package/src/wizard/integration-wizard.ts +679 -619
- package/src/wizard/package-writer.ts +98 -0
|
@@ -2,29 +2,29 @@
|
|
|
2
2
|
// scrollbar + buffered scroll canvas push.
|
|
3
3
|
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
4
|
export function emitTickScrollCanvasPhase() {
|
|
5
|
-
return `
|
|
6
|
-
// ②b Draw scrollbar + push canvas for the buffered scroll container.
|
|
7
|
-
if (bufferedScrollNode >= 0 && bufferedScrollCanvas) {
|
|
8
|
-
ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
|
|
9
|
-
bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
|
|
10
|
-
bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
|
|
11
|
-
} else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
|
|
12
|
-
// Band render fallback: the main loop didn't trigger it at the owner's
|
|
13
|
-
// z-slot (e.g. the owner was filtered out after dispatch). Render now so
|
|
14
|
-
// the subtree is never left unpainted. ui_render_scroll_bands sets
|
|
15
|
-
// lastPaintedScrollY itself on success; only the failure fallback (band
|
|
16
|
-
// canvas wouldn't allocate) needs to record the scrollY here so the next
|
|
17
|
-
// frame's delta is correct.
|
|
18
|
-
if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
19
|
-
// No band canvas means there is no coherent frame to submit. Retain the
|
|
20
|
-
// previous viewport rather than drawing the scrollbar/direct subtree to
|
|
21
|
-
// the live SPI target, which would reintroduce tearing.
|
|
22
|
-
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
23
|
-
__ui_nodes[bufferedScrollNode].dirty = 0;
|
|
24
|
-
}
|
|
25
|
-
} else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
|
|
26
|
-
// The direct-full path is intentionally disabled for tear-sensitive scroll
|
|
27
|
-
// rendering. Keep the retained pixels when no compositor is available.
|
|
28
|
-
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
5
|
+
return `
|
|
6
|
+
// ②b Draw scrollbar + push canvas for the buffered scroll container.
|
|
7
|
+
if (bufferedScrollNode >= 0 && bufferedScrollCanvas) {
|
|
8
|
+
ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
|
|
9
|
+
bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
|
|
10
|
+
bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
|
|
11
|
+
} else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
|
|
12
|
+
// Band render fallback: the main loop didn't trigger it at the owner's
|
|
13
|
+
// z-slot (e.g. the owner was filtered out after dispatch). Render now so
|
|
14
|
+
// the subtree is never left unpainted. ui_render_scroll_bands sets
|
|
15
|
+
// lastPaintedScrollY itself on success; only the failure fallback (band
|
|
16
|
+
// canvas wouldn't allocate) needs to record the scrollY here so the next
|
|
17
|
+
// frame's delta is correct.
|
|
18
|
+
if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
19
|
+
// No band canvas means there is no coherent frame to submit. Retain the
|
|
20
|
+
// previous viewport rather than drawing the scrollbar/direct subtree to
|
|
21
|
+
// the live SPI target, which would reintroduce tearing.
|
|
22
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
23
|
+
__ui_nodes[bufferedScrollNode].dirty = 0;
|
|
24
|
+
}
|
|
25
|
+
} else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
|
|
26
|
+
// The direct-full path is intentionally disabled for tear-sensitive scroll
|
|
27
|
+
// rendering. Keep the retained pixels when no compositor is available.
|
|
28
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
29
29
|
__ui_nodes[bufferedScrollNode].dirty = 0;`;
|
|
30
30
|
}
|
|
@@ -26,7 +26,7 @@ extern const uint16_t __ui_rangechange_handler_count;
|
|
|
26
26
|
|
|
27
27
|
// Touch state machine: tracks down → hold → up → click lifecycle
|
|
28
28
|
// Touch node/scroll node state is defined near navigation because ui_navigate resets it.
|
|
29
|
-
static uint32_t __ui_touch_down_time = 0; //
|
|
29
|
+
static uint32_t __ui_touch_down_time = 0; // __tc_now_ms() when touch started
|
|
30
30
|
static int16_t __ui_touch_down_y_pos = 0; // Y position when touch started (for tap vs drag detection)
|
|
31
31
|
static uint32_t __ui_last_touch_time = 0; // for debounce (updated on touch down only)
|
|
32
32
|
static uint32_t __ui_last_release_time = 0; // for release debounce
|
|
@@ -174,13 +174,13 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
|
|
|
174
174
|
// option rows. No hit-test/press/scroll while the overlay is open.
|
|
175
175
|
if (__ui_select_menu >= 0) {
|
|
176
176
|
__ui_touch_state = 1;
|
|
177
|
-
__ui_touch_down_time =
|
|
177
|
+
__ui_touch_down_time = __tc_now_ms();
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
int16_t node = ui_hit_test(tx, ty);
|
|
181
181
|
__ui_touch_node = node;
|
|
182
182
|
__ui_touch_state = 1;
|
|
183
|
-
__ui_touch_down_time =
|
|
183
|
+
__ui_touch_down_time = __tc_now_ms();
|
|
184
184
|
__ui_touch_down_y_pos = ty;
|
|
185
185
|
__ui_drag_start_x = tx;
|
|
186
186
|
__ui_drag_start_y = ty;
|
|
@@ -215,7 +215,7 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
|
|
|
215
215
|
int16_t usable = __ui_nodes[node].box.w - 8;
|
|
216
216
|
if (usable <= 0) usable = 1;
|
|
217
217
|
int16_t nextVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
|
|
218
|
-
nextVal =
|
|
218
|
+
nextVal = __ui_constrain(nextVal, rMin, rMax);
|
|
219
219
|
if (nextVal != __ui_nodes[node].value) {
|
|
220
220
|
__ui_nodes[node].value = nextVal;
|
|
221
221
|
ui_mark_dirty(node);
|
|
@@ -261,7 +261,7 @@ static void ui_touch_up() {
|
|
|
261
261
|
if (__ui_select_menu >= 0) {
|
|
262
262
|
ui_select_menu_tap(__ui_last_touch_x, __ui_last_touch_y);
|
|
263
263
|
__ui_touch_state = 0;
|
|
264
|
-
__ui_last_touch_time =
|
|
264
|
+
__ui_last_touch_time = __tc_now_ms();
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
267
|
#if !defined(UI_HIDE_OSK)
|
|
@@ -269,11 +269,11 @@ static void ui_touch_up() {
|
|
|
269
269
|
ui_kb_handle_tap(__ui_last_touch_x, __ui_last_touch_y);
|
|
270
270
|
__ui_kb_bs_held = 0;
|
|
271
271
|
__ui_touch_state = 0;
|
|
272
|
-
__ui_last_touch_time =
|
|
272
|
+
__ui_last_touch_time = __tc_now_ms();
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
#endif
|
|
276
|
-
uint32_t elapsed =
|
|
276
|
+
uint32_t elapsed = __tc_now_ms() - __ui_touch_down_time;
|
|
277
277
|
// Release the scroll owner: arm a bounded settle (bounce-back / edge-snap).
|
|
278
278
|
// No fling — motion ends with the finger (the settle animation is the only
|
|
279
279
|
// post-lift motion, terminating within UI_SCROLL_SETTLE_MS).
|
|
@@ -353,7 +353,7 @@ static void ui_touch_up() {
|
|
|
353
353
|
// Called each frame from ui_poll_touch when touch is detected.
|
|
354
354
|
// Implements debounce + the down/hold/up/click state machine.
|
|
355
355
|
static inline void ui_handle_touch(int16_t tx, int16_t ty) {
|
|
356
|
-
uint32_t now =
|
|
356
|
+
uint32_t now = __tc_now_ms();
|
|
357
357
|
// Track last touch coords for tap-up routing.
|
|
358
358
|
__ui_last_touch_x = tx;
|
|
359
359
|
__ui_last_touch_y = ty;
|
|
@@ -404,7 +404,7 @@ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
|
|
|
404
404
|
int16_t usable = __ui_nodes[__ui_range_node].box.w - 8;
|
|
405
405
|
if (usable <= 0) usable = 1;
|
|
406
406
|
int16_t newVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
|
|
407
|
-
newVal =
|
|
407
|
+
newVal = __ui_constrain(newVal, rMin, rMax);
|
|
408
408
|
if (newVal != __ui_nodes[__ui_range_node].value) {
|
|
409
409
|
__ui_nodes[__ui_range_node].value = newVal;
|
|
410
410
|
ui_mark_dirty(__ui_range_node);
|
|
@@ -457,9 +457,9 @@ static inline void ui_handle_no_touch() {
|
|
|
457
457
|
if (__ui_touch_state != 0) {
|
|
458
458
|
// Debounce: require a gap since the last release before processing another.
|
|
459
459
|
// This prevents crash from rapid touch/no-touch flicker on resistive screens.
|
|
460
|
-
if (
|
|
460
|
+
if (__tc_now_ms() - __ui_last_release_time < UI_TOUCH_DEBOUNCE_MS) return;
|
|
461
461
|
ui_touch_up();
|
|
462
|
-
__ui_last_release_time =
|
|
462
|
+
__ui_last_release_time = __tc_now_ms();
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
|
|
@@ -2,115 +2,115 @@
|
|
|
2
2
|
// Guard open, includes, and #define knobs.
|
|
3
3
|
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
4
|
export function emitTypesDefines() {
|
|
5
|
-
return `
|
|
6
|
-
// ── Cuttlefish UI runtime (emit once per TU) ──────────────────────────────
|
|
7
|
-
#ifndef __TC_UI_RUNTIME
|
|
8
|
-
#define __TC_UI_RUNTIME
|
|
9
|
-
#include <stdint.h>
|
|
10
|
-
#include <cstring>
|
|
11
|
-
#include <new>
|
|
12
|
-
#define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
|
|
13
|
-
#define UI_TEXT_LINE_BUF 96
|
|
14
|
-
#define UI_WS_NORMAL 0
|
|
15
|
-
#define UI_WS_NOWRAP 1
|
|
16
|
-
#define UI_WS_PRE 2
|
|
17
|
-
#define UI_WS_PRE_LINE 3
|
|
18
|
-
#ifndef UI_MAX_BUFFERED_PAINT_PIXELS
|
|
19
|
-
#define UI_MAX_BUFFERED_PAINT_PIXELS 20000
|
|
20
|
-
#endif
|
|
21
|
-
// Above this many pixels, prefer the band renderer (small ~10KB SRAM canvas,
|
|
22
|
-
// strip-at-a-time) over the repair canvas even when the repair canvas could
|
|
23
|
-
// allocate (e.g. in PSRAM). A large repair canvas (a full-width button is
|
|
24
|
-
// ~15-30KB) has higher alloc/seed/push latency than banding — the per-band
|
|
25
|
-
// push is smaller and the band canvas lives in fast internal SRAM. Set below
|
|
26
|
-
// UI_MAX_BUFFERED_PAINT_PIXELS so large rects band while small repaints keep
|
|
27
|
-
// the fast single-canvas composite. Tune per target; 0 = always prefer the
|
|
28
|
-
// repair canvas when it allocates (the pre-PSRAM behavior).
|
|
29
|
-
#ifndef UI_BAND_PREFER_PIXELS
|
|
30
|
-
#define UI_BAND_PREFER_PIXELS 8000
|
|
31
|
-
#endif
|
|
32
|
-
#ifndef UI_USE_FULL_FRAMEBUFFER
|
|
33
|
-
#define UI_USE_FULL_FRAMEBUFFER 0
|
|
34
|
-
#endif
|
|
35
|
-
#ifndef UI_TRANSITION_SNAP_MS
|
|
36
|
-
#define UI_TRANSITION_SNAP_MS 100
|
|
37
|
-
#endif
|
|
38
|
-
// Scroll physics + capability defaults. The per-TU #define overrides emitted
|
|
39
|
-
// by the UI emitter (from resolveScrollConfig) hit BEFORE this header, so these
|
|
40
|
-
// #ifndef guards adopt the configured values. Spec: 2026-06-28-scroll-engine-rewrite-design.md
|
|
41
|
-
#ifndef UI_SCROLL_MAX_OVERSCROLL
|
|
42
|
-
#define UI_SCROLL_MAX_OVERSCROLL 40
|
|
43
|
-
#endif
|
|
44
|
-
#ifndef UI_SCROLL_STIFFNESS_X10
|
|
45
|
-
#define UI_SCROLL_STIFFNESS_X10 5
|
|
46
|
-
#endif
|
|
47
|
-
#ifndef UI_SCROLL_EDGE_SNAP_PX
|
|
48
|
-
#define UI_SCROLL_EDGE_SNAP_PX 12
|
|
49
|
-
#endif
|
|
50
|
-
#ifndef UI_SCROLL_DRAG_SCALE_X10
|
|
51
|
-
#define UI_SCROLL_DRAG_SCALE_X10 10
|
|
52
|
-
#endif
|
|
53
|
-
#ifndef UI_SCROLL_SETTLE_MS
|
|
54
|
-
#define UI_SCROLL_SETTLE_MS 180
|
|
55
|
-
#endif
|
|
56
|
-
#ifndef UI_SCROLL_DEADBAND_PX
|
|
57
|
-
#define UI_SCROLL_DEADBAND_PX 2
|
|
58
|
-
#endif
|
|
59
|
-
// Capability tier flags (emitted per-TU before this header; defaults = full).
|
|
60
|
-
// The default-on check must run BEFORE the per-flag #ifndef defaults below —
|
|
61
|
-
// after them, all three flags are defined 0 and the fallback would redefine
|
|
62
|
-
// UI_SCROLL_INPUT_TIER_RESISTIVE with a different value (ill-formed).
|
|
63
|
-
#if !defined(UI_SCROLL_INPUT_TIER_CAPACITIVE) && !defined(UI_SCROLL_INPUT_TIER_RESISTIVE) && !defined(UI_SCROLL_INPUT_TIER_NONE)
|
|
64
|
-
#define UI_SCROLL_INPUT_TIER_RESISTIVE 1
|
|
65
|
-
#endif
|
|
66
|
-
#ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
|
|
67
|
-
#define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
|
|
68
|
-
#endif
|
|
69
|
-
#ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
|
|
70
|
-
#define UI_SCROLL_INPUT_TIER_RESISTIVE 0
|
|
71
|
-
#endif
|
|
72
|
-
#ifndef UI_SCROLL_INPUT_TIER_NONE
|
|
73
|
-
#define UI_SCROLL_INPUT_TIER_NONE 0
|
|
74
|
-
#endif
|
|
75
|
-
#ifndef UI_SCROLL_RENDER_TIER_FULL
|
|
76
|
-
#define UI_SCROLL_RENDER_TIER_FULL 1
|
|
77
|
-
#endif
|
|
78
|
-
#ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
|
|
79
|
-
#define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
|
|
80
|
-
#endif
|
|
81
|
-
#define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
|
|
82
|
-
#define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
|
|
83
|
-
// Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
|
|
84
|
-
#ifndef UI_SCROLL_DEBUG
|
|
85
|
-
#define UI_SCROLL_DEBUG 0
|
|
86
|
-
#endif
|
|
87
|
-
// Band renderer: height (px) of the horizontal band canvas used to composite
|
|
88
|
-
// scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
|
|
89
|
-
// budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
|
|
90
|
-
// single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
|
|
91
|
-
// fits within one band — if a glyph straddles a band boundary, its anti-
|
|
92
|
-
// aliased edge pixels blend toward the wrong bg in the adjacent band, visible
|
|
93
|
-
// as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
|
|
94
|
-
// that's ~30KB (fits SRAM on most ESP32 targets).
|
|
95
|
-
// Dirty-rect merging (LVGL refresh-cycle technique): several same-frame
|
|
96
|
-
// dirty nodes whose paint rects are near each other composite as one banded
|
|
97
|
-
// region instead of per-node transactions. Regions cap at the band-
|
|
98
|
-
// compositable pixel budget; singleton rects keep the per-node ladder.
|
|
99
|
-
#ifndef UI_MERGE_INFLATE
|
|
100
|
-
#define UI_MERGE_INFLATE 8
|
|
101
|
-
#endif
|
|
102
|
-
#ifndef UI_MERGE_MAX_REGIONS
|
|
103
|
-
#define UI_MERGE_MAX_REGIONS 6
|
|
104
|
-
#endif
|
|
105
|
-
#ifndef UI_MERGE_MAX_NODES
|
|
106
|
-
#define UI_MERGE_MAX_NODES 12
|
|
107
|
-
#endif
|
|
108
|
-
|
|
109
|
-
#ifndef UI_STRIP_BAND_HEIGHT
|
|
110
|
-
#define UI_STRIP_BAND_HEIGHT 32
|
|
111
|
-
#endif
|
|
112
|
-
#if defined(ESP32) || defined(ESP8266)
|
|
113
|
-
#include <Esp.h>
|
|
114
|
-
#endif
|
|
5
|
+
return `
|
|
6
|
+
// ── Cuttlefish UI runtime (emit once per TU) ──────────────────────────────
|
|
7
|
+
#ifndef __TC_UI_RUNTIME
|
|
8
|
+
#define __TC_UI_RUNTIME
|
|
9
|
+
#include <stdint.h>
|
|
10
|
+
#include <cstring>
|
|
11
|
+
#include <new>
|
|
12
|
+
#define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
|
|
13
|
+
#define UI_TEXT_LINE_BUF 96
|
|
14
|
+
#define UI_WS_NORMAL 0
|
|
15
|
+
#define UI_WS_NOWRAP 1
|
|
16
|
+
#define UI_WS_PRE 2
|
|
17
|
+
#define UI_WS_PRE_LINE 3
|
|
18
|
+
#ifndef UI_MAX_BUFFERED_PAINT_PIXELS
|
|
19
|
+
#define UI_MAX_BUFFERED_PAINT_PIXELS 20000
|
|
20
|
+
#endif
|
|
21
|
+
// Above this many pixels, prefer the band renderer (small ~10KB SRAM canvas,
|
|
22
|
+
// strip-at-a-time) over the repair canvas even when the repair canvas could
|
|
23
|
+
// allocate (e.g. in PSRAM). A large repair canvas (a full-width button is
|
|
24
|
+
// ~15-30KB) has higher alloc/seed/push latency than banding — the per-band
|
|
25
|
+
// push is smaller and the band canvas lives in fast internal SRAM. Set below
|
|
26
|
+
// UI_MAX_BUFFERED_PAINT_PIXELS so large rects band while small repaints keep
|
|
27
|
+
// the fast single-canvas composite. Tune per target; 0 = always prefer the
|
|
28
|
+
// repair canvas when it allocates (the pre-PSRAM behavior).
|
|
29
|
+
#ifndef UI_BAND_PREFER_PIXELS
|
|
30
|
+
#define UI_BAND_PREFER_PIXELS 8000
|
|
31
|
+
#endif
|
|
32
|
+
#ifndef UI_USE_FULL_FRAMEBUFFER
|
|
33
|
+
#define UI_USE_FULL_FRAMEBUFFER 0
|
|
34
|
+
#endif
|
|
35
|
+
#ifndef UI_TRANSITION_SNAP_MS
|
|
36
|
+
#define UI_TRANSITION_SNAP_MS 100
|
|
37
|
+
#endif
|
|
38
|
+
// Scroll physics + capability defaults. The per-TU #define overrides emitted
|
|
39
|
+
// by the UI emitter (from resolveScrollConfig) hit BEFORE this header, so these
|
|
40
|
+
// #ifndef guards adopt the configured values. Spec: 2026-06-28-scroll-engine-rewrite-design.md
|
|
41
|
+
#ifndef UI_SCROLL_MAX_OVERSCROLL
|
|
42
|
+
#define UI_SCROLL_MAX_OVERSCROLL 40
|
|
43
|
+
#endif
|
|
44
|
+
#ifndef UI_SCROLL_STIFFNESS_X10
|
|
45
|
+
#define UI_SCROLL_STIFFNESS_X10 5
|
|
46
|
+
#endif
|
|
47
|
+
#ifndef UI_SCROLL_EDGE_SNAP_PX
|
|
48
|
+
#define UI_SCROLL_EDGE_SNAP_PX 12
|
|
49
|
+
#endif
|
|
50
|
+
#ifndef UI_SCROLL_DRAG_SCALE_X10
|
|
51
|
+
#define UI_SCROLL_DRAG_SCALE_X10 10
|
|
52
|
+
#endif
|
|
53
|
+
#ifndef UI_SCROLL_SETTLE_MS
|
|
54
|
+
#define UI_SCROLL_SETTLE_MS 180
|
|
55
|
+
#endif
|
|
56
|
+
#ifndef UI_SCROLL_DEADBAND_PX
|
|
57
|
+
#define UI_SCROLL_DEADBAND_PX 2
|
|
58
|
+
#endif
|
|
59
|
+
// Capability tier flags (emitted per-TU before this header; defaults = full).
|
|
60
|
+
// The default-on check must run BEFORE the per-flag #ifndef defaults below —
|
|
61
|
+
// after them, all three flags are defined 0 and the fallback would redefine
|
|
62
|
+
// UI_SCROLL_INPUT_TIER_RESISTIVE with a different value (ill-formed).
|
|
63
|
+
#if !defined(UI_SCROLL_INPUT_TIER_CAPACITIVE) && !defined(UI_SCROLL_INPUT_TIER_RESISTIVE) && !defined(UI_SCROLL_INPUT_TIER_NONE)
|
|
64
|
+
#define UI_SCROLL_INPUT_TIER_RESISTIVE 1
|
|
65
|
+
#endif
|
|
66
|
+
#ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
|
|
67
|
+
#define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
|
|
68
|
+
#endif
|
|
69
|
+
#ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
|
|
70
|
+
#define UI_SCROLL_INPUT_TIER_RESISTIVE 0
|
|
71
|
+
#endif
|
|
72
|
+
#ifndef UI_SCROLL_INPUT_TIER_NONE
|
|
73
|
+
#define UI_SCROLL_INPUT_TIER_NONE 0
|
|
74
|
+
#endif
|
|
75
|
+
#ifndef UI_SCROLL_RENDER_TIER_FULL
|
|
76
|
+
#define UI_SCROLL_RENDER_TIER_FULL 1
|
|
77
|
+
#endif
|
|
78
|
+
#ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
|
|
79
|
+
#define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
|
|
80
|
+
#endif
|
|
81
|
+
#define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
|
|
82
|
+
#define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
|
|
83
|
+
// Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
|
|
84
|
+
#ifndef UI_SCROLL_DEBUG
|
|
85
|
+
#define UI_SCROLL_DEBUG 0
|
|
86
|
+
#endif
|
|
87
|
+
// Band renderer: height (px) of the horizontal band canvas used to composite
|
|
88
|
+
// scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
|
|
89
|
+
// budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
|
|
90
|
+
// single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
|
|
91
|
+
// fits within one band — if a glyph straddles a band boundary, its anti-
|
|
92
|
+
// aliased edge pixels blend toward the wrong bg in the adjacent band, visible
|
|
93
|
+
// as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
|
|
94
|
+
// that's ~30KB (fits SRAM on most ESP32 targets).
|
|
95
|
+
// Dirty-rect merging (LVGL refresh-cycle technique): several same-frame
|
|
96
|
+
// dirty nodes whose paint rects are near each other composite as one banded
|
|
97
|
+
// region instead of per-node transactions. Regions cap at the band-
|
|
98
|
+
// compositable pixel budget; singleton rects keep the per-node ladder.
|
|
99
|
+
#ifndef UI_MERGE_INFLATE
|
|
100
|
+
#define UI_MERGE_INFLATE 8
|
|
101
|
+
#endif
|
|
102
|
+
#ifndef UI_MERGE_MAX_REGIONS
|
|
103
|
+
#define UI_MERGE_MAX_REGIONS 6
|
|
104
|
+
#endif
|
|
105
|
+
#ifndef UI_MERGE_MAX_NODES
|
|
106
|
+
#define UI_MERGE_MAX_NODES 12
|
|
107
|
+
#endif
|
|
108
|
+
|
|
109
|
+
#ifndef UI_STRIP_BAND_HEIGHT
|
|
110
|
+
#define UI_STRIP_BAND_HEIGHT 32
|
|
111
|
+
#endif
|
|
112
|
+
#if defined(ESP32) || defined(ESP8266)
|
|
113
|
+
#include <Esp.h>
|
|
114
|
+
#endif
|
|
115
115
|
`;
|
|
116
116
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SHADCN_KIT_CSS = "/* ---------------------------------------------------------------------------\n * shadcn-style component kit \u2014 BUILT IN and always included.\n *\n * These tokens + class recipes are prepended to every build and preview\n * automatically, BEFORE your stylesheets, so anything you write overrides\n * them by normal cascade order. No scaffolding, no imports needed: put the\n * classes on native elements and they work.\n *\n * THEMES: a theme is any CSS file. Pre-packaged ones ship with @typecad/ui \u2014\n * import by bare specifier from a <style> block (or the sidecar .ui.css):\n *\n * @import \"@typecad/ui/themes/blue.css\"; (zinc slate stone gray neutral\n * blue green red)\n *\n * Your own: save a ui.shadcn.com / tweakcn export into the project and\n * @import it by path \u2014 its :root/.dark token blocks override the kit\n * defaults (later definitions win). Both dialects parse \u2014 classic HSL channel triplets and\n * Tailwind-v4 oklch(); alpha in colors is ignored (no blending on bare\n * metal). Dark mode activates with themeClass: 'dark' in the config's\n * display block.\n *\n * Overriding recipes: redefine any class in your own stylesheet; your\n * definition wins.\n *\n * Components (classes over native elements):\n * Button <button class=\"btn btn-primary\">Save</button>\n * variants: -secondary -outline -ghost -destructive\n * sizes: .btn-sm .btn-lg full width: .btn-block\n * Badge <text class=\"badge badge-secondary\">new</text>\n * variants: -default(omit) -destructive -outline\n * Card <view class=\"card\">\n * <view class=\"card-header\">\n * <text class=\"card-title\">Title</text>\n * <text class=\"card-description\">Subtitle</text>\n * </view>\n * <view class=\"card-content\">...</view>\n * <view class=\"card-footer\">...buttons...</view>\n * </view>\n * Input <input class=\"input\" placeholder=\"...\"/>\n * Label <label class=\"form-label\">Name</label>\n * Row (kit) <view class=\"row\"> ... </view> (flex row + gap, for composing)\n * Validation .input-error (destructive border) + .field-error hint;\n * runtime-driven: bind borderColor/visible to a signal\n * Separator <hr class=\"separator\"/> (horizontal) or\n * <hr class=\"vseparator\"/> (vertical; stretches to the row height)\n * Alert <view class=\"alert alert-destructive\">\n * <text class=\"alert-title\">...</text>\n * <text class=\"alert-description\">...</text>\n * </view>\n * Skeleton <view class=\"skeleton\"/> (pulse while loading)\n * Spinner <view class=\"spinner\"><view class=\"spinner-dot\"/></view>\n * (indeterminate loading: a dot orbiting a ring via pure\n * transform keyframes \u2014 translate lerps smoothly, unlike\n * rotate which only renders exact quarter turns)\n * Dialog <dialog id=\"d\"><view class=\"dialog-scrim\" on:click={...}/>\n * <view class=\"dialog-card\"> ... <view class=\"dialog-footer\">\n * </view></dialog>\n * (centered modal: programmatic ui.dialog.open(id), scrim tap\n * closes; same slot machinery as the drawer)\n * Toast <toast id=\"t\" side=\"bottom\" duration=\"2500\" class=\"toast\">\n * ...</toast> (ui.toast(id) shows it; auto-closes after the\n * duration; stacks as authored siblings)\n * Progress <progress class=\"progress\" value=\"40\"/>\n * Avatar <img class=\"avatar\" src=\"face.bmp\"/>\n * Switch <check class=\"switch\"/> (pill container; the\n * 16px indicator is runtime-drawn at the top-left \u2014 best-effort)\n * Tabs <view class=\"tabs-list\"><button class=\"tabs-trigger\">..</button>..\n * <view class=\"tabs-content-area\"><view class=\"tabs-content\">..\n * (panes toggle via ui.bind(x, 'visible', ...) on a signal)\n * Accordion <view class=\"accordion\"><view class=\"accordion-item\">..\n * (content toggles via ui.bind visible; chevron via text bind)\n * Table <table> ... </table> (UA styles apply; see the\n * html-table-approximation build note)\n * ------------------------------------------------------------------------- */\n\n:root {\n --background: #ffffff;\n --foreground: #09090b;\n --card: #ffffff;\n --card-foreground: #09090b;\n --primary: #18181b;\n --primary-foreground: #fafafa;\n --secondary: #f4f4f5;\n --secondary-foreground: #18181b;\n --muted: #f4f4f5;\n --muted-foreground: #71717a;\n --accent: #f4f4f5;\n --accent-foreground: #18181b;\n --destructive: #dc2626;\n --destructive-foreground: #fafafa;\n --destructive-background: #fee2e2;\n --border: #e4e4e7;\n --input: #e4e4e7;\n --radius: 8px;\n --shadow-sm: 0 1px 3px rgb(0 0 0 / 0.1);\n /* Solid scrim for dialogs \u2014 alpha blending needs a canvas underneath, so\n the kit uses an opaque near-black that reads as a dimmed backdrop. */\n --scrim: #101014;\n}\n\n.dark {\n --background: #09090b;\n --foreground: #fafafa;\n --card: #18181b;\n --card-foreground: #fafafa;\n --primary: #fafafa;\n --primary-foreground: #18181b;\n --secondary: #27272a;\n --secondary-foreground: #fafafa;\n --muted: #27272a;\n --muted-foreground: #a1a1aa;\n --accent: #27272a;\n --accent-foreground: #fafafa;\n --destructive: #ef4444;\n --destructive-foreground: #09090b;\n --destructive-background: #451a1a;\n --border: #27272a;\n --input: #3f3f46;\n --radius: 8px;\n}\n\n/* ---- Button ---------------------------------------------------------------- */\n\n.btn {\n border-radius: var(--radius);\n padding: 10px 16px;\n font-weight: bold;\n transition: background 80ms;\n}\n.btn:pressed { transform: translateY(1px); }\n.btn-primary { background: var(--primary); color: var(--primary-foreground); }\n.btn-secondary { background: var(--secondary); color: var(--secondary-foreground); }\n.btn-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }\n.btn-ghost { color: var(--foreground); }\n.btn-destructive { background: var(--destructive); color: var(--destructive-foreground); }\n.btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }\n.btn-lg { padding: 14px 22px; min-height: 52px; font-size: 18px; }\n.btn-block { align-self: stretch; }\n\n/* ---- Badge ------------------------------------------------------------------ */\n\n.badge {\n background: var(--primary);\n color: var(--primary-foreground);\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 12px;\n text-align: center;\n}\n.badge-secondary { background: var(--secondary); color: var(--secondary-foreground); }\n.badge-destructive { background: var(--destructive); color: var(--destructive-foreground); }\n.badge-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }\n\n/* ---- Layout utility -------------------------------------------------------------\n Not a shadcn component \u2014 a kit convenience for composing recipes. */\n\n.row { flex-direction: row; align-items: center; gap: 8px; }\n\n/* ---- Card ------------------------------------------------------------------- */\n\n.card {\n background: var(--card);\n color: var(--card-foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow-sm);\n padding: 16px;\n gap: 8px;\n align-self: stretch;\n}\n.card-header { gap: 4px; }\n.card-title { font-size: 18px; font-weight: bold; }\n.card-description { font-size: 14px; color: var(--muted-foreground); }\n.card-content { gap: 8px; }\n.card-footer { flex-direction: row; gap: 8px; }\n\n/* ---- Tabs ---------------------------------------------------------------- */\n\n/* Segmented trigger row (shadcn TabsList). Active-trigger styling is driven\n at runtime by ui.bind background/color bindings in author code \u2014 runtime\n color swaps are literal hex (compile-time tokens), so pin them to the\n active theme the way native_demo accents do. */\n.tabs-list {\n flex-direction: row;\n gap: 4px;\n align-self: stretch;\n background: var(--muted);\n border-radius: var(--radius);\n padding: 4px;\n}\n.tabs-trigger {\n flex-grow: 1;\n border-radius: calc(var(--radius) - 2px);\n padding: 6px 10px;\n min-height: 32px;\n font-size: 13px;\n text-align: center;\n}\n/* Fixed-height content region: panes are absolutely stacked inside it and\n toggled via ui.bind(x, 'visible', ...) \u2014 layout keeps every pane's box,\n so switching never re-flows. */\n.tabs-content-area {\n position: relative;\n align-self: stretch;\n height: 150px;\n}\n.tabs-content {\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n gap: 8px;\n}\n\n/* ---- Accordion (single-open) --------------------------------------------- */\n\n/* Stacked collapsible sections. The trigger is a button + chevron row; the\n content pane toggles via ui.bind(x, 'visible', () => signal === i) \u2014 the\n same signal pattern as Tabs. The chevron swaps v/^ via a text binding. */\n.accordion {\n gap: 8px;\n align-self: stretch;\n}\n.accordion-item {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n}\n.accordion-trigger-row {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n}\n.accordion-trigger {\n flex-grow: 1;\n text-align: left;\n font-weight: bold;\n padding: 0;\n min-height: 20px;\n /* The UA sheet gives every button a 1px border (border: 1px solid, colored\n by the foreground token). A trigger is a plain text row on the card \u2014\n cancel it or each one renders with a bright outline. */\n border: none;\n}\n.accordion-chevron {\n color: var(--muted-foreground);\n font-size: 14px;\n /* Wide enough for the '^' glyph (advance 12px at 14px size) \u2014 a single\n char wider than the box wraps to nothing. */\n width: 16px;\n text-align: center;\n}\n.accordion-content {\n padding: 0 12px 10px 12px;\n gap: 8px;\n}\n\n/* ---- Form controls ----------------------------------------------------------- */\n\n.input {\n background: var(--background);\n color: var(--foreground);\n border: 1px solid var(--input);\n border-radius: var(--radius);\n}\n.form-label { color: var(--foreground); font-size: 14px; }\n\n/* Switch: pill container. The runtime draws a 16px circular knob that\n slides with state \u2014 hollow at the left when off, solid at the right when\n on (a static jump, no travel animation). min-height overrides the UA\n touch target so the pill stays 26px tall. */\n.switch {\n width: 44px;\n height: 26px;\n /* Override the UA's touch-target min-height (42px on small panels) \u2014 the\n pill must stay 26px tall to read as a switch, not a checkbox. */\n min-height: 26px;\n border-radius: 999px;\n border: 1px solid var(--border);\n /* --card, not --muted: on a bare (near-black) page background a --muted\n track reads as a stray gray box; --card keeps the pill affordance while\n staying quiet. Swap back to var(--muted) inside lighter containers. */\n background: var(--card);\n}\n\n/* ---- Checked-state pairs (shadcn data-[state=checked]) ------------------------\n The ON control carries the primary pair: the switch's TRACK turns primary\n with a primary-foreground knob, the checkbox face and the radio's selected\n ring do the same. The select's pair themes the OPTION LIST's selected row\n with the accent pair (shadcn's SelectItem selected state). These bake at\n build time from :checked rules (a separate style bucket, like :pressed);\n the device runtime swaps the pair onto the indicator when the value flips,\n and the preview draws through the same fields. */\n.switch:checked {\n background: var(--primary);\n border-color: var(--primary);\n color: var(--primary-foreground);\n}\ncheck:checked {\n background: var(--primary);\n color: var(--primary-foreground);\n}\nradio:checked {\n background: var(--primary);\n color: var(--primary-foreground);\n}\n.select:checked {\n background: var(--accent);\n color: var(--accent-foreground);\n}\n\n/* ---- Form validation states -------------------------------------------------- */\n\n/* Static hook: always-invalid styling for hardcoded markup. Runtime-driven\n validation (recommended) binds the input's borderColor + a .field-error\n hint's visible/text to a signal \u2014 see the demo's Forms screen. */\n.input-error {\n border-color: var(--destructive);\n}\n.field-error {\n color: var(--destructive);\n font-size: 12px;\n}\n.field-success {\n color: var(--muted-foreground);\n font-size: 12px;\n}\n\n/* ---- Separator ---------------------------------------------------------------- */\n\n.separator {\n height: 1px;\n background: var(--border);\n margin: 8px 0;\n}\n/* Vertical: 1px wide, stretches to the row's cross height (shadcn's\n <Separator orientation=\"vertical\" /> \u2014 flex column + align-self: stretch). */\n.vseparator {\n width: 1px;\n height: 100%;\n align-self: stretch;\n background: var(--border);\n margin: 0 8px;\n}\n\n/* ---- Alert -------------------------------------------------------------------- */\n\n.alert {\n background: var(--background);\n color: var(--foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 12px;\n gap: 4px;\n align-self: stretch;\n}\n.alert-destructive {\n background: var(--destructive-background);\n border: 1px solid var(--destructive);\n color: var(--destructive);\n}\n.alert-title { font-weight: bold; }\n.alert-description { font-size: 14px; color: var(--muted-foreground); }\n\n/* ---- Skeleton (loading placeholder) --------------------------------------------- */\n\n/* Opacity pulse. If the target display's keyframe support skips opacity,\n the block still renders as a static muted placeholder. */\n.skeleton {\n background: var(--muted);\n border-radius: var(--radius);\n height: 16px;\n align-self: stretch;\n animation: ui-skeleton-pulse 1.2s ease-in-out infinite;\n}\n@keyframes ui-skeleton-pulse {\n 0% { opacity: 1; }\n 50% { opacity: 0.55; }\n 100% { opacity: 1; }\n}\n\n/* ---- Spinner ---------------------------------------------------------------- */\n\n/* Indeterminate loading indicator: a dot orbiting inside a ring, pure\n transform keyframes. translate() lerps CONTINUOUSLY between stops (unlike\n rotate(), which only renders exact quarter turns), so the orbit is smooth.\n The dot is absolute + out of flow; the ring is a fixed square so border\n clipping stays symmetric. Sizes: change .spinner's width/height and keep\n the dot inset consistent (orbit travel = inner - dot). */\n.spinner {\n position: relative;\n width: 22px;\n height: 22px;\n border: 2px solid var(--muted);\n border-radius: 999px;\n}\n.spinner-dot {\n position: absolute;\n /* The dot's BASE position is the orbit's TOP-LEFT corner, not the ring\n center: the keyframes translate 0..8px from here, and the path only\n centers when base + travel/2 == ring center (with the 2px border,\n top/left 2px + border lands the 6px dot at 4,4; its center travels\n 7..15 around the 22px ring's center at 11,11). */\n top: 2px;\n left: 2px;\n width: 6px;\n height: 6px;\n background: var(--primary);\n border-radius: 999px;\n animation: ui-spinner-orbit 1000ms linear infinite;\n}\n@keyframes ui-spinner-orbit {\n 0% { transform: translate(0px, 0px); }\n 25% { transform: translate(8px, 0px); }\n 50% { transform: translate(8px, 8px); }\n 75% { transform: translate(0px, 8px); }\n 100% { transform: translate(0px, 0px); }\n}\n/* ---- Dialog ---------------------------------------------------------------- */\n\n/* Centered modal: <dialog> is a centered drawer (side-free) \u2014 programmatic\n open/close via ui.dialog.open(id)/close(id?), hidden while closed by the\n runtime (visibility gate for centered panels; offsets can't hide them).\n The scrim is part of the markup: an absolute full-area view whose tap\n closes the dialog; the card sits above it. z-index keeps both over page\n content. */\n.dialog-scrim {\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n background: var(--scrim);\n z-index: 30;\n}\n.dialog-card {\n position: absolute;\n left: 24px;\n right: 24px;\n top: 64px;\n background: var(--card);\n color: var(--card-foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);\n padding: 16px;\n gap: 8px;\n z-index: 31;\n}\n.dialog-footer {\n flex-direction: row;\n justify-content: flex-end;\n gap: 8px;\n}\n\n/* ---- Toast ----------------------------------------------------------------- */\n\n/* Transient notification: <toast side=\"bottom\" duration=\"2500\"> slides from\n the bottom edge (author-positioned, flush to the edge so the slide fully\n hides it) and auto-closes after duration ms. Show with ui.toast(id).\n Sibling toasts stack by author layout; each closes on its own timer. */\n.toast {\n position: absolute;\n left: 12px;\n right: 12px;\n bottom: 0;\n background: var(--foreground);\n color: var(--background);\n border-radius: var(--radius);\n padding: 12px;\n gap: 4px;\n z-index: 40;\n}\n.toast-title { font-weight: bold; font-size: 14px; }\n.toast-description { font-size: 12px; color: var(--muted-foreground); }\n\n\n\n/* ---- Progress / Avatar ----------------------------------------------------------- */\n\n.progress {\n height: 12px;\n border-radius: 999px;\n background: var(--secondary);\n border: 1px solid var(--border);\n color: var(--primary);\n}\n/* Avatar: plain image element. The runtime draws images rectangular \u2014 no\n rounded clipping \u2014 so no border/radius here (a border would just draw\n over the image). Wrap in a sized view if you want a frame. */\n";
|
|
1
|
+
export declare const SHADCN_KIT_CSS = "/* ---------------------------------------------------------------------------\n * shadcn-style component kit \u2014 BUILT IN and always included.\n *\n * These tokens + class recipes are prepended to every build and preview\n * automatically, BEFORE your stylesheets, so anything you write overrides\n * them by normal cascade order. No scaffolding, no imports needed: put the\n * classes on native elements and they work.\n *\n * THEMES: a theme is any CSS file. Pre-packaged ones ship with @typecad/ui \u2014\n * import by bare specifier from a <style> block (or the sidecar .ui.css):\n *\n * @import \"@typecad/ui/themes/blue.css\"; (zinc slate stone gray neutral\n * blue green red)\n *\n * Your own: save a ui.shadcn.com / tweakcn export into the project and\n * @import it by path \u2014 its :root/.dark token blocks override the kit\n * defaults (later definitions win). Both dialects parse \u2014 classic HSL channel triplets and\n * Tailwind-v4 oklch(); alpha in colors is ignored (no blending on bare\n * metal). Dark mode activates with themeClass: 'dark' in the config's\n * display block.\n *\n * Overriding recipes: redefine any class in your own stylesheet; your\n * definition wins.\n *\n * Components (classes over native elements):\n * Button <button class=\"btn btn-primary\">Save</button>\n * variants: -secondary -outline -ghost -destructive\n * sizes: .btn-sm .btn-lg full width: .btn-block\n * Badge <text class=\"badge badge-secondary\">new</text>\n * variants: -default(omit) -destructive -outline\n * Card <view class=\"card\">\n * <view class=\"card-header\">\n * <text class=\"card-title\">Title</text>\n * <text class=\"card-description\">Subtitle</text>\n * </view>\n * <view class=\"card-content\">...</view>\n * <view class=\"card-footer\">...buttons...</view>\n * </view>\n * Input <input class=\"input\" placeholder=\"...\"/>\n * Label <label class=\"form-label\">Name</label>\n * Row (kit) <view class=\"row\"> ... </view> (flex row + gap, for composing)\n * Validation .input-error (destructive border) + .field-error hint;\n * runtime-driven: bind borderColor/visible to a signal\n * Separator <hr class=\"separator\"/> (horizontal) or\n * <hr class=\"vseparator\"/> (vertical; stretches to the row height)\n * Alert <view class=\"alert alert-destructive\">\n * <text class=\"alert-title\">...</text>\n * <text class=\"alert-description\">...</text>\n * </view>\n * Skeleton <view class=\"skeleton\"/> (pulse while loading)\n * Spinner <view class=\"spinner\"><view class=\"spinner-dot\"/></view>\n * (indeterminate loading: a dot orbiting a ring via pure\n * transform keyframes \u2014 translate lerps smoothly, unlike\n * rotate which only renders exact quarter turns)\n * Dialog <dialog id=\"d\"><view class=\"dialog-scrim\" on:click={...}/>\n * <view class=\"dialog-card\"> ... <view class=\"dialog-footer\">\n * </view></dialog>\n * (centered modal: programmatic ui.dialog.open(id), scrim tap\n * closes; same slot machinery as the drawer)\n * Toast <toast id=\"t\" side=\"bottom\" duration=\"2500\" class=\"toast\">\n * ...</toast> (ui.toast(id) shows it; auto-closes after the\n * duration; stacks as authored siblings)\n * Progress <progress class=\"progress\" value=\"40\"/>\n * Avatar <img class=\"avatar\" src=\"face.bmp\"/>\n * Switch <check class=\"switch\"/> (pill container; the\n * 16px indicator is runtime-drawn at the top-left \u2014 best-effort)\n * Tabs <view class=\"tabs-list\"><button class=\"tabs-trigger\">..</button>..\n * <view class=\"tabs-content-area\"><view class=\"tabs-content\">..\n * (panes toggle via ui.bind(x, 'visible', ...) on a signal)\n * Accordion <view class=\"accordion\"><view class=\"accordion-item\">..\n * (content toggles via ui.bind visible; chevron via text bind)\n * Table <table> ... </table> (UA styles apply; see the\n * html-table-approximation build note)\n * ------------------------------------------------------------------------- */\n\n:root {\n --background: #ffffff;\n --foreground: #09090b;\n --card: #ffffff;\n --card-foreground: #09090b;\n --primary: #18181b;\n --primary-foreground: #fafafa;\n --secondary: #f4f4f5;\n --secondary-foreground: #18181b;\n --muted: #f4f4f5;\n --muted-foreground: #71717a;\n --accent: #f4f4f5;\n --accent-foreground: #18181b;\n --destructive: #dc2626;\n --destructive-foreground: #fafafa;\n --destructive-background: #fee2e2;\n --border: #e4e4e7;\n --input: #e4e4e7;\n --radius: 8px;\n --shadow-sm: 0 1px 3px rgb(0 0 0 / 0.1);\n /* Solid scrim for dialogs \u2014 alpha blending needs a canvas underneath, so\n the kit uses an opaque near-black that reads as a dimmed backdrop. */\n --scrim: #101014;\n}\n\n.dark {\n --background: #09090b;\n --foreground: #fafafa;\n --card: #18181b;\n --card-foreground: #fafafa;\n --primary: #fafafa;\n --primary-foreground: #18181b;\n --secondary: #27272a;\n --secondary-foreground: #fafafa;\n --muted: #27272a;\n --muted-foreground: #a1a1aa;\n --accent: #27272a;\n --accent-foreground: #fafafa;\n --destructive: #ef4444;\n --destructive-foreground: #09090b;\n --destructive-background: #451a1a;\n --border: #27272a;\n --input: #3f3f46;\n --radius: 8px;\n}\n\n/* ---- Button ---------------------------------------------------------------- */\n\n.btn {\n border-radius: var(--radius);\n padding: 10px 16px;\n font-weight: bold;\n transition: background 80ms;\n /* Reset the UA's browser-default button border (shadcn's button reset):\n ghost must be truly borderless \u2014 variants that want one (outline)\n re-declare it themselves. */\n border: none;\n}\n.btn:pressed { transform: translateY(1px); }\n.btn-primary { background: var(--primary); color: var(--primary-foreground); }\n.btn-secondary { background: var(--secondary); color: var(--secondary-foreground); }\n.btn-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }\n.btn-ghost { color: var(--foreground); }\n.btn-destructive { background: var(--destructive); color: var(--destructive-foreground); }\n.btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }\n.btn-lg { padding: 14px 22px; min-height: 52px; font-size: 18px; }\n.btn-block { align-self: stretch; }\n\n/* ---- Badge ------------------------------------------------------------------ */\n\n.badge {\n background: var(--primary);\n color: var(--primary-foreground);\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 12px;\n text-align: center;\n}\n.badge-secondary { background: var(--secondary); color: var(--secondary-foreground); }\n.badge-destructive { background: var(--destructive); color: var(--destructive-foreground); }\n.badge-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }\n\n/* ---- Layout utility -------------------------------------------------------------\n Not a shadcn component \u2014 a kit convenience for composing recipes. */\n\n.row { flex-direction: row; align-items: center; gap: 8px; }\n\n/* ---- Card ------------------------------------------------------------------- */\n\n.card {\n background: var(--card);\n color: var(--card-foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow-sm);\n padding: 16px;\n gap: 8px;\n align-self: stretch;\n}\n.card-header { gap: 4px; }\n.card-title { font-size: 18px; font-weight: bold; }\n.card-description { font-size: 14px; color: var(--muted-foreground); }\n.card-content { gap: 8px; }\n.card-footer { flex-direction: row; gap: 8px; }\n\n/* ---- Tabs ---------------------------------------------------------------- */\n\n/* Segmented trigger row (shadcn TabsList). Active-trigger styling is driven\n at runtime by ui.bind background/color bindings in author code \u2014 runtime\n color swaps are literal hex (compile-time tokens), so pin them to the\n active theme the way native_demo accents do. */\n.tabs-list {\n flex-direction: row;\n gap: 4px;\n align-self: stretch;\n background: var(--muted);\n border-radius: var(--radius);\n padding: 4px;\n}\n.tabs-trigger {\n flex-grow: 1;\n border-radius: calc(var(--radius) - 2px);\n padding: 6px 10px;\n min-height: 32px;\n font-size: 13px;\n text-align: center;\n}\n/* Fixed-height content region: panes are absolutely stacked inside it and\n toggled via ui.bind(x, 'visible', ...) \u2014 layout keeps every pane's box,\n so switching never re-flows. */\n.tabs-content-area {\n position: relative;\n align-self: stretch;\n height: 150px;\n}\n.tabs-content {\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n gap: 8px;\n}\n\n/* ---- Accordion (single-open) --------------------------------------------- */\n\n/* Stacked collapsible sections. The trigger is a button + chevron row; the\n content pane toggles via ui.bind(x, 'visible', () => signal === i) \u2014 the\n same signal pattern as Tabs. The chevron swaps v/^ via a text binding. */\n.accordion {\n gap: 8px;\n align-self: stretch;\n}\n.accordion-item {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n}\n.accordion-trigger-row {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n}\n.accordion-trigger {\n flex-grow: 1;\n text-align: left;\n font-weight: bold;\n padding: 0;\n min-height: 20px;\n /* The UA sheet gives every button a 1px border (border: 1px solid, colored\n by the foreground token). A trigger is a plain text row on the card \u2014\n cancel it or each one renders with a bright outline. */\n border: none;\n}\n.accordion-chevron {\n color: var(--muted-foreground);\n font-size: 14px;\n /* Wide enough for the '^' glyph (advance 12px at 14px size) \u2014 a single\n char wider than the box wraps to nothing. */\n width: 16px;\n text-align: center;\n}\n.accordion-content {\n padding: 0 12px 10px 12px;\n gap: 8px;\n}\n\n/* ---- Form controls ----------------------------------------------------------- */\n\n.input {\n background: var(--background);\n color: var(--foreground);\n border: 1px solid var(--input);\n border-radius: var(--radius);\n}\n.form-label { color: var(--foreground); font-size: 14px; }\n\n/* Switch: pill container. The runtime draws a 16px circular knob that\n slides with state \u2014 hollow at the left when off, solid at the right when\n on (a static jump, no travel animation). min-height overrides the UA\n touch target so the pill stays 26px tall. */\n.switch {\n width: 44px;\n height: 26px;\n /* Override the UA's touch-target min-height (42px on small panels) \u2014 the\n pill must stay 26px tall to read as a switch, not a checkbox. */\n min-height: 26px;\n border-radius: 999px;\n border: 1px solid var(--border);\n /* --card, not --muted: on a bare (near-black) page background a --muted\n track reads as a stray gray box; --card keeps the pill affordance while\n staying quiet. Swap back to var(--muted) inside lighter containers. */\n background: var(--card);\n}\n\n/* ---- Checked-state pairs (shadcn data-[state=checked]) ------------------------\n The ON control carries the primary pair: the switch's TRACK turns primary\n with a primary-foreground knob, the checkbox face and the radio's selected\n ring do the same. The select's pair themes the OPTION LIST's selected row\n with the accent pair (shadcn's SelectItem selected state). These bake at\n build time from :checked rules (a separate style bucket, like :pressed);\n the device runtime swaps the pair onto the indicator when the value flips,\n and the preview draws through the same fields. */\n.switch:checked {\n background: var(--primary);\n border-color: var(--primary);\n color: var(--primary-foreground);\n}\ncheck:checked {\n background: var(--primary);\n color: var(--primary-foreground);\n}\nradio:checked {\n background: var(--primary);\n color: var(--primary-foreground);\n}\n.select:checked {\n background: var(--accent);\n color: var(--accent-foreground);\n}\n\n/* ---- Form validation states -------------------------------------------------- */\n\n/* Static hook: always-invalid styling for hardcoded markup. Runtime-driven\n validation (recommended) binds the input's borderColor + a .field-error\n hint's visible/text to a signal \u2014 see the demo's Forms screen. */\n.input-error {\n border-color: var(--destructive);\n}\n.field-error {\n color: var(--destructive);\n font-size: 12px;\n}\n.field-success {\n color: var(--muted-foreground);\n font-size: 12px;\n}\n\n/* ---- Separator ---------------------------------------------------------------- */\n\n.separator {\n height: 1px;\n background: var(--border);\n margin: 8px 0;\n}\n/* Vertical: 1px wide, stretches to the row's cross height (shadcn's\n <Separator orientation=\"vertical\" /> \u2014 flex column + align-self: stretch). */\n.vseparator {\n width: 1px;\n height: 100%;\n align-self: stretch;\n background: var(--border);\n margin: 0 8px;\n}\n\n/* ---- Alert -------------------------------------------------------------------- */\n\n.alert {\n background: var(--background);\n color: var(--foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 12px;\n gap: 4px;\n align-self: stretch;\n}\n.alert-destructive {\n background: var(--destructive-background);\n border: 1px solid var(--destructive);\n color: var(--destructive);\n}\n.alert-title { font-weight: bold; }\n.alert-description { font-size: 14px; color: var(--muted-foreground); }\n\n/* ---- Skeleton (loading placeholder) --------------------------------------------- */\n\n/* Opacity pulse. If the target display's keyframe support skips opacity,\n the block still renders as a static muted placeholder. */\n.skeleton {\n background: var(--muted);\n border-radius: var(--radius);\n height: 16px;\n align-self: stretch;\n animation: ui-skeleton-pulse 1.2s ease-in-out infinite;\n}\n@keyframes ui-skeleton-pulse {\n 0% { opacity: 1; }\n 50% { opacity: 0.55; }\n 100% { opacity: 1; }\n}\n\n/* ---- Spinner ---------------------------------------------------------------- */\n\n/* Indeterminate loading indicator: a dot orbiting inside a ring, pure\n transform keyframes. translate() lerps CONTINUOUSLY between stops (unlike\n rotate(), which only renders exact quarter turns), so the orbit is smooth.\n The dot is absolute + out of flow; the ring is a fixed square so border\n clipping stays symmetric. Sizes: change .spinner's width/height and keep\n the dot inset consistent (orbit travel = inner - dot). */\n.spinner {\n position: relative;\n width: 22px;\n height: 22px;\n border: 2px solid var(--muted);\n border-radius: 999px;\n}\n.spinner-dot {\n position: absolute;\n /* The dot's BASE position is the orbit's TOP-LEFT corner, not the ring\n center: the keyframes translate 0..8px from here, and the path only\n centers when base + travel/2 == ring center (with the 2px border,\n top/left 2px + border lands the 6px dot at 4,4; its center travels\n 7..15 around the 22px ring's center at 11,11). */\n top: 2px;\n left: 2px;\n width: 6px;\n height: 6px;\n background: var(--primary);\n border-radius: 999px;\n animation: ui-spinner-orbit 1000ms linear infinite;\n}\n@keyframes ui-spinner-orbit {\n 0% { transform: translate(0px, 0px); }\n 25% { transform: translate(8px, 0px); }\n 50% { transform: translate(8px, 8px); }\n 75% { transform: translate(0px, 8px); }\n 100% { transform: translate(0px, 0px); }\n}\n/* ---- Dialog ---------------------------------------------------------------- */\n\n/* Centered modal: <dialog> is a centered drawer (side-free) \u2014 programmatic\n open/close via ui.dialog.open(id)/close(id?), hidden while closed by the\n runtime (visibility gate for centered panels; offsets can't hide them).\n The scrim is part of the markup: an absolute full-area view whose tap\n closes the dialog; the card sits above it. z-index keeps both over page\n content. */\n.dialog-scrim {\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n background: var(--scrim);\n z-index: 30;\n}\n.dialog-card {\n position: absolute;\n left: 24px;\n right: 24px;\n top: 64px;\n background: var(--card);\n color: var(--card-foreground);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);\n padding: 16px;\n gap: 8px;\n z-index: 31;\n}\n.dialog-footer {\n flex-direction: row;\n justify-content: flex-end;\n gap: 8px;\n}\n\n/* ---- Toast ----------------------------------------------------------------- */\n\n/* Transient notification: <toast side=\"bottom\" duration=\"2500\"> slides from\n the bottom edge (author-positioned, flush to the edge so the slide fully\n hides it) and auto-closes after duration ms. Show with ui.toast(id).\n Sibling toasts stack by author layout; each closes on its own timer. */\n.toast {\n position: absolute;\n left: 12px;\n right: 12px;\n bottom: 0;\n background: var(--foreground);\n color: var(--background);\n border-radius: var(--radius);\n padding: 12px;\n gap: 4px;\n z-index: 40;\n}\n.toast-title { font-weight: bold; font-size: 14px; }\n.toast-description { font-size: 12px; color: var(--muted-foreground); }\n\n\n\n/* ---- Progress / Avatar ----------------------------------------------------------- */\n\n.progress {\n height: 12px;\n border-radius: 999px;\n background: var(--secondary);\n border: 1px solid var(--border);\n color: var(--primary);\n}\n/* Avatar: plain image element. The runtime draws images rectangular \u2014 no\n rounded clipping \u2014 so no border/radius here (a border would just draw\n over the image). Wrap in a sized view if you want a frame. */\n";
|
|
@@ -135,6 +135,10 @@ export const SHADCN_KIT_CSS = `/* ----------------------------------------------
|
|
|
135
135
|
padding: 10px 16px;
|
|
136
136
|
font-weight: bold;
|
|
137
137
|
transition: background 80ms;
|
|
138
|
+
/* Reset the UA's browser-default button border (shadcn's button reset):
|
|
139
|
+
ghost must be truly borderless — variants that want one (outline)
|
|
140
|
+
re-declare it themselves. */
|
|
141
|
+
border: none;
|
|
138
142
|
}
|
|
139
143
|
.btn:pressed { transform: translateY(1px); }
|
|
140
144
|
.btn-primary { background: var(--primary); color: var(--primary-foreground); }
|
|
@@ -27,5 +27,5 @@ export interface LoweredUI {
|
|
|
27
27
|
}
|
|
28
28
|
type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
29
29
|
type Storage = "progmem" | "flash";
|
|
30
|
-
export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[]): LoweredUI;
|
|
30
|
+
export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], keyframeNs?: string): LoweredUI;
|
|
31
31
|
export {};
|
|
@@ -16,7 +16,7 @@ import { resolveColor } from "./color.js";
|
|
|
16
16
|
import { defaultFontAttribution } from "./default-font.js";
|
|
17
17
|
import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
|
|
18
18
|
import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
|
|
19
|
-
export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = []) {
|
|
19
|
+
export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = [], keyframeNs = "") {
|
|
20
20
|
void storage;
|
|
21
21
|
const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
|
|
22
22
|
// Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
|
|
@@ -69,7 +69,7 @@ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [],
|
|
|
69
69
|
: `void (*__ui_kb_loaders[])() = {};\nconst uint16_t __ui_kb_loader_count = 0;`;
|
|
70
70
|
const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
|
|
71
71
|
const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
|
|
72
|
-
const keyframeTables = emitKeyframeTables(model);
|
|
72
|
+
const keyframeTables = emitKeyframeTables(model, keyframeNs);
|
|
73
73
|
return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, diagnostics: [] };
|
|
74
74
|
}
|
|
75
75
|
function sanitizedId(id) {
|
|
@@ -358,7 +358,7 @@ function emitTransitionTable(model) {
|
|
|
358
358
|
].join("\n");
|
|
359
359
|
}
|
|
360
360
|
/** Emit keyframe stop arrays + keyframe set index + animation table. */
|
|
361
|
-
function emitKeyframeTables(model) {
|
|
361
|
+
function emitKeyframeTables(model, keyframeNs = "") {
|
|
362
362
|
if (model.keyframeSets.length === 0 && model.animations.length === 0) {
|
|
363
363
|
return [
|
|
364
364
|
`const UIKeyframeSet __ui_keyframe_sets[] = {};`,
|
|
@@ -369,8 +369,15 @@ function emitKeyframeTables(model) {
|
|
|
369
369
|
}
|
|
370
370
|
const lines = [];
|
|
371
371
|
// Emit one stop array per keyframe set.
|
|
372
|
+
// `keyframeNs` namespaces symbols per UI module, and shared stylesheets can
|
|
373
|
+
// register the same animation name into multiple mounted modules — dedupe
|
|
374
|
+
// by name so identical sets emit once (duplicate static arrays fail to link).
|
|
375
|
+
const seenKfNames = new Set();
|
|
372
376
|
for (const ks of model.keyframeSets) {
|
|
373
|
-
|
|
377
|
+
if (seenKfNames.has(ks.name))
|
|
378
|
+
continue;
|
|
379
|
+
seenKfNames.add(ks.name);
|
|
380
|
+
const safeName = (keyframeNs ? keyframeNs + "_" : "") + ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
374
381
|
lines.push(`static const UIKeyframeStop __ui_kf_${safeName}_stops[] = {`);
|
|
375
382
|
for (const s of ks.stops) {
|
|
376
383
|
lines.push(` { .percent=${s.percent}, .props=${s.props}, .bg=${hex(s.bg)}, .fg=${hex(s.fg)}, .opacity=${s.opacity}, .transformOffsetX=${s.transformOffsetX}, .transformOffsetY=${s.transformOffsetY}, .translatePctX=${s.translatePctX}, .translatePctY=${s.translatePctY}, .scaleX=${s.scaleX}, .scaleY=${s.scaleY}, .rotateDeg=${s.rotateDeg}, .width=${s.width}, .height=${s.height} },`);
|
|
@@ -380,7 +387,9 @@ function emitKeyframeTables(model) {
|
|
|
380
387
|
// Emit keyframe set index table.
|
|
381
388
|
lines.push(`const UIKeyframeSet __ui_keyframe_sets[] = {`);
|
|
382
389
|
model.keyframeSets.forEach((ks) => {
|
|
383
|
-
|
|
390
|
+
if (!seenKfNames.has(ks.name))
|
|
391
|
+
return;
|
|
392
|
+
const safeName = (keyframeNs ? keyframeNs + "_" : "") + ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
384
393
|
lines.push(` { .stopCount=${ks.stops.length}, .stops=__ui_kf_${safeName}_stops },`);
|
|
385
394
|
});
|
|
386
395
|
lines.push(`};`);
|
|
@@ -165,7 +165,8 @@ export function lowerOnMount(htmlPath, opts) {
|
|
|
165
165
|
const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
|
|
166
166
|
// Load image assets before lowering so imgDataId can be set.
|
|
167
167
|
const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
|
|
168
|
-
const
|
|
168
|
+
const kfNs = hashlibLike(abs);
|
|
169
|
+
const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, kfNs);
|
|
169
170
|
// Layout diagnostics: viewport-overflow (node bottom past the viewport) and
|
|
170
171
|
// text-overflow (node right edge past its parent). Both reference the node by
|
|
171
172
|
// a human-label (#id / <tag "text"> / <tag>) so the author can find it in
|
|
@@ -247,6 +248,13 @@ export function getLoweredUIModule(htmlPath) {
|
|
|
247
248
|
export function hasUIModule(htmlPath) {
|
|
248
249
|
return modules.has(path.resolve(htmlPath));
|
|
249
250
|
}
|
|
251
|
+
/** Stable short per-module namespace for generated symbols (link-level dedup). */
|
|
252
|
+
function hashlibLike(absPath) {
|
|
253
|
+
let h = 5381;
|
|
254
|
+
for (let i = 0; i < absPath.length; i++)
|
|
255
|
+
h = ((h * 33) ^ absPath.charCodeAt(i)) >>> 0;
|
|
256
|
+
return "m" + h.toString(36);
|
|
257
|
+
}
|
|
250
258
|
/** All loaded UI modules (used by the emitter to inject every mounted tree). */
|
|
251
259
|
export function allUIModules() {
|
|
252
260
|
return [...modules.values()];
|
|
@@ -31,8 +31,6 @@ export interface DisplayCatalogEntry {
|
|
|
31
31
|
invert?: boolean;
|
|
32
32
|
antialias?: boolean;
|
|
33
33
|
};
|
|
34
|
-
/** Arduino libraries the driver needs (for the printed next steps). */
|
|
35
|
-
arduinoLibraries?: string[];
|
|
36
34
|
/** A wiring note printed with the summary. */
|
|
37
35
|
wiringNote?: string;
|
|
38
36
|
}
|
|
@@ -64,7 +62,6 @@ export interface TouchCatalogEntry {
|
|
|
64
62
|
rx: number;
|
|
65
63
|
};
|
|
66
64
|
};
|
|
67
|
-
arduinoLibrary?: string;
|
|
68
65
|
}
|
|
69
66
|
export declare const TOUCH_CATALOG: readonly TouchCatalogEntry[];
|
|
70
67
|
/**
|