@typecad/ui 1.0.0-alpha.12 → 1.0.0-alpha.13

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 (113) hide show
  1. package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
  2. package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  3. package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
  4. package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
  5. package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
  6. package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
  7. package/assets/fonts/dejavu/LICENSE +187 -0
  8. package/assets/fonts/dejavu/README.md +49 -0
  9. package/dist/engine-index.js +2 -0
  10. package/dist/preview/build-program.d.ts +7 -1
  11. package/dist/preview/build-program.js +94 -15
  12. package/dist/preview/host-gfx.d.ts +3 -0
  13. package/dist/preview/host-gfx.js +30 -3
  14. package/dist/preview/host-ui-runtime.d.ts +115 -0
  15. package/dist/preview/host-ui-runtime.js +928 -46
  16. package/dist/ui-engine/block-layout.js +25 -11
  17. package/dist/ui-engine/color.d.ts +6 -0
  18. package/dist/ui-engine/color.js +80 -2
  19. package/dist/ui-engine/compat-report.d.ts +8 -0
  20. package/dist/ui-engine/compat-report.js +124 -0
  21. package/dist/ui-engine/css-imports.d.ts +4 -0
  22. package/dist/ui-engine/css-imports.js +70 -0
  23. package/dist/ui-engine/css-parser.js +58 -14
  24. package/dist/ui-engine/default-font.d.ts +25 -0
  25. package/dist/ui-engine/default-font.js +80 -0
  26. package/dist/ui-engine/font-assets.d.ts +2 -2
  27. package/dist/ui-engine/font-assets.js +22 -5
  28. package/dist/ui-engine/html-parser.d.ts +4 -0
  29. package/dist/ui-engine/html-parser.js +241 -26
  30. package/dist/ui-engine/image-assets.d.ts +8 -1
  31. package/dist/ui-engine/image-assets.js +35 -2
  32. package/dist/ui-engine/image-decode.d.ts +22 -0
  33. package/dist/ui-engine/image-decode.js +194 -0
  34. package/dist/ui-engine/inline-parser.js +1 -1
  35. package/dist/ui-engine/layout-engine.d.ts +19 -0
  36. package/dist/ui-engine/layout-engine.js +59 -3
  37. package/dist/ui-engine/model.d.ts +18 -0
  38. package/dist/ui-engine/model.js +75 -7
  39. package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
  40. package/dist/ui-engine/runtime-header/canvas-helpers.js +3 -0
  41. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
  42. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
  43. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
  44. package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
  45. package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
  46. package/dist/ui-engine/runtime-header/keyboard.js +682 -380
  47. package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1386
  48. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
  49. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
  50. package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
  51. package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
  52. package/dist/ui-engine/runtime-header/structs.js +228 -216
  53. package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
  54. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
  55. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
  56. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
  57. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +78 -23
  58. package/dist/ui-engine/runtime-header/types-defines.js +110 -93
  59. package/dist/ui-engine/shadcn-kit.d.ts +1 -0
  60. package/dist/ui-engine/shadcn-kit.js +502 -0
  61. package/dist/ui-engine/style-resolver.d.ts +4 -0
  62. package/dist/ui-engine/style-resolver.js +58 -6
  63. package/dist/ui-engine/transpile-ui.js +12 -5
  64. package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
  65. package/dist/ui-engine/ua-stylesheet.js +119 -32
  66. package/dist/ui-engine/ui-lowering.js +37 -3
  67. package/dist/ui-engine/ui-registry.js +32 -4
  68. package/dist/ui-engine/yoga-layout.js +57 -52
  69. package/package.json +9 -3
  70. package/src/engine-index.ts +53 -51
  71. package/src/preview/build-program.ts +810 -734
  72. package/src/preview/host-gfx.ts +30 -3
  73. package/src/preview/host-ui-runtime.ts +4133 -3289
  74. package/src/ui-engine/block-layout.ts +24 -11
  75. package/src/ui-engine/color.ts +77 -2
  76. package/src/ui-engine/compat-report.ts +139 -0
  77. package/src/ui-engine/css-imports.ts +69 -0
  78. package/src/ui-engine/css-parser.ts +64 -15
  79. package/src/ui-engine/default-font.ts +95 -0
  80. package/src/ui-engine/font-assets.ts +545 -525
  81. package/src/ui-engine/html-parser.ts +615 -397
  82. package/src/ui-engine/image-assets.ts +37 -2
  83. package/src/ui-engine/image-decode.ts +244 -0
  84. package/src/ui-engine/inline-parser.ts +1 -1
  85. package/src/ui-engine/layout-engine.ts +61 -3
  86. package/src/ui-engine/model.ts +1317 -1230
  87. package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
  88. package/src/ui-engine/runtime-header/canvas-helpers.ts +3 -0
  89. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
  90. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
  91. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
  92. package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
  93. package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
  94. package/src/ui-engine/runtime-header/keyboard.ts +689 -386
  95. package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1411
  96. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
  97. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
  98. package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
  99. package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
  100. package/src/ui-engine/runtime-header/structs.ts +234 -222
  101. package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
  102. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
  103. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
  104. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
  105. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +78 -23
  106. package/src/ui-engine/runtime-header/types-defines.ts +116 -99
  107. package/src/ui-engine/shadcn-kit.ts +503 -0
  108. package/src/ui-engine/style-resolver.ts +471 -416
  109. package/src/ui-engine/transpile-ui.ts +12 -5
  110. package/src/ui-engine/ua-stylesheet.ts +137 -31
  111. package/src/ui-engine/ui-lowering.ts +486 -452
  112. package/src/ui-engine/ui-registry.ts +556 -524
  113. package/src/ui-engine/yoga-layout.ts +307 -309
@@ -2,30 +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
- }
7
- // ②b Draw scrollbar + push canvas for the buffered scroll container.
8
- if (bufferedScrollNode >= 0 && bufferedScrollCanvas) {
9
- ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
10
- bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
11
- bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
12
- } else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
13
- // Band render fallback: the main loop didn't trigger it at the owner's
14
- // z-slot (e.g. the owner was filtered out after dispatch). Render now so
15
- // the subtree is never left unpainted. ui_render_scroll_bands sets
16
- // lastPaintedScrollY itself on success; only the failure fallback (band
17
- // canvas wouldn't allocate) needs to record the scrollY here so the next
18
- // frame's delta is correct.
19
- if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
20
- // No band canvas means there is no coherent frame to submit. Retain the
21
- // previous viewport rather than drawing the scrollbar/direct subtree to
22
- // the live SPI target, which would reintroduce tearing.
23
- if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
24
- __ui_nodes[bufferedScrollNode].dirty = 0;
25
- }
26
- } else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
27
- // The direct-full path is intentionally disabled for tear-sensitive scroll
28
- // rendering. Keep the retained pixels when no compositor is available.
29
- 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;
30
29
  __ui_nodes[bufferedScrollNode].dirty = 0;`;
31
30
  }
@@ -14,11 +14,14 @@ extern UIRadioGroup __ui_radio_groups[];
14
14
  extern const uint16_t __ui_radio_group_count;
15
15
 
16
16
  // Forward-declare the click handler type + tables (defined by the emit layer).
17
- extern void (*__ui_click_handlers[])();
18
- extern void (*__ui_hold_handlers[])();
19
- extern void (*__ui_release_handlers[])();
17
+ // The tables are const (flash rodata) — they are never written at runtime.
18
+ extern void (*const __ui_click_handlers[])();
19
+ extern void (*const __ui_hold_handlers[])();
20
+ extern void (*const __ui_release_handlers[])();
20
21
  extern void (*__ui_rangechange_handlers[])();
21
22
  extern const uint16_t __ui_click_handler_count;
23
+ extern const uint16_t __ui_hold_handler_count;
24
+ extern const uint16_t __ui_release_handler_count;
22
25
  extern const uint16_t __ui_rangechange_handler_count;
23
26
 
24
27
  // Touch state machine: tracks down → hold → up → click lifecycle
@@ -78,6 +81,10 @@ static inline void ui_kb_handle_touch(int16_t tx, int16_t ty);
78
81
  static inline void ui_kb_handle_tap(int16_t tx, int16_t ty);
79
82
  static inline void ui_kb_key_rect(uint8_t idx, UIRect* out);
80
83
  static inline void ui_kb_draw();
84
+
85
+ // Modal <select> + <drawer> declarations live in forward-decls.ts
86
+ // (emit order: ui_navigate/visibility/ui_init reference them before this module).
87
+
81
88
  static inline void ui_kb_draw_key(uint8_t i);
82
89
  static inline void ui_kb_draw_text_row();
83
90
  static inline void ui_kb_compute_box();
@@ -86,13 +93,22 @@ static inline void ui_kb_compute_box();
86
93
  #define UI_DRAG_THRESHOLD 10
87
94
 
88
95
  // Hit-test a touch point against all visible nodes (topmost first).
89
- // Returns the node index of the topmost node that BOTH contains the point
90
- // AND has a click handler registered. Returns -1 if none.
96
+ // Returns the node the tap TARGETS, resolved like the DOM: the topmost node
97
+ // whose box contains the point, then bubbling to ANCESTORS only. Returns -1
98
+ // if no ancestor of the target is interactive or handler-bearing.
91
99
  static int16_t ui_hit_test(int16_t tx, int16_t ty) {
92
- int16_t best = -1;
100
+ // Phase 1 — the event target: the TOPMOST node containing the tap (last in
101
+ // draw order), regardless of handlers. Filtering for handler-bearing nodes
102
+ // here let a tap fall through SIBLING layers: a modal card (no handler,
103
+ // higher z) over a click-to-close scrim (handler) routed every card tap to
104
+ // the scrim underneath, so any tap dismissed the dialog. In the DOM the
105
+ // click targets the topmost element and never propagates sideways to a
106
+ // covered sibling.
107
+ int16_t target = -1;
93
108
  for (uint16_t i = 0; i < __ui_node_count; i++) {
94
109
  if (!ui_is_effectively_visible(i)) continue;
95
110
  if (__ui_nodes[i].screenId != __ui_active_screen) continue;
111
+ if (__ui_nodes[i].disabled) continue; // HTML disabled: not a tap target
96
112
  int16_t drawX = ui_draw_x_for_node(static_cast<uint16_t>(i));
97
113
  int16_t drawY = ui_draw_y_for_node(static_cast<uint16_t>(i));
98
114
  if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
@@ -102,26 +118,29 @@ static int16_t ui_hit_test(int16_t tx, int16_t ty) {
102
118
  // a tall wrapped paragraph can overflow below the fold yet still have a
103
119
  // tappable link segment in its visible portion.
104
120
  if (ui_is_point_clipped_by_scroll(static_cast<uint16_t>(i), tx, ty)) continue;
105
- // Skip nodes without any click handler they're containers, not targets.
106
- // Exceptions: NODE_RANGE (horizontal drag), NODE_INPUT (opens keyboard),
107
- // NODE_LIST (virtualized item tap), and NODE_BUTTON — the last so a button
108
- // gets :pressed/transition visual feedback even with no JS onClick wired
109
- // (a pure-CSS button like a demo "tap to transition" control).
110
- if (__ui_nodes[i].kind == NODE_RANGE || __ui_nodes[i].kind == NODE_INPUT || __ui_nodes[i].kind == NODE_LIST || __ui_nodes[i].kind == NODE_BUTTON || __ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
111
- if (best < 0 || ui_node_draws_before(best, i)) best = i;
112
- continue;
113
- }
114
- if (i < __ui_click_handler_count &&
115
- (__ui_click_handlers[i] || __ui_hold_handlers[i] || __ui_release_handlers[i])) {
116
- if (best < 0 || ui_node_draws_before(best, i)) best = i;
117
- }
121
+ if (target < 0 || ui_node_draws_before(target, i)) target = static_cast<int16_t>(i);
118
122
  }
119
123
  }
120
- return best;
124
+ // Phase 2 — bubbling: walk from the target up through its ancestors.
125
+ // Interactive kinds match even with no JS handler wired — NODE_RANGE
126
+ // (horizontal drag), NODE_INPUT (opens the keyboard), NODE_LIST
127
+ // (virtualized item tap), and NODE_BUTTON/NODE_CHECK/NODE_RADIO so a
128
+ // pure-CSS control still gets :pressed/transition visual feedback.
129
+ int16_t n = target;
130
+ while (n >= 0) {
131
+ uint16_t u = static_cast<uint16_t>(n);
132
+ if (__ui_nodes[u].kind == NODE_RANGE || __ui_nodes[u].kind == NODE_INPUT || __ui_nodes[u].kind == NODE_LIST || __ui_nodes[u].kind == NODE_BUTTON || __ui_nodes[u].kind == NODE_CHECK || __ui_nodes[u].kind == NODE_RADIO) return n;
133
+ if ((u < __ui_click_handler_count && __ui_click_handlers[u]) ||
134
+ (u < __ui_hold_handler_count && __ui_hold_handlers[u]) ||
135
+ (u < __ui_release_handler_count && __ui_release_handlers[u])) return n;
136
+ if (__ui_nodes[u].parent == UI_NO_PARENT || __ui_nodes[u].parent >= __ui_node_count) break;
137
+ n = static_cast<int16_t>(__ui_nodes[u].parent);
138
+ }
139
+ return -1;
121
140
  }
122
141
 
123
142
  // Dispatch a handler from the given table if registered for the node.
124
- static void ui_dispatch(void (**table)(), uint16_t count, int16_t node) {
143
+ static void ui_dispatch(void (*const* table)(), uint16_t count, int16_t node) {
125
144
  if (node >= 0 && static_cast<uint16_t>(node) < count && table[node]) {
126
145
  table[node]();
127
146
  }
@@ -151,6 +170,13 @@ static inline void ui_open_keyboard_for_input(uint16_t nodeIdx) {
151
170
 
152
171
  // Touch down: called when screen is first touched.
153
172
  static void ui_touch_down(int16_t tx, int16_t ty) {
173
+ // Modal <select> list: record the touch position; tap-up routes to the
174
+ // option rows. No hit-test/press/scroll while the overlay is open.
175
+ if (__ui_select_menu >= 0) {
176
+ __ui_touch_state = 1;
177
+ __ui_touch_down_time = millis();
178
+ return;
179
+ }
154
180
  int16_t node = ui_hit_test(tx, ty);
155
181
  __ui_touch_node = node;
156
182
  __ui_touch_state = 1;
@@ -209,6 +235,35 @@ static void ui_touch_up() {
209
235
  // Modal keyboard: route tap-up to the keyboard; swallow normal click logic.
210
236
  // (Skipped on UI_HIDE_OSK desktop targets — touches pass through to the app,
211
237
  // since the editing session is driven by the real keyboard, not the grid.)
238
+ // Open drawers close on outside taps (device parity with the preview).
239
+ {
240
+ int16_t dx0 = __ui_last_touch_x;
241
+ int16_t dy0 = __ui_last_touch_y;
242
+ for (uint8_t s = 0; s < __ui_drawer_slots; s++) {
243
+ if (__ui_drawer_idx[s] < 0 || !__ui_drawer_open[s] || __ui_drawer_progress[s] < 255) continue;
244
+ uint16_t di = static_cast<uint16_t>(__ui_drawer_idx[s]);
245
+ // Hit-test the SUBTREE rect: a <dialog> root with absolutely
246
+ // positioned children lays out at h=0 — the root box made every tap
247
+ // count as "outside" and closed the dialog immediately.
248
+ UIRect panelRect;
249
+ if (!ui_subtree_current_paint_rect(di, &panelRect)) {
250
+ panelRect.x = ui_draw_x_for_node(di);
251
+ panelRect.y = ui_draw_y_for_node(di);
252
+ panelRect.w = __ui_nodes[di].box.w;
253
+ panelRect.h = __ui_nodes[di].box.h;
254
+ }
255
+ if (dx0 < panelRect.x || dx0 >= panelRect.x + panelRect.w ||
256
+ dy0 < panelRect.y || dy0 >= panelRect.y + panelRect.h) {
257
+ __ui_drawer_open[s] = 0;
258
+ }
259
+ }
260
+ }
261
+ if (__ui_select_menu >= 0) {
262
+ ui_select_menu_tap(__ui_last_touch_x, __ui_last_touch_y);
263
+ __ui_touch_state = 0;
264
+ __ui_last_touch_time = millis();
265
+ return;
266
+ }
212
267
  #if !defined(UI_HIDE_OSK)
213
268
  if (__ui_kb_visible) {
214
269
  ui_kb_handle_tap(__ui_last_touch_x, __ui_last_touch_y);
@@ -242,7 +297,7 @@ static void ui_touch_up() {
242
297
  if (target >= 0) ui_navigate(static_cast<uint8_t>(target));
243
298
  }
244
299
  }
245
- ui_dispatch(__ui_release_handlers, __ui_click_handler_count, __ui_touch_node);
300
+ ui_dispatch(__ui_release_handlers, __ui_release_handler_count, __ui_touch_node);
246
301
  // Built-in check/radio/select handlers mutate the node value during click
247
302
  // dispatch and need one repaint. Buttons are already dirty from releasing
248
303
  // :pressed; links, lists, inputs, and ordinary callbacks are either handled
@@ -390,7 +445,7 @@ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
390
445
  if (__ui_touch_state == 1 && __ui_touch_node >= 0 && !__ui_is_dragging) {
391
446
  if (now - __ui_touch_down_time >= UI_TOUCH_HOLD_MS) {
392
447
  __ui_touch_state = 2;
393
- ui_dispatch(__ui_hold_handlers, __ui_click_handler_count, __ui_touch_node);
448
+ ui_dispatch(__ui_hold_handlers, __ui_hold_handler_count, __ui_touch_node);
394
449
  }
395
450
  }
396
451
  }
@@ -2,98 +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
- #ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
61
- #define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
62
- #endif
63
- #ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
64
- #define UI_SCROLL_INPUT_TIER_RESISTIVE 0
65
- #endif
66
- #ifndef UI_SCROLL_INPUT_TIER_NONE
67
- #define UI_SCROLL_INPUT_TIER_NONE 0
68
- #endif
69
- #ifndef UI_SCROLL_RENDER_TIER_FULL
70
- #define UI_SCROLL_RENDER_TIER_FULL 1
71
- #endif
72
- #ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
73
- #define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
74
- #endif
75
- #if (UI_SCROLL_INPUT_TIER_CAPACITIVE + UI_SCROLL_INPUT_TIER_RESISTIVE + UI_SCROLL_INPUT_TIER_NONE) == 0
76
- #define UI_SCROLL_INPUT_TIER_RESISTIVE 1
77
- #endif
78
- #define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
79
- #define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
80
- // Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
81
- #ifndef UI_SCROLL_DEBUG
82
- #define UI_SCROLL_DEBUG 0
83
- #endif
84
- // Band renderer: height (px) of the horizontal band canvas used to composite
85
- // scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
86
- // budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
87
- // single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
88
- // fits within one band if a glyph straddles a band boundary, its anti-
89
- // aliased edge pixels blend toward the wrong bg in the adjacent band, visible
90
- // as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
91
- // that's ~30KB (fits SRAM on most ESP32 targets).
92
- #ifndef UI_STRIP_BAND_HEIGHT
93
- #define UI_STRIP_BAND_HEIGHT 32
94
- #endif
95
- #if defined(ESP32) || defined(ESP8266)
96
- #include <Esp.h>
97
- #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
98
115
  `;
99
116
  }
@@ -0,0 +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";