@typecad/ui 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
@@ -0,0 +1,429 @@
1
+ // ---------------------------------------------------------------------------
2
+ // UI module registry — cross-phase state for `.ui.html` modules.
3
+ //
4
+ // `.ui.html` is parsed once at resolution time (graph build), and its styled
5
+ // tree is stored here. Two later phases consume it independently:
6
+ // - call lowering (ui.mount) → lowerOnMount() does final layout+lower using
7
+ // the mount's viewport, and marks the entry file as having a UI.
8
+ // - emit injection (cpp-emitter) → reads lowered tables + the entry-has-UI
9
+ // flag to inject the runtime header + tables into the entry TU.
10
+ //
11
+ // Mirrors the module-scoped Map pattern of halInstances / halClassRegistry.
12
+ //
13
+ // Design choice: final layout is deferred to mount time because the viewport
14
+ // comes from ui.mount's display context, not from the .ui.html itself. So
15
+ // loadUIModule stores the styled tree; lowerOnMount produces the C++ tables.
16
+ // ---------------------------------------------------------------------------
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { parseHtmlWithKeyboards, extractStyleBlocks } from "./html-parser.js";
20
+ import { getThemeCss } from "@typecad/cuttlefish/stores/theme-store";
21
+ import { parseCss, parseFontFaces, parseKeyframes } from "./css-parser.js";
22
+ import { resolveStyles } from "./style-resolver.js";
23
+ import { buildKeyframeSets } from "./keyframes.js";
24
+ import { selectEngine } from "./select-engine.js";
25
+ import { measureWithFonts } from "./layout-engine.js";
26
+ import { lowerUIToCpp } from "./ui-lowering.js";
27
+ import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
28
+ import { resolveScrollConfig } from "@typecad/cuttlefish/api/shared";
29
+ import { buildUIFontAssets } from "./font-assets.js";
30
+ import { emitImageTables, loadImageAssets } from "./image-assets.js";
31
+ import { splitUiFile } from "./ui-file-splitter.js";
32
+ const modules = new Map();
33
+ const lowered = new Map();
34
+ let entryHasUIFlag = false;
35
+ /** Reset the registry. Called at the start of each transpile run. */
36
+ export function resetUIRegistry() {
37
+ modules.clear();
38
+ lowered.clear();
39
+ entryHasUIFlag = false;
40
+ }
41
+ /** Load a `.ui.html` module: parse HTML + sibling `.ui.css`, resolve styles, cache. */
42
+ export function loadUIModule(htmlPath) {
43
+ const abs = path.resolve(htmlPath);
44
+ const cached = modules.get(abs);
45
+ if (cached)
46
+ return cached;
47
+ if (!fs.existsSync(abs)) {
48
+ throw new Error(`UI module not found: ${abs}`);
49
+ }
50
+ const htmlText = fs.readFileSync(abs, "utf-8");
51
+ // CSS path: use theme override if set, else the default sibling .ui.css.
52
+ const themeOverride = getThemeCss();
53
+ const cssPath = themeOverride
54
+ ? (path.isAbsolute(themeOverride) ? themeOverride : path.resolve(path.dirname(abs), themeOverride))
55
+ : abs.replace(/\.ui\.html$/, ".ui.css");
56
+ const cssText = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf-8") : "";
57
+ return loadUIModuleFromText(abs, htmlText, cssText, cssPath);
58
+ }
59
+ /**
60
+ * Load a UI module from already-read HTML + CSS text (used by the .ui single-file
61
+ * component path, which splits the file and passes the parts directly). The
62
+ * disk-reading `loadUIModule` delegates here after reading the files.
63
+ */
64
+ export function loadUIModuleFromText(htmlPath, htmlText, cssText, cssPathForFonts = path.dirname(htmlPath)) {
65
+ const abs = path.resolve(htmlPath);
66
+ const cached = modules.get(abs);
67
+ if (cached)
68
+ return cached;
69
+ const moduleDiagnostics = [];
70
+ const parsed = parseHtmlWithKeyboards(htmlText, moduleDiagnostics);
71
+ const tree = parsed.tree;
72
+ const allScreens = parsed.screens;
73
+ const keyboards = parsed.keyboards;
74
+ // Merge <style> blocks from the HTML with the external CSS.
75
+ const styleBlocks = extractStyleBlocks(htmlText);
76
+ const fullCss = cssText + "\n" + styleBlocks;
77
+ const rules = parseCss(fullCss, moduleDiagnostics);
78
+ const fontFaces = parseFontFaces(fullCss);
79
+ const styled = resolveStyles(tree, rules, moduleDiagnostics);
80
+ const allStyledScreens = allScreens.map(s => resolveStyles(s, rules, moduleDiagnostics));
81
+ const fontRoot = { tag: "screen", classes: [], style: {}, children: allStyledScreens };
82
+ const fontAssets = buildUIFontAssets(fontRoot, fontFaces, path.dirname(cssPathForFonts));
83
+ const rawKeyframes = parseKeyframes(fullCss);
84
+ const typeInfo = uiTypeDeclInfoForHtmlPath(abs);
85
+ const mod = {
86
+ htmlPath: abs,
87
+ typeDeclPath: typeInfo.dtsPath,
88
+ typeDeclSourceRoot: typeInfo.sourceRoot,
89
+ typeDeclRoot: typeInfo.typesRoot,
90
+ styled,
91
+ allStyledScreens,
92
+ keyboards,
93
+ rules,
94
+ fontFaces,
95
+ fontAssets,
96
+ rawKeyframes,
97
+ diagnostics: moduleDiagnostics,
98
+ mountDiagnostics: [],
99
+ };
100
+ modules.set(abs, mod);
101
+ // Write a generated type declaration so editors and the type-checker see the
102
+ // imported `screen` symbol with precise per-id typing.
103
+ writeTypeDecl(abs, allStyledScreens);
104
+ return mod;
105
+ }
106
+ /** Count a styled subtree's nodes (pre-order). */
107
+ function countNodes(node) {
108
+ let n = 1;
109
+ for (const c of node.children)
110
+ n += countNodes(c);
111
+ return n;
112
+ }
113
+ /** Human-referenceable label for a node: #id when present, else <tag "text">
114
+ * when it has text, else <tag>. Authors can grep for this in their source
115
+ * (a bare node index is meaningless). */
116
+ function nodeLabel(node) {
117
+ if (node.id)
118
+ return `#${node.id}`;
119
+ const text = (node.text ?? "").trim();
120
+ if (text)
121
+ return `<${node.tag} "${text.slice(0, 20)}">`;
122
+ return `<${node.tag}>`;
123
+ }
124
+ /** Produce (or return cached) the lowered C++ tables for a module, using the mount viewport. */
125
+ export function lowerOnMount(htmlPath, opts) {
126
+ const abs = path.resolve(htmlPath);
127
+ const cached = lowered.get(abs);
128
+ if (cached)
129
+ return cached;
130
+ const mod = modules.get(abs);
131
+ if (!mod)
132
+ throw new Error(`Cannot lower unregistered UI module: ${abs}`);
133
+ const htmlBase = path.basename(mod.htmlPath);
134
+ const viewport = { x: 0, y: 0, w: opts.viewport.width, h: opts.viewport.height };
135
+ // Layout all screens (each gets its own Yoga layout pass; boxes concatenated).
136
+ let allBoxes = [];
137
+ let allStyled = [];
138
+ for (const screen of mod.allStyledScreens.length > 0 ? mod.allStyledScreens : [mod.styled]) {
139
+ const engine = selectEngine(screen);
140
+ const screenBoxes = engine.arrange(screen, viewport, measureWithFonts(mod.fontAssets));
141
+ allBoxes = allBoxes.concat(screenBoxes);
142
+ allStyled.push(screen);
143
+ }
144
+ // Resolve @keyframes from the module's parsed keyframe sets.
145
+ const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
146
+ // Load image assets before lowering so imgDataId can be set.
147
+ const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
148
+ const scrollBudget = resolveScrollConfig(getDisplayProfile(), { buildTarget: getDisplayProfile()._buildTarget }).scrollCanvasBudgetBytes;
149
+ const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, scrollBudget);
150
+ for (const d of result.scrollMemoryDiagnostics) {
151
+ mod.mountDiagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
152
+ }
153
+ // Layout diagnostics: viewport-overflow (node bottom past the viewport) and
154
+ // text-overflow (node right edge past its parent). Both reference the node by
155
+ // a human-label (#id / <tag "text"> / <tag>) so the author can find it in
156
+ // their source — a bare node index is meaningless. Boxes are pre-order DFS,
157
+ // matching the styled tree walk.
158
+ const layoutDiags = [];
159
+ function walkLayoutDiags(node, nodeBox, parentBox, idx, insideScroll) {
160
+ // A scroll container's content is INTENTIONALLY taller than the viewport —
161
+ // that's the point of scroll. Suppress the viewport-overflow check for the
162
+ // scroll container itself AND its descendants: the container's box is the
163
+ // viewport-sized window, and its children sit below the fold by design.
164
+ // The check still fires for genuine non-scroll clipping elsewhere.
165
+ const isScrollContainer = node.style.overflow === "scroll" || node.style.overflow === "hidden";
166
+ const suppressViewportOverflow = insideScroll || isScrollContainer;
167
+ if (nodeBox) {
168
+ // Viewport-overflow: bottom past the viewport (silently clipped — no
169
+ // scroll on a non-scroll container).
170
+ if (!suppressViewportOverflow && nodeBox.h > 0) {
171
+ const bottom = nodeBox.y + nodeBox.h;
172
+ if (bottom > opts.viewport.height + 1) { // +1px tolerance
173
+ layoutDiags.push({
174
+ severity: "warning",
175
+ code: "layout-viewport-overflow",
176
+ message: `${nodeLabel(node)} bottom at y=${bottom} exceeds the ${opts.viewport.height}px viewport by ${bottom - opts.viewport.height}px (clipped off-screen).`,
177
+ hint: `Reduce content height, tighten padding/gap, or add overflow:scroll to a container.`,
178
+ source: htmlBase,
179
+ });
180
+ }
181
+ }
182
+ // Text-overflow: a text/button node whose right edge extends past its
183
+ // parent's content right edge (sibling-sum overflow).
184
+ if (parentBox && (node.tag === "text" || node.tag === "button")) {
185
+ const nodeRight = nodeBox.x + nodeBox.w;
186
+ const parentContentRight = parentBox.x + parentBox.w;
187
+ if (nodeRight > parentContentRight + 1) { // +1px tolerance
188
+ layoutDiags.push({
189
+ severity: "warning",
190
+ code: "layout-text-overflow",
191
+ message: `${nodeLabel(node)} right edge at x=${nodeRight} extends past its parent's right edge at x=${parentContentRight} by ${Math.round(nodeRight - parentContentRight)}px.`,
192
+ hint: `Shorten the text, use white-space:nowrap, or widen the parent.`,
193
+ source: htmlBase,
194
+ });
195
+ }
196
+ }
197
+ }
198
+ for (const child of node.children) {
199
+ const childBox = allBoxes[idx.i];
200
+ idx.i++;
201
+ walkLayoutDiags(child, childBox, nodeBox, idx, isScrollContainer || insideScroll);
202
+ }
203
+ }
204
+ let boxIdx = 1; // box 0 is the screen itself
205
+ for (const screen of allStyled) {
206
+ walkLayoutDiags(screen, allBoxes[boxIdx - 1], undefined, { i: boxIdx }, false);
207
+ boxIdx += countNodes(screen);
208
+ }
209
+ for (const d of layoutDiags) {
210
+ result.diagnostics.push(d);
211
+ mod.mountDiagnostics.push(d);
212
+ }
213
+ // Emit image tables.
214
+ result.imageTables = emitImageTables(imageAssets.assets, opts.colorFormat);
215
+ lowered.set(abs, result);
216
+ return result;
217
+ }
218
+ export function getUIModule(htmlPath) {
219
+ return modules.get(path.resolve(htmlPath));
220
+ }
221
+ export function getLoweredUIModule(htmlPath) {
222
+ return lowered.get(path.resolve(htmlPath));
223
+ }
224
+ export function hasUIModule(htmlPath) {
225
+ return modules.has(path.resolve(htmlPath));
226
+ }
227
+ /** All loaded UI modules (used by the emitter to inject every mounted tree). */
228
+ export function allUIModules() {
229
+ return [...modules.values()];
230
+ }
231
+ /** All lowered UI modules (those whose ui.mount has been processed). */
232
+ export function allLoweredUIModules() {
233
+ return [...lowered.entries()].map(([htmlPath, l]) => ({ htmlPath, lowered: l }));
234
+ }
235
+ // ── Entry-has-UI flag (gates runtime header + table injection) ──────────────
236
+ export function markEntryHasUI() {
237
+ entryHasUIFlag = true;
238
+ }
239
+ export function entryHasUI() {
240
+ return entryHasUIFlag;
241
+ }
242
+ export function clearEntryHasUI() {
243
+ entryHasUIFlag = false;
244
+ }
245
+ // ── Type-declaration sibling (.ui.d.html.ts) ────────────────────────────────
246
+ function findCuttlefishProjectRoot(filePath) {
247
+ let dir = path.dirname(path.resolve(filePath));
248
+ while (true) {
249
+ if (fs.existsSync(path.join(dir, "cuttlefish.config.ts")) ||
250
+ fs.existsSync(path.join(dir, "cuttlefish.config.js")) ||
251
+ fs.existsSync(path.join(dir, "cuttlefish.config.mjs")) ||
252
+ fs.existsSync(path.join(dir, "cuttlefish.config.cjs"))) {
253
+ return dir;
254
+ }
255
+ const parent = path.dirname(dir);
256
+ if (parent === dir)
257
+ return path.dirname(path.resolve(filePath));
258
+ dir = parent;
259
+ }
260
+ }
261
+ function isInsidePath(child, parent) {
262
+ const rel = path.relative(parent, child);
263
+ return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
264
+ }
265
+ function walkUIFiles(dir, out) {
266
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
267
+ return;
268
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
269
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "out" || entry.name === "types") {
270
+ continue;
271
+ }
272
+ const abs = path.join(dir, entry.name);
273
+ if (entry.isDirectory()) {
274
+ walkUIFiles(abs, out);
275
+ continue;
276
+ }
277
+ if (!entry.isFile())
278
+ continue;
279
+ const lower = entry.name.toLowerCase();
280
+ if (lower.endsWith(".ui") || lower.endsWith(".ui.html")) {
281
+ out.push(abs);
282
+ }
283
+ }
284
+ }
285
+ export function uiTypeDeclInfoForHtmlPath(htmlPath) {
286
+ const abs = path.resolve(htmlPath);
287
+ const projectRoot = findCuttlefishProjectRoot(abs);
288
+ const srcRoot = path.join(projectRoot, "src");
289
+ const sourceRoot = isInsidePath(abs, srcRoot) ? srcRoot : projectRoot;
290
+ const rel = path.relative(sourceRoot, abs);
291
+ const safeRel = rel && !rel.startsWith("..") && !path.isAbsolute(rel)
292
+ ? rel
293
+ : path.basename(abs);
294
+ const dtsRel = safeRel.replace(/\.ui\.html$/i, ".ui.d.html.ts");
295
+ const typesRoot = path.join(projectRoot, "types");
296
+ return {
297
+ dtsPath: path.join(typesRoot, dtsRel),
298
+ sourceRoot,
299
+ typesRoot,
300
+ };
301
+ }
302
+ export function writeUITypeDeclarationFromHtmlText(htmlPath, htmlText) {
303
+ const parsed = parseHtmlWithKeyboards(htmlText, []);
304
+ writeTypeDecl(htmlPath, parsed.screens);
305
+ return uiTypeDeclInfoForHtmlPath(htmlPath).dtsPath;
306
+ }
307
+ export function generateProjectUITypeDeclarations(projectRoot) {
308
+ const root = path.resolve(projectRoot);
309
+ const srcRoot = path.join(root, "src");
310
+ const files = [];
311
+ walkUIFiles(srcRoot, files);
312
+ const written = [];
313
+ const errors = [];
314
+ for (const filePath of files) {
315
+ try {
316
+ const source = fs.readFileSync(filePath, "utf-8");
317
+ if (filePath.toLowerCase().endsWith(".ui")) {
318
+ const parts = splitUiFile(source);
319
+ written.push(writeUITypeDeclarationFromHtmlText(filePath + ".html", parts.html));
320
+ }
321
+ else {
322
+ written.push(writeUITypeDeclarationFromHtmlText(filePath, source));
323
+ }
324
+ }
325
+ catch (error) {
326
+ errors.push({
327
+ filePath,
328
+ error: error instanceof Error ? error : new Error(String(error)),
329
+ });
330
+ }
331
+ }
332
+ return { written, errors };
333
+ }
334
+ function uiElementTypeForTag(tag) {
335
+ switch (tag) {
336
+ case "button": return "ButtonElement";
337
+ case "view":
338
+ case "screen": return "ViewElement";
339
+ case "check": return "CheckElement";
340
+ case "select": return "SelectElement";
341
+ case "radio": return "RadioElement";
342
+ case "progress": return "ProgressElement";
343
+ case "range": return "RangeElement";
344
+ case "input": return "InputElement";
345
+ case "canvas": return "CanvasElement";
346
+ default: return "TextElement";
347
+ }
348
+ }
349
+ function writeTypeDecl(htmlPath, styled) {
350
+ // Node16 module resolution with `allowArbitraryExtensions` types a non-JS
351
+ // module `<base>.<ext>` (here `app.ui.html`) via a sibling named
352
+ // `<base>.d.<ext>.ts` (here `app.ui.d.html.ts`). The older `.ui.html.d.ts`
353
+ // name is rejected by Node16 regardless of host hooks — the declaration file
354
+ // MUST follow the `<base>.d.<ext>.ts` convention.
355
+ const { dtsPath } = uiTypeDeclInfoForHtmlPath(htmlPath);
356
+ const roots = Array.isArray(styled) ? styled : [styled];
357
+ // Flat handles (backward-compatible): every element's ref ?? id at top level.
358
+ const flatIds = new Map();
359
+ const collectFlat = (n) => {
360
+ const handle = n.ref ?? n.id;
361
+ if (handle && !flatIds.has(handle))
362
+ flatIds.set(handle, n.tag);
363
+ n.children.forEach(collectFlat);
364
+ };
365
+ roots.forEach(collectFlat);
366
+ // Grouped handles: per-screen namespace (screen.groups.<screenId>.<handle>).
367
+ // Only screens with an id produce a group. Relieves naming pressure — two
368
+ // screens can both have id="btn" → screen.groups.home.btn / .forms.btn.
369
+ const groups = new Map(); // screenId → (handle → tag)
370
+ for (const root of roots) {
371
+ const screenId = root.id;
372
+ if (!screenId)
373
+ continue;
374
+ const group = new Map();
375
+ const collectGroup = (n) => {
376
+ // Don't include the screen root itself in its own group.
377
+ if (n !== root) {
378
+ const handle = n.ref ?? n.id;
379
+ if (handle && !group.has(handle))
380
+ group.set(handle, n.tag);
381
+ }
382
+ n.children.forEach(collectGroup);
383
+ };
384
+ collectGroup(root);
385
+ if (group.size > 0)
386
+ groups.set(screenId, group);
387
+ }
388
+ const flatFields = [...flatIds.entries()]
389
+ .map(([id, tag]) => ` ${id}: ${uiElementTypeForTag(tag)};`)
390
+ .join("\n");
391
+ const groupInterfaces = [];
392
+ const groupFields = [];
393
+ for (const [screenId, members] of groups) {
394
+ const ifaceName = `${screenId.charAt(0).toUpperCase()}${screenId.slice(1)}ScreenGroup`;
395
+ const memberFields = [...members.entries()]
396
+ .map(([id, tag]) => ` ${id}: ${uiElementTypeForTag(tag)};`)
397
+ .join("\n");
398
+ groupInterfaces.push(`interface ${ifaceName} {\n${memberFields}\n}`);
399
+ groupFields.push(` ${screenId}: ${ifaceName};`);
400
+ }
401
+ const groupsInterface = groupInterfaces.length > 0
402
+ ? [...groupInterfaces, `interface ScreenGroups {\n${groupFields.join("\n")}\n}`].join("\n")
403
+ : "";
404
+ const groupsField = groupInterfaces.length > 0 ? " groups: ScreenGroups;" : "";
405
+ const allFields = [flatFields, groupsField].filter(Boolean).join("\n");
406
+ const dts = [
407
+ `// Auto-generated by cuttlefish (UI lowering). Do not edit.`,
408
+ `import type { TextElement, ButtonElement, ViewElement, CheckElement, SelectElement, RadioElement, ProgressElement, RangeElement, InputElement, CanvasElement } from "@typecad/ui";`,
409
+ groupsInterface,
410
+ `export interface ScreenTree {`,
411
+ allFields,
412
+ `}`,
413
+ `export const screen: ScreenTree;`,
414
+ ].filter(Boolean).join("\n");
415
+ fs.mkdirSync(path.dirname(dtsPath), { recursive: true });
416
+ fs.writeFileSync(dtsPath, dts, "utf-8");
417
+ const oldSiblingPath = path.resolve(htmlPath).replace(/\.ui\.html$/i, ".ui.d.html.ts");
418
+ if (oldSiblingPath !== dtsPath && fs.existsSync(oldSiblingPath)) {
419
+ try {
420
+ const existing = fs.readFileSync(oldSiblingPath, "utf-8");
421
+ if (existing.startsWith("// Auto-generated by cuttlefish")) {
422
+ fs.unlinkSync(oldSiblingPath);
423
+ }
424
+ }
425
+ catch {
426
+ // Best-effort cleanup only; stale generated siblings are harmless.
427
+ }
428
+ }
429
+ }
@@ -0,0 +1,12 @@
1
+ import { Box, IntrinsicSize, LayoutEngine } from "./layout-engine.js";
2
+ import { StyledNode } from "./style-resolver.js";
3
+ export declare class YogaLayoutEngine implements LayoutEngine {
4
+ readonly id: "flex";
5
+ arrange(root: StyledNode, viewport: Box, measureFn: (node: StyledNode, availableWidth?: number) => IntrinsicSize): Box[];
6
+ /** Recursively build a Yoga node tree from a StyledNode tree. */
7
+ private buildTree;
8
+ /** Walk the Yoga tree in pre-order DFS, extracting absolute {x,y,w,h} per node.
9
+ * Yoga returns positions relative to the parent — we accumulate parentX/Y
10
+ * to produce absolute coordinates. Must match the StyledNode tree's DFS. */
11
+ private extractBoxes;
12
+ }