@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
@@ -1,11 +1,70 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // UA (User-Agent) default stylesheet.
3
3
  // Applied before user CSS so elements have sensible built-in behavior without
4
- // requiring explicit CSS. User CSS overrides (cascade: last-wins).
5
- // Minimal and embedded-oriented, not a browser clone.
4
+ // requiring explicit CSS. Author CSS overrides it (cascade: the UA layer loses
5
+ // to any author rule; see style-resolver.ts).
6
+ //
7
+ // The sheet is display-anchored, like a browser's per-device-class defaults:
8
+ // the root type size (what a browser fixes at 16px on desktop) scales with the
9
+ // target display — 16px on large color panels, ~14px on 320×240 TFTs, ~12px on
10
+ // tiny panels — and every UA size derives from that root, so heading ratios
11
+ // and touch-target minimums stay web-familiar at every pixel budget.
12
+ // Monochrome displays additionally snap sizes to the stock font's buckets
13
+ // (only ~4 distinct rendered sizes exist there).
6
14
  // ---------------------------------------------------------------------------
7
15
  import { parseCss } from "./css-parser.js";
8
- const UA_CSS = `
16
+ import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
17
+ import { DEFAULT_FONT_FAMILY, DEFAULT_MONO_FAMILY, defaultFontApplies } from "./default-font.js";
18
+ /** Derive the UA type scale from the display class. Height is the portrait
19
+ * dimension the profile reports; an unbound profile gets the color/large
20
+ * default (root 16 — the classic browser default). */
21
+ export function uaScaleFor(height, mono) {
22
+ if (mono) {
23
+ // Stock font buckets: ≤12 → size 1, 13–20 → 2, 21–28 → 3. Snap UA sizes
24
+ // to 12/20/28 so every declared size renders at a DISTINCT bucket.
25
+ const tall = (height ?? 128) >= 96;
26
+ return {
27
+ root: 12,
28
+ mono: true,
29
+ h: tall ? [28, 20, 20, 12, 12, 12] : [20, 20, 12, 12, 12, 12],
30
+ blockMargin: 6,
31
+ controlMinHeight: 32,
32
+ buttonPadV: 6,
33
+ buttonPadH: 12,
34
+ };
35
+ }
36
+ const root = height === undefined ? 16 : height >= 300 ? 16 : height >= 150 ? 14 : 12;
37
+ const r = (m) => Math.round(root * m);
38
+ return {
39
+ root,
40
+ mono: false,
41
+ h: [r(1.5), r(1.25), r(1.125), r(1), r(0.875), r(0.75)],
42
+ blockMargin: r(0.5),
43
+ controlMinHeight: Math.max(32, Math.min(48, r(3))),
44
+ buttonPadV: r(0.625),
45
+ buttonPadH: r(1),
46
+ };
47
+ }
48
+ /** Read the active display profile (null when unbound — the default scale). */
49
+ function profileFor() {
50
+ try {
51
+ const p = getDisplayProfile();
52
+ return {
53
+ height: p.height,
54
+ mono: p.colorFormat === "mono",
55
+ colorFormat: p.colorFormat,
56
+ };
57
+ }
58
+ catch {
59
+ return { height: undefined, mono: false, colorFormat: undefined };
60
+ }
61
+ }
62
+ /** Build the UA stylesheet text for a scale. Sizes interpolate as concrete px
63
+ * so the CSS pipeline stays unit-simple. */
64
+ function uaCssFor(s, useDefaultFont) {
65
+ const gap = Math.round(s.root * 0.625);
66
+ const headerGap = Math.round(s.root * 0.5);
67
+ return `
9
68
  /* Browser-like light defaults: dark text on a near-white background.
10
69
  A dark theme overrides these via user CSS (e.g. screen { color: #f0f0f0;
11
70
  background: #1a1a1a }). Without these, screen fg defaults to white
@@ -14,7 +73,7 @@ screen {
14
73
  display: flex;
15
74
  flex-direction: column;
16
75
  color: #1a1a1a;
17
- background: #ffffff;
76
+ background: #ffffff;${useDefaultFont ? `\n /* Bundled default font: AA + continuous sizes out of the box. */\n font-family: "${DEFAULT_FONT_FAMILY}";` : ""}
18
77
  }
19
78
  body {
20
79
  display: flex;
@@ -27,40 +86,41 @@ view {
27
86
  flex-direction: column;
28
87
  }
29
88
  /* Scrollable content area — the standard pattern for multi-screen apps.
30
- align-items:center + gap give readable card-style layouts out of the box. */
31
- .scrollBody {
89
+ align-items:center + gap give readable card-style layouts out of the box.
90
+ .ui-scroll-body / .ui-screen-header are the framework-prefixed spellings. */
91
+ .scrollBody, .ui-scroll-body {
32
92
  overflow: scroll;
33
93
  flex: 1;
34
94
  align-items: center;
35
- gap: 10px;
95
+ gap: ${gap}px;
36
96
  }
37
97
  /* Header bar (back link + title). flex-direction:row is the expected layout;
38
98
  without it the back link and title stack vertically. */
39
- .screenHeader {
99
+ .screenHeader, .ui-screen-header {
40
100
  flex-direction: row;
41
101
  align-items: center;
42
- gap: 8px;
102
+ gap: ${headerGap}px;
43
103
  }
44
104
  button {
45
- padding: 10px 16px;
105
+ padding: ${s.buttonPadV}px ${s.buttonPadH}px;
46
106
  border: 1px solid;
47
- min-height: 48px;
107
+ min-height: ${s.controlMinHeight}px;
48
108
  text-align: center;
49
109
  }
50
110
  input {
51
- padding: 8px;
111
+ padding: ${Math.round(s.root * 0.5)}px;
52
112
  border: 1px solid;
53
113
  }
54
114
  select {
55
- padding: 10px 12px;
115
+ padding: ${s.buttonPadV}px ${s.buttonPadH}px;
56
116
  border: 1px solid;
57
- min-height: 48px;
117
+ min-height: ${s.controlMinHeight}px;
58
118
  }
59
119
  check, radio {
60
- min-height: 48px;
120
+ min-height: ${s.controlMinHeight}px;
61
121
  }
62
122
  range {
63
- height: 40px;
123
+ height: ${Math.round(s.root * 2.5)}px;
64
124
  }
65
125
  list {
66
126
  overflow: scroll;
@@ -72,17 +132,35 @@ a {
72
132
  label {
73
133
  text-align: left;
74
134
  }
75
- /* h3 is non-bold by default: the runtime bumps textSize for bold text
76
- (roughly doubling per-char advance), which wraps multi-word captions
77
- like "width / height / min / max" to 3 lines. Normal weight keeps
78
- them readable. Users can override with font-weight:bold if needed. */
79
- h1 { font-size: 24px; font-weight: bold; }
80
- h2 { font-size: 20px; font-weight: bold; }
81
- h3 { font-size: 18px; font-weight: normal; }
82
- h4 { font-size: 16px; font-weight: bold; }
83
- h5 { font-size: 14px; }
84
- h6 { font-size: 12px; }
85
- p { font-size: 16px; }
135
+ /* Headings and paragraphs carry browser-like margins so a bare heading stack
136
+ doesn't render glued together. Sizes derive from the display-anchored root
137
+ (see uaScaleFor). */
138
+ h1 { font-size: ${s.h[0]}px; font-weight: bold; margin: ${s.blockMargin}px 0; }
139
+ h2 { font-size: ${s.h[1]}px; font-weight: bold; margin: ${s.blockMargin}px 0; }
140
+ h3 { font-size: ${s.h[2]}px; font-weight: bold; margin: ${s.blockMargin}px 0; }
141
+ h4 { font-size: ${s.h[3]}px; font-weight: bold; margin: ${s.blockMargin}px 0; }
142
+ h5 { font-size: ${s.h[4]}px; margin: ${s.blockMargin}px 0; }
143
+ h6 { font-size: ${s.h[5]}px; margin: ${s.blockMargin}px 0; }
144
+ p { font-size: ${s.root}px; margin: ${s.blockMargin}px 0; }
145
+ small { font-size: ${Math.round(s.root * 0.8)}px; }
146
+ hr { height: 1px; background: #808080; margin: ${s.blockMargin}px 0; }
147
+ /* Definition lists: bold term, indented description — the settings-screen
148
+ idiom web authors reach for. */
149
+ dt { font-weight: bold; }
150
+ dd { margin-left: ${Math.round(s.root * 0.75)}px; }
151
+ /* Tables: the equal-width flex approximation — tr is a row, td/th are
152
+ stretched cells (flex:1 basis 0), th bold + centered like browsers.
153
+ No auto column sizing; set widths/flex-grow on cells for custom columns. */
154
+ table { gap: 1px; }
155
+ tr { flex-direction: row; align-items: stretch; gap: 1px; }
156
+ td { flex: 1; padding: ${Math.round(s.root * 0.3)}px; text-align: left; }
157
+ th { flex: 1; padding: ${Math.round(s.root * 0.3)}px; text-align: center; font-weight: bold; }
158
+ caption { text-align: center; margin: ${Math.round(s.root * 0.25)}px 0; }
159
+ /* Grouped form chrome. */
160
+ fieldset { border: 1px solid; padding: ${s.blockMargin}px; }
161
+ /* Preformatted text keeps its whitespace (engine white-space modes); the
162
+ monospace family rides on the bundled DejaVu Sans Mono when available. */
163
+ pre { white-space: pre; }${useDefaultFont ? `\npre, code, kbd, samp { font-family: "${DEFAULT_MONO_FAMILY}"; }` : ""}
86
164
  /* Keyboard structural defaults (the OSK is runtime-generated, not
87
165
  user-authored HTML). Border keeps keys visually separated. Colors
88
166
  are design choices left to user CSS. */
@@ -90,10 +168,19 @@ p { font-size: 16px; }
90
168
  border: 1px solid;
91
169
  }
92
170
  `;
93
- let UA_RULES = null;
94
- /** Get the parsed UA default rules. Lazily computed, cached. */
171
+ }
172
+ const cache = new Map();
173
+ /** Get the parsed UA default rules for the active display profile. Lazily
174
+ * computed, cached per scale (profile changes pick up a new scale key). */
95
175
  export function getUARules() {
96
- if (!UA_RULES)
97
- UA_RULES = parseCss(UA_CSS);
98
- return UA_RULES;
176
+ const profile = profileFor();
177
+ const scale = uaScaleFor(profile.height, profile.mono);
178
+ const useDefaultFont = defaultFontApplies(profile.colorFormat);
179
+ const key = `${scale.root}|${scale.mono}|${useDefaultFont}|${scale.h.join(",")}`;
180
+ let rules = cache.get(key);
181
+ if (!rules) {
182
+ rules = parseCss(uaCssFor(scale, useDefaultFont));
183
+ cache.set(key, rules);
184
+ }
185
+ return rules;
99
186
  }
@@ -13,6 +13,7 @@
13
13
  // ---------------------------------------------------------------------------
14
14
  import { lowerUIToModel } from "./model.js";
15
15
  import { resolveColor } from "./color.js";
16
+ import { defaultFontAttribution } from "./default-font.js";
16
17
  import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
17
18
  import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
18
19
  export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = []) {
@@ -51,6 +52,14 @@ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [],
51
52
  addIfNeeded(spec.type === "number" ? DEFAULT_NUMBER_KEYBOARD : DEFAULT_ALPHA_KEYBOARD);
52
53
  }
53
54
  }
55
+ // The runtime header's keyboard page-swap (the 123/ABC toggle key) calls
56
+ // BOTH default loaders regardless of which input opened the keyboard. Emit
57
+ // both whenever any input exists, or a text-only project fails to link
58
+ // (undefined __ui_kb_load_default_number on non-Arduino linkers).
59
+ if (inputSpecs.length > 0) {
60
+ addIfNeeded(DEFAULT_ALPHA_KEYBOARD);
61
+ addIfNeeded(DEFAULT_NUMBER_KEYBOARD);
62
+ }
54
63
  const keyboardLoaders = neededKeyboards
55
64
  .map(kb => emitKeyboardLoader(loaderNameForId(kb.id), kb, rules, colorFormat))
56
65
  .join("\n\n");
@@ -204,7 +213,12 @@ function emitFontTables(model) {
204
213
  }
205
214
  const lines = [];
206
215
  for (const asset of assets) {
207
- lines.push(`// Font ${asset.id}: ${asset.family} ${asset.px}px ${asset.fontWeight} ${asset.fontStyle} ${asset.subset}`);
216
+ lines.push(`// Font ${asset.id}: ${asset.family} ${asset.px}px ${asset.fontWeight} ${asset.fontStyle} ${asset.subset} (source: ${asset.sourcePath.split(/[\\/]/).pop()})`);
217
+ // Bundled DejaVu faces carry their license attribution into the binary
218
+ // (the Bitstream Vera License requires the notice accompany distributions).
219
+ const attribution = defaultFontAttribution(asset.sourcePath);
220
+ if (attribution)
221
+ lines.push(`// ${attribution}`);
208
222
  // Alpha data goes in PROGMEM (constants in flash, not RAM) - accessed via pgm_read_byte on AVR
209
223
  lines.push(`static const uint8_t __ui_font_${asset.id}_alpha[] PROGMEM = {`);
210
224
  lines.push(byteArray(asset.alpha));
@@ -231,6 +245,19 @@ function emitNodeTable(model) {
231
245
  const listBindingByNode = new Map();
232
246
  for (const lb of getListBindings())
233
247
  listBindingByNode.set(lb.nodeIndex, lb);
248
+ // <select> option tables: one fn per select feeding the node's optionTextFn
249
+ // (the modal option list reads options through it). Emitted before the node
250
+ // table so the initializers can reference the names.
251
+ const optionFnByNode = new Map();
252
+ const optionFns = [];
253
+ for (const n of model.nodes) {
254
+ if (n.kind !== "select" || !n.options || n.options.length === 0)
255
+ continue;
256
+ const fnName = `__ui_select_opts_${n.index}`;
257
+ optionFnByNode.set(n.index, fnName);
258
+ const cases = n.options.map((o, i) => ` if (idx == ${i}) { snprintf(buf, static_cast<size_t>(size), "%s", ${JSON.stringify(o.text)}); return; }`).join("\n");
259
+ optionFns.push(`static void ${fnName}(uint8_t idx, char* buf, uint8_t size) {\n${cases}\n buf[0] = 0;\n}`);
260
+ }
234
261
  // Accumulate rich-text run / segment / line parallel arrays across all
235
262
  // run-bearing nodes, assigning each a contiguous slice (runStart/richSegStart/
236
263
  // richLineStart + counts). Emitted as global tables after the node table.
@@ -285,9 +312,12 @@ function emitNodeTable(model) {
285
312
  const richSegStart = rr ? rr.richSegStart : 0;
286
313
  const richSegCount = rr ? rr.richSegCount : 0;
287
314
  const richLineStart = rr ? rr.richLineStart : 0;
288
- return ` { .box=${box}, .bg=${hex(n.bg)}, .fg=${hex(n.fg)}, .kind=${cppKind(n.kind)}, .text=${inputText}, .textBuffer={0}, .hasTextBinding=0, .font=${font}, .hasBg=${n.hasBg ? 1 : 0}, .textAlign=${n.textAlign}, .textSize=${n.textSize}, .lineHeight=${n.lineHeight}, .letterSpacing=${n.letterSpacing}, .fontAntialias=${n.fontAntialias ? 1 : 0}, .fontFace=${n.fontFace}, .borderColor=${hex(n.borderColor)}, .borderStyle=${n.borderStyle}, .borderWidth=${n.borderWidth}, .borderTopWidth=${n.borderTopWidth ?? n.borderWidth}, .borderRightWidth=${n.borderRightWidth ?? n.borderWidth}, .borderBottomWidth=${n.borderBottomWidth ?? n.borderWidth}, .borderLeftWidth=${n.borderLeftWidth ?? n.borderWidth}, .hasPerSideBorder=${((n.borderTopWidth ?? n.borderWidth) !== n.borderWidth || (n.borderRightWidth ?? n.borderWidth) !== n.borderWidth || (n.borderBottomWidth ?? n.borderWidth) !== n.borderWidth || (n.borderLeftWidth ?? n.borderWidth) !== n.borderWidth) ? 1 : 0}, .borderRadius=${n.borderRadius}, .paddingTop=${n.paddingTop ?? 0}, .paddingRight=${n.paddingRight ?? 0}, .paddingBottom=${n.paddingBottom ?? 0}, .paddingLeft=${n.paddingLeft ?? 0}, .gradientEnabled=${n.gradientEnabled}, .gradientColor1=${hex(n.gradientColor1)}, .gradientColor2=${hex(n.gradientColor2)}, .outlineColor=${hex(n.outlineColor)}, .outlineStyle=${n.outlineStyle}, .outlineWidth=${n.outlineWidth}, .zIndex=${n.zIndex}, .transformOffsetX=${n.transformOffsetX}, .transformOffsetY=${n.transformOffsetY}, .rotateDeg=${n.rotateDeg}, .pressedOffsetX=${n.pressedOffsetX}, .pressedOffsetY=${n.pressedOffsetY}, .shadowCount=${n.shadowCount}, .shadowOffsetX=${shArr(n.shadowOffsetX)}, .shadowOffsetY=${shArr(n.shadowOffsetY)}, .shadowBlur=${shArr(n.shadowBlur)}, .shadowColor={${n.shadowColor.slice(0, 4).map(hex).join(",")}}, .shadowAlpha=${shArr(n.shadowAlpha)}, .shadowInset=${shArr(n.shadowInset.map(v => v ? 1 : 0))}, .textShadowCount=${n.textShadowCount}, .textShadowOffsetX=${n.textShadowOffsetX}, .textShadowOffsetY=${n.textShadowOffsetY}, .textShadowBlur=${n.textShadowBlur}, .textShadowColor=${hex(n.textShadowColor)}, .textShadowAlpha=${n.textShadowAlpha}, .underline=${n.underline}, .textOverflow=${n.textOverflow ? 1 : 0}, .nowrap=${n.nowrap ? 1 : 0}, .whiteSpaceMode=${n.whiteSpaceMode}, .visible=${n.visible ? 1 : 0}, .opacity=${n.opacity}, .clearColor=${hex(n.clearColor)}, .lastTextWidth=${lastTextWidth}, .lastTextHeight=0, .layoutCacheKey=0, .layoutMetricsW=0, .layoutMetricsH=0, .scrollable=${n.scrollable ? 1 : 0}, .virtualized=${virtualized}, .scrollY=0, .contentHeight=${n.contentHeight}, .overscrollPx=0, .settling=0, .lastPaintedScrollY=0, .listCount=0, .listCountFn=${listCountFn}, .listItemFn=${listItemFn}, .listTapFn=${listTapFn}, .parent=${parent}, .subtreeEnd=${n.subtreeEnd}, .screenId=${n.screenId}, .imgDataId=${n.imgDataId ?? 255}, .objectFit=${n.objectFit ?? 1}, .listItemHeight=${n.listItemHeight ?? 0}, .rangeMin=${n.rangeMin}, .rangeMax=${n.rangeMax}, .maxlen=${n.maxlen}, .canvasW=${n.canvasW ?? 0}, .canvasH=${n.canvasH ?? 0}, .runCount=${runCount}, .richLineCount=${richLineCount}, .runStart=${runStart}, .richSegStart=${richSegStart}, .richSegCount=${richSegCount}, .richLineStart=${richLineStart}, .dirty=0, .value=${n.value} },`;
315
+ return ` { .box=${box}, .bg=${hex(n.bg)}, .fg=${hex(n.fg)}, .kind=${cppKind(n.kind)}, .text=${inputText}, .textBuffer={0}, .hasTextBinding=0, .font=${font}, .hasBg=${n.hasBg ? 1 : 0}, .textAlign=${n.textAlign}, .textSize=${n.textSize}, .lineHeight=${n.lineHeight}, .letterSpacing=${n.letterSpacing}, .fontAntialias=${n.fontAntialias ? 1 : 0}, .fontFace=${n.fontFace}, .borderColor=${hex(n.borderColor)}, .borderStyle=${n.borderStyle}, .borderWidth=${n.borderWidth}, .borderTopWidth=${n.borderTopWidth ?? n.borderWidth}, .borderRightWidth=${n.borderRightWidth ?? n.borderWidth}, .borderBottomWidth=${n.borderBottomWidth ?? n.borderWidth}, .borderLeftWidth=${n.borderLeftWidth ?? n.borderWidth}, .hasPerSideBorder=${((n.borderTopWidth ?? n.borderWidth) !== n.borderWidth || (n.borderRightWidth ?? n.borderWidth) !== n.borderWidth || (n.borderBottomWidth ?? n.borderWidth) !== n.borderWidth || (n.borderLeftWidth ?? n.borderWidth) !== n.borderWidth) ? 1 : 0}, .borderRadius=${Math.min(255, n.borderRadius)}, .paddingTop=${n.paddingTop ?? 0}, .paddingRight=${n.paddingRight ?? 0}, .paddingBottom=${n.paddingBottom ?? 0}, .paddingLeft=${n.paddingLeft ?? 0}, .gradientEnabled=${n.gradientEnabled}, .gradientColor1=${hex(n.gradientColor1)}, .gradientColor2=${hex(n.gradientColor2)}, .outlineColor=${hex(n.outlineColor)}, .outlineStyle=${n.outlineStyle}, .outlineWidth=${n.outlineWidth}, .zIndex=${n.zIndex}, .transformOffsetX=${n.transformOffsetX}, .transformOffsetY=${n.transformOffsetY}, .rotateDeg=${n.rotateDeg}, .pressedOffsetX=${n.pressedOffsetX}, .pressedOffsetY=${n.pressedOffsetY}, .shadowCount=${n.shadowCount}, .shadowOffsetX=${shArr(n.shadowOffsetX)}, .shadowOffsetY=${shArr(n.shadowOffsetY)}, .shadowBlur=${shArr(n.shadowBlur)}, .shadowColor={${n.shadowColor.slice(0, 4).map(hex).join(",")}}, .shadowAlpha=${shArr(n.shadowAlpha)}, .shadowInset=${shArr(n.shadowInset.map(v => v ? 1 : 0))}, .textShadowCount=${n.textShadowCount}, .textShadowOffsetX=${n.textShadowOffsetX}, .textShadowOffsetY=${n.textShadowOffsetY}, .textShadowBlur=${n.textShadowBlur}, .textShadowColor=${hex(n.textShadowColor)}, .textShadowAlpha=${n.textShadowAlpha}, .underline=${n.underline}, .textOverflow=${n.textOverflow ? 1 : 0}, .nowrap=${n.nowrap ? 1 : 0}, .whiteSpaceMode=${n.whiteSpaceMode}, .visible=${n.visible ? 1 : 0}, .opacity=${n.opacity}, .clearColor=${hex(n.clearColor)}, .lastTextWidth=${lastTextWidth}, .lastTextHeight=0, .layoutCacheKey=0, .layoutMetricsW=0, .layoutMetricsH=0, .scrollable=${n.scrollable ? 1 : 0}, .virtualized=${virtualized}, .scrollY=0, .contentHeight=${n.contentHeight}, .overscrollPx=0, .settling=0, .lastPaintedScrollY=0, .listCount=0, .listCountFn=${listCountFn}, .listItemFn=${listItemFn}, .listTapFn=${listTapFn}, .parent=${parent}, .subtreeEnd=${n.subtreeEnd}, .screenId=${n.screenId}, .imgDataId=${n.imgDataId ?? 255}, .objectFit=${n.objectFit ?? 1}, .listItemHeight=${n.listItemHeight ?? 0}, .rangeMin=${n.rangeMin}, .rangeMax=${n.rangeMax}, .maxlen=${n.maxlen}, .canvasW=${n.canvasW ?? 0}, .canvasH=${n.canvasH ?? 0}, .runCount=${runCount}, .richLineCount=${richLineCount}, .runStart=${runStart}, .richSegStart=${richSegStart}, .richSegCount=${richSegCount}, .richLineStart=${richLineStart}, .dirty=0, .value=${n.value}, .disabled=${n.disabled ? 1 : 0}, .optionCount=${n.kind === "select" && n.options ? n.options.length : 0}, .optionTextFn=${optionFnByNode.get(n.index) ?? "nullptr"}, .drawerSide=${n.drawerSide ?? -1}, .toastDuration=${n.toastDuration ?? 0}, .checkedBg=${n.checkedBg !== undefined && n.checkedBg >= 0 ? hex(n.checkedBg) : "0x0"}, .checkedFg=${n.checkedFg !== undefined && n.checkedFg >= 0 ? hex(n.checkedFg) : "0x0"}, .hasCheckedBg=${n.checkedBg !== undefined && n.checkedBg >= 0 ? 1 : 0}, .hasCheckedFg=${n.checkedFg !== undefined && n.checkedFg >= 0 ? 1 : 0}, .flowAxis=${n.flowAxis ?? 0}, .flowGap=${n.flowGap ?? 0}, .flowFlags=${n.flowFlags ?? 0} },`;
289
316
  });
290
317
  return [
318
+ // <select> option tables (must precede __ui_nodes[]: initializers
319
+ // reference the function names).
320
+ ...optionFns,
291
321
  // Mutable (not const) so ui_tick can update bg/dirty during transitions.
292
322
  // AVR would want PROGMEM + a shadow copy; ESP32-class has RAM to spare.
293
323
  `UINode __ui_nodes[] = {`,
@@ -310,7 +340,11 @@ function emitNodeTable(model) {
310
340
  ].join("\n");
311
341
  }
312
342
  function emitTransitionTable(model) {
313
- const entries = model.transitions.map((t) => {
343
+ // Skip no-op transitions (pressed target equals base) they'd never change
344
+ // the node's appearance and only bloat the per-tick scan.
345
+ const entries = model.transitions
346
+ .filter((t) => t.pressedTarget !== t.baseTarget)
347
+ .map((t) => {
314
348
  const prop = t.prop === "background" ? "PROP_BG" : "PROP_FG";
315
349
  return ` { .node=${t.node}, .prop=${prop}, .durationMs=${t.durationMs}, .pressedTarget=${hex(t.pressedTarget)}, .baseTarget=${hex(t.baseTarget)} },`;
316
350
  });
@@ -26,7 +26,11 @@ import { measureWithFonts } from "./layout-engine.js";
26
26
  import { lowerUIToCpp } from "./ui-lowering.js";
27
27
  import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
28
28
  import { buildUIFontAssets } from "./font-assets.js";
29
- import { emitImageTables, loadImageAssets } from "./image-assets.js";
29
+ import { injectDefaultFontFaces } from "./default-font.js";
30
+ import { cssCompatDiagnostics } from "./compat-report.js";
31
+ import { expandCssImports } from "./css-imports.js";
32
+ import { SHADCN_KIT_CSS } from "./shadcn-kit.js";
33
+ import { emitImageTables, loadImageAssets, applyDecodedImageSizes } from "./image-assets.js";
30
34
  import { splitUiFile } from "./ui-file-splitter.js";
31
35
  const modules = new Map();
32
36
  const lowered = new Map();
@@ -70,11 +74,23 @@ export function loadUIModuleFromText(htmlPath, htmlText, cssText, cssPathForFont
70
74
  const tree = parsed.tree;
71
75
  const allScreens = parsed.screens;
72
76
  const keyboards = parsed.keyboards;
73
- // Merge <style> blocks from the HTML with the external CSS.
77
+ // Merge <style> blocks from the HTML with the external CSS, then inline any
78
+ // local @import statements (shared stylesheets like the shadcn preset).
74
79
  const styleBlocks = extractStyleBlocks(htmlText);
75
- const fullCss = cssText + "\n" + styleBlocks;
80
+ const cssBaseDir = path.dirname(cssPathForFonts);
81
+ // Built-in shadcn kit first, user CSS after: user token blocks and recipe
82
+ // overrides win by cascade order. The kit has no @imports of its own.
83
+ const fullCss = SHADCN_KIT_CSS + "\n" + expandCssImports(cssText + "\n" + styleBlocks, cssBaseDir);
76
84
  const rules = parseCss(fullCss, moduleDiagnostics);
77
- const fontFaces = parseFontFaces(fullCss);
85
+ // Color displays get the bundled DejaVu faces appended (unless the author
86
+ // registered their own face under the default family) so AA text works out
87
+ // of the box. The UA sheet points at the same family.
88
+ let profileColorFormat;
89
+ try {
90
+ profileColorFormat = getDisplayProfile().colorFormat;
91
+ }
92
+ catch { /* unbound */ }
93
+ const fontFaces = injectDefaultFontFaces(parseFontFaces(fullCss), profileColorFormat);
78
94
  const styled = resolveStyles(tree, rules, moduleDiagnostics);
79
95
  const allStyledScreens = allScreens.map(s => resolveStyles(s, rules, moduleDiagnostics));
80
96
  const fontRoot = { tag: "screen", classes: [], style: {}, children: allStyledScreens };
@@ -134,6 +150,11 @@ export function lowerOnMount(htmlPath, opts) {
134
150
  // Layout all screens (each gets its own Yoga layout pass; boxes concatenated).
135
151
  let allBoxes = [];
136
152
  let allStyled = [];
153
+ // Converted images (<img src="*.png"> etc.) contribute their natural size
154
+ // to layout when the author gave no explicit width/height — must land
155
+ // BEFORE arrange. Served from the warm-up cache primed by the transpile
156
+ // graph collector / preview builder before this module loaded.
157
+ applyDecodedImageSizes(mod.allStyledScreens.length > 0 ? mod.allStyledScreens : [mod.styled], path.dirname(abs));
137
158
  for (const screen of mod.allStyledScreens.length > 0 ? mod.allStyledScreens : [mod.styled]) {
138
159
  const engine = selectEngine(screen);
139
160
  const screenBoxes = engine.arrange(screen, viewport, measureWithFonts(mod.fontAssets));
@@ -205,6 +226,13 @@ export function lowerOnMount(htmlPath, opts) {
205
226
  result.diagnostics.push(d);
206
227
  mod.mountDiagnostics.push(d);
207
228
  }
229
+ // CSS compatibility report: display-anchored scale summary, ignored alpha,
230
+ // stock-font quantization, viewport-share sanity. Ordinary diagnostics —
231
+ // printed with the rest, upgradeable to errors via --strict-css.
232
+ for (const d of cssCompatDiagnostics(allStyled.length > 0 ? allStyled : [mod.styled], mod.fontAssets, opts.viewport, opts.colorFormat, htmlBase)) {
233
+ result.diagnostics.push(d);
234
+ mod.mountDiagnostics.push(d);
235
+ }
208
236
  // Emit image tables.
209
237
  result.imageTables = emitImageTables(imageAssets.assets, opts.colorFormat);
210
238
  lowered.set(abs, result);
@@ -10,11 +10,13 @@
10
10
  // matching the StyledNode tree, so lowerUIToCpp's flatten() can index
11
11
  // boxes[] in lockstep with the styled nodes.
12
12
  // ---------------------------------------------------------------------------
13
- import { isDisplayNone, parseAspectRatio } from "./layout-engine.js";
13
+ import { isDisplayNone, parseAspectRatio, cssDimValue, cssSizeValue, dimSet, expandBoxShorthand } from "./layout-engine.js";
14
14
  import Yoga from "yoga-layout";
15
15
  /** Parse the first numeric value from a CSS string ("8px", "8px 16px" → 8). */
16
16
  /** Parse a single CSS length to device pixels.
17
- * rem/em × 16 (root font size), px/bare as-is, decimals supported. */
17
+ * rem/em × 16 (root font size), px/bare as-is, decimals supported.
18
+ * Percentages and "auto" fold to numbers here — use cssDimValue() for the
19
+ * properties (sizes, margins, paddings, insets) where they are meaningful. */
18
20
  function cssLength(val) {
19
21
  const v = val.trim();
20
22
  const remM = /^(-?[\d.]+)rem$/.exec(v);
@@ -31,22 +33,6 @@ function cssNum(val) {
31
33
  return 0;
32
34
  return cssLength(val);
33
35
  }
34
- /** Parse horizontal padding from shorthand ("8px 16px" → 16 for left/right). */
35
- function cssPadH(val) {
36
- if (!val)
37
- return 0;
38
- const parts = val.trim().split(/\s+/);
39
- if (parts.length <= 1)
40
- return cssLength(parts[0] ?? "0");
41
- return cssLength(parts[1] ?? parts[0]);
42
- }
43
- /** Parse vertical padding from shorthand ("8px 16px" → 8 for top/bottom). */
44
- function cssPadV(val) {
45
- if (!val)
46
- return 0;
47
- const parts = val.trim().split(/\s+/);
48
- return cssLength(parts[0] ?? "0");
49
- }
50
36
  /** Parse border width from "2px solid #808080" → 2. */
51
37
  function cssBorderWidth(val) {
52
38
  if (!val)
@@ -92,28 +78,43 @@ export class YogaLayoutEngine {
92
78
  yn.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN_REVERSE);
93
79
  else
94
80
  yn.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
95
- // Padding (all edges from shorthand, using the vertical value for uniformity)
96
- const padV = cssPadV(s.padding);
97
- const padH = cssPadH(s.padding);
98
- if (padV || padH) {
99
- yn.setPadding(Yoga.EDGE_TOP, padV);
100
- yn.setPadding(Yoga.EDGE_BOTTOM, padV);
101
- yn.setPadding(Yoga.EDGE_LEFT, padH);
102
- yn.setPadding(Yoga.EDGE_RIGHT, padH);
81
+ // Padding: full 1-4 value shorthand (TRBL rotation) with % support.
82
+ // "auto" is not valid padding — fold it to the default 0 via asSize.
83
+ const asSize = (d) => (d === "auto" ? 0 : d);
84
+ const [padT, padR, padB, padL] = expandBoxShorthand(s.padding);
85
+ if (dimSet(padT))
86
+ yn.setPadding(Yoga.EDGE_TOP, asSize(padT));
87
+ if (dimSet(padR))
88
+ yn.setPadding(Yoga.EDGE_RIGHT, asSize(padR));
89
+ if (dimSet(padB))
90
+ yn.setPadding(Yoga.EDGE_BOTTOM, asSize(padB));
91
+ if (dimSet(padL))
92
+ yn.setPadding(Yoga.EDGE_LEFT, asSize(padL));
93
+ // Scroll containers draw an overlay scrollbar at the right edge (4px
94
+ // strip: box.x + box.w - 4). Reserve the strip plus a 4px breathing gap
95
+ // as right padding so stretched children (align-items: stretch selects,
96
+ // cards) never lay out or paint against/under the scrollbar — browser
97
+ // behavior for classic scrollbars, which take layout space.
98
+ if (s.overflow === "scroll") {
99
+ const basePad = typeof padR === "number" ? padR : 0;
100
+ yn.setPadding(Yoga.EDGE_RIGHT, basePad + 8);
103
101
  }
104
- // Margin: per-side overrides win over the `margin` shorthand when set.
105
- const marginV = cssPadV(s.margin);
106
- const marginH = cssPadH(s.margin);
107
- const mTop = s.marginTop !== undefined ? cssLength(s.marginTop) : marginV;
108
- const mBottom = s.marginBottom !== undefined ? cssLength(s.marginBottom) : marginV;
109
- const mLeft = s.marginLeft !== undefined ? cssLength(s.marginLeft) : marginH;
110
- const mRight = s.marginRight !== undefined ? cssLength(s.marginRight) : marginH;
111
- if (mTop || mBottom || mLeft || mRight) {
102
+ // Margin: per-side longhands win over the `margin` shorthand when set.
103
+ // Full 1-4 value shorthand expansion; percentages resolve against the
104
+ // parent, and "auto" margins absorb free space (margin: 0 auto centers).
105
+ const [mgT, mgR, mgB, mgL] = expandBoxShorthand(s.margin);
106
+ const mTop = s.marginTop !== undefined ? cssDimValue(s.marginTop) : mgT;
107
+ const mBottom = s.marginBottom !== undefined ? cssDimValue(s.marginBottom) : mgB;
108
+ const mLeft = s.marginLeft !== undefined ? cssDimValue(s.marginLeft) : mgL;
109
+ const mRight = s.marginRight !== undefined ? cssDimValue(s.marginRight) : mgR;
110
+ if (dimSet(mTop))
112
111
  yn.setMargin(Yoga.EDGE_TOP, mTop);
112
+ if (dimSet(mBottom))
113
113
  yn.setMargin(Yoga.EDGE_BOTTOM, mBottom);
114
+ if (dimSet(mLeft))
114
115
  yn.setMargin(Yoga.EDGE_LEFT, mLeft);
116
+ if (dimSet(mRight))
115
117
  yn.setMargin(Yoga.EDGE_RIGHT, mRight);
116
- }
117
118
  // Gap: row-gap / column-gap are applied per-axis. Uniform `gap`
118
119
  // (both equal) uses GUTTER_ALL for efficiency; mismatched values
119
120
  // set GUTTER_ROW and GUTTER_COLUMN separately.
@@ -199,24 +200,23 @@ export class YogaLayoutEngine {
199
200
  if (s.flexShrink)
200
201
  yn.setFlexShrink(cssNum(s.flexShrink));
201
202
  if (s.flexBasis) {
202
- const basis = cssNum(s.flexBasis);
203
203
  if (s.flexBasis === "auto")
204
204
  yn.setFlexBasisAuto();
205
205
  else
206
- yn.setFlexBasis(basis);
206
+ yn.setFlexBasis(cssSizeValue(s.flexBasis));
207
207
  }
208
208
  // Order (Yoga may not expose setOrder in its types, but the runtime has it)
209
209
  if (s.order)
210
210
  yn.setOrder?.(cssNum(s.order));
211
- // Min/max dimensions
211
+ // Min/max dimensions (px or % of the parent)
212
212
  if (s.minWidth)
213
- yn.setMinWidth(cssNum(s.minWidth));
213
+ yn.setMinWidth(cssSizeValue(s.minWidth));
214
214
  if (s.maxWidth)
215
- yn.setMaxWidth(cssNum(s.maxWidth));
215
+ yn.setMaxWidth(cssSizeValue(s.maxWidth));
216
216
  if (s.minHeight)
217
- yn.setMinHeight(cssNum(s.minHeight));
217
+ yn.setMinHeight(cssSizeValue(s.minHeight));
218
218
  if (s.maxHeight)
219
- yn.setMaxHeight(cssNum(s.maxHeight));
219
+ yn.setMaxHeight(cssSizeValue(s.maxHeight));
220
220
  // Box sizing
221
221
  if (s.boxSizing === "border-box")
222
222
  yn.setBoxSizing?.(Yoga.BOX_SIZING_BORDER_BOX);
@@ -233,18 +233,18 @@ export class YogaLayoutEngine {
233
233
  else
234
234
  yn.setPositionType(Yoga.POSITION_TYPE_STATIC);
235
235
  if (s.top !== undefined)
236
- yn.setPosition(Yoga.EDGE_TOP, cssNum(s.top));
236
+ yn.setPosition(Yoga.EDGE_TOP, cssSizeValue(s.top));
237
237
  if (s.right !== undefined)
238
- yn.setPosition(Yoga.EDGE_RIGHT, cssNum(s.right));
238
+ yn.setPosition(Yoga.EDGE_RIGHT, cssSizeValue(s.right));
239
239
  if (s.bottom !== undefined)
240
- yn.setPosition(Yoga.EDGE_BOTTOM, cssNum(s.bottom));
240
+ yn.setPosition(Yoga.EDGE_BOTTOM, cssSizeValue(s.bottom));
241
241
  if (s.left !== undefined)
242
- yn.setPosition(Yoga.EDGE_LEFT, cssNum(s.left));
243
- // Width/height (explicit)
242
+ yn.setPosition(Yoga.EDGE_LEFT, cssSizeValue(s.left));
243
+ // Width/height (explicit; px or % of the parent)
244
244
  if (s.width)
245
- yn.setWidth(cssNum(s.width));
245
+ yn.setWidth(cssSizeValue(s.width));
246
246
  if (s.height)
247
- yn.setHeight(cssNum(s.height));
247
+ yn.setHeight(cssSizeValue(s.height));
248
248
  const aspectRatio = parseAspectRatio(s.aspectRatio);
249
249
  if (aspectRatio !== undefined)
250
250
  yn.setAspectRatio(aspectRatio);
@@ -271,7 +271,11 @@ export class YogaLayoutEngine {
271
271
  // Leaf nodes with text: let Yoga pass available width into measurement so
272
272
  // wrapped text can expand height under constraints.
273
273
  if (node.children.length === 0) {
274
- const textLike = node.tag === "text" || node.tag === "button" || node.tag === "check" || node.tag === "radio";
274
+ // select is text-like too: its label is the current option, and the
275
+ // generic else-branch sizes the width without the UA control padding —
276
+ // a wider option ("Gamma" vs "Alpha" in a proportional font) then
277
+ // overflows the box and wraps mid-word.
278
+ const textLike = node.tag === "text" || node.tag === "button" || node.tag === "check" || node.tag === "radio" || node.tag === "select";
275
279
  if (textLike) {
276
280
  yn.setMeasureFunc((width, widthMode) => {
277
281
  const hasWidth = widthMode !== Yoga.MEASURE_MODE_UNDEFINED && Number.isFinite(width) && width > 0;
@@ -284,8 +288,9 @@ export class YogaLayoutEngine {
284
288
  }
285
289
  else {
286
290
  const intrinsic = measureFn(node);
287
- const childPadV = cssPadV(s.padding);
288
- const childPadH = cssPadH(s.padding);
291
+ const [padTop, , , padLeft] = expandBoxShorthand(s.padding);
292
+ const childPadV = typeof padTop === "number" ? padTop : 0;
293
+ const childPadH = typeof padLeft === "number" ? padLeft : 0;
289
294
  const childBorder = cssBorderWidth(s.border);
290
295
  if (!s.width && intrinsic.w > 0) {
291
296
  yn.setWidth(Math.ceil(intrinsic.w + childPadH * 2 + childBorder * 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/ui",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "TypeCAD UI authoring library — HTML/CSS-driven graphics for microcontrollers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,9 @@
25
25
  "types": "./dist/ui-engine/*.d.ts",
26
26
  "default": "./dist/ui-engine/*.js"
27
27
  },
28
+ "./themes/*": {
29
+ "default": "./themes/*"
30
+ },
28
31
  "./preview/*": {
29
32
  "types": "./dist/preview/*.d.ts",
30
33
  "default": "./dist/preview/*.js"
@@ -37,14 +40,17 @@
37
40
  "sideEffects": false,
38
41
  "files": [
39
42
  "dist",
40
- "src"
43
+ "src",
44
+ "assets"
41
45
  ],
42
46
  "dependencies": {
43
- "@typecad/cuttlefish": "1.0.0-alpha.12",
47
+ "@typecad/cuttlefish": "1.0.0-alpha.13",
44
48
  "chalk": "^4.1.2",
45
49
  "css-tree": "^3.2.1",
50
+ "decode-ico": "^0.4.1",
46
51
  "linkedom": "^0.18.12",
47
52
  "opentype.js": "^2.0.0",
53
+ "sharp": "^0.35.3",
48
54
  "typescript": "^5.7.3",
49
55
  "yoga-layout": "^3.2.1"
50
56
  },