@typecad/ui 1.0.0-alpha.11 → 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 (135) hide show
  1. package/README.md +21 -1
  2. package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
  3. package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  4. package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
  5. package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
  6. package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
  7. package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
  8. package/assets/fonts/dejavu/LICENSE +187 -0
  9. package/assets/fonts/dejavu/README.md +49 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +74 -0
  12. package/dist/engine-index.js +2 -0
  13. package/dist/preview/build-program.d.ts +7 -1
  14. package/dist/preview/build-program.js +94 -15
  15. package/dist/preview/host-gfx.d.ts +3 -0
  16. package/dist/preview/host-gfx.js +30 -3
  17. package/dist/preview/host-ui-runtime.d.ts +115 -0
  18. package/dist/preview/host-ui-runtime.js +928 -46
  19. package/dist/ui-engine/block-layout.js +25 -11
  20. package/dist/ui-engine/color.d.ts +6 -0
  21. package/dist/ui-engine/color.js +80 -2
  22. package/dist/ui-engine/compat-report.d.ts +8 -0
  23. package/dist/ui-engine/compat-report.js +124 -0
  24. package/dist/ui-engine/css-imports.d.ts +4 -0
  25. package/dist/ui-engine/css-imports.js +70 -0
  26. package/dist/ui-engine/css-parser.js +58 -14
  27. package/dist/ui-engine/default-font.d.ts +25 -0
  28. package/dist/ui-engine/default-font.js +80 -0
  29. package/dist/ui-engine/font-assets.d.ts +2 -2
  30. package/dist/ui-engine/font-assets.js +22 -5
  31. package/dist/ui-engine/html-parser.d.ts +4 -0
  32. package/dist/ui-engine/html-parser.js +241 -26
  33. package/dist/ui-engine/image-assets.d.ts +8 -1
  34. package/dist/ui-engine/image-assets.js +35 -2
  35. package/dist/ui-engine/image-decode.d.ts +22 -0
  36. package/dist/ui-engine/image-decode.js +194 -0
  37. package/dist/ui-engine/inline-parser.js +1 -1
  38. package/dist/ui-engine/layout-engine.d.ts +19 -0
  39. package/dist/ui-engine/layout-engine.js +59 -3
  40. package/dist/ui-engine/model.d.ts +18 -0
  41. package/dist/ui-engine/model.js +76 -8
  42. package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
  43. package/dist/ui-engine/runtime-header/canvas-helpers.js +5 -2
  44. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
  45. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
  46. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
  47. package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
  48. package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
  49. package/dist/ui-engine/runtime-header/keyboard.js +682 -380
  50. package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1377
  51. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
  52. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
  53. package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
  54. package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
  55. package/dist/ui-engine/runtime-header/structs.js +228 -216
  56. package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
  57. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
  58. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
  59. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
  60. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +79 -23
  61. package/dist/ui-engine/runtime-header/types-defines.js +110 -93
  62. package/dist/ui-engine/shadcn-kit.d.ts +1 -0
  63. package/dist/ui-engine/shadcn-kit.js +502 -0
  64. package/dist/ui-engine/style-resolver.d.ts +4 -0
  65. package/dist/ui-engine/style-resolver.js +58 -6
  66. package/dist/ui-engine/transpile-ui.js +12 -5
  67. package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
  68. package/dist/ui-engine/ua-stylesheet.js +119 -32
  69. package/dist/ui-engine/ui-lowering.js +37 -3
  70. package/dist/ui-engine/ui-registry.js +32 -4
  71. package/dist/ui-engine/yoga-layout.js +57 -52
  72. package/dist/wizard/config-writer.d.ts +49 -0
  73. package/dist/wizard/config-writer.js +366 -0
  74. package/dist/wizard/display-catalog.d.ts +75 -0
  75. package/dist/wizard/display-catalog.js +207 -0
  76. package/dist/wizard/index.d.ts +7 -0
  77. package/dist/wizard/index.js +10 -0
  78. package/dist/wizard/integration-wizard.d.ts +15 -0
  79. package/dist/wizard/integration-wizard.js +486 -0
  80. package/dist/wizard/prompts.d.ts +32 -0
  81. package/dist/wizard/prompts.js +95 -0
  82. package/dist/wizard/starter-ui.d.ts +1 -0
  83. package/dist/wizard/starter-ui.js +56 -0
  84. package/package.json +18 -5
  85. package/src/cli.ts +87 -0
  86. package/src/engine-index.ts +53 -51
  87. package/src/preview/build-program.ts +810 -734
  88. package/src/preview/host-gfx.ts +30 -3
  89. package/src/preview/host-ui-runtime.ts +4133 -3289
  90. package/src/ui-engine/block-layout.ts +24 -11
  91. package/src/ui-engine/color.ts +77 -2
  92. package/src/ui-engine/compat-report.ts +139 -0
  93. package/src/ui-engine/css-imports.ts +69 -0
  94. package/src/ui-engine/css-parser.ts +64 -15
  95. package/src/ui-engine/default-font.ts +95 -0
  96. package/src/ui-engine/font-assets.ts +545 -525
  97. package/src/ui-engine/html-parser.ts +615 -397
  98. package/src/ui-engine/image-assets.ts +37 -2
  99. package/src/ui-engine/image-decode.ts +244 -0
  100. package/src/ui-engine/inline-parser.ts +1 -1
  101. package/src/ui-engine/layout-engine.ts +61 -3
  102. package/src/ui-engine/model.ts +1317 -1230
  103. package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
  104. package/src/ui-engine/runtime-header/canvas-helpers.ts +5 -2
  105. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
  106. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
  107. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
  108. package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
  109. package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
  110. package/src/ui-engine/runtime-header/keyboard.ts +689 -386
  111. package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1402
  112. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
  113. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
  114. package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
  115. package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
  116. package/src/ui-engine/runtime-header/structs.ts +234 -222
  117. package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
  118. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
  119. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
  120. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
  121. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +79 -23
  122. package/src/ui-engine/runtime-header/types-defines.ts +116 -99
  123. package/src/ui-engine/shadcn-kit.ts +503 -0
  124. package/src/ui-engine/style-resolver.ts +471 -416
  125. package/src/ui-engine/transpile-ui.ts +12 -5
  126. package/src/ui-engine/ua-stylesheet.ts +137 -31
  127. package/src/ui-engine/ui-lowering.ts +486 -452
  128. package/src/ui-engine/ui-registry.ts +556 -524
  129. package/src/ui-engine/yoga-layout.ts +307 -309
  130. package/src/wizard/config-writer.ts +404 -0
  131. package/src/wizard/display-catalog.ts +273 -0
  132. package/src/wizard/index.ts +38 -0
  133. package/src/wizard/integration-wizard.ts +619 -0
  134. package/src/wizard/prompts.ts +145 -0
  135. package/src/wizard/starter-ui.ts +58 -0
@@ -0,0 +1,366 @@
1
+ // ---------------------------------------------------------------------------
2
+ // cuttlefish.config.ts reader/writer for the @typecad/ui integration wizard.
3
+ //
4
+ // The build's config loader (packages/cuttlefish/src/config-loader.ts) is
5
+ // AST-based and deliberately never evaluates user code — only inline literals
6
+ // survive extraction. The writer here plays by the same rules: the wizard
7
+ // emits plain object literals and splices them into the config's default
8
+ // export object via the TypeScript AST, so every other section (and its
9
+ // comments) survives an update byte-for-byte.
10
+ // ---------------------------------------------------------------------------
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import ts from "typescript";
14
+ const CONFIG_FILENAME = "cuttlefish.config.ts";
15
+ /** Walk up from `startDir` looking for cuttlefish.config.ts. */
16
+ export function findCuttlefishConfig(startDir) {
17
+ let dir = path.resolve(startDir);
18
+ for (;;) {
19
+ const candidate = path.join(dir, CONFIG_FILENAME);
20
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
21
+ return candidate;
22
+ }
23
+ const parent = path.dirname(dir);
24
+ if (parent === dir)
25
+ return undefined;
26
+ dir = parent;
27
+ }
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // AST helpers (kept aligned with the config loader's extraction rules)
31
+ // ---------------------------------------------------------------------------
32
+ function unwrapTypeCast(node) {
33
+ let curr = node;
34
+ for (;;) {
35
+ if (ts.isAsExpression(curr) || ts.isTypeAssertionExpression(curr) || ts.isParenthesizedExpression(curr)) {
36
+ curr = curr.expression;
37
+ continue;
38
+ }
39
+ const isSatisfies = ts.isSatisfiesExpression;
40
+ if (typeof isSatisfies === "function" && isSatisfies(curr)) {
41
+ curr = curr.expression;
42
+ continue;
43
+ }
44
+ return curr;
45
+ }
46
+ }
47
+ function propertyKeyName(prop) {
48
+ const name = prop.name;
49
+ if (!name)
50
+ return undefined;
51
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
52
+ }
53
+ function getScalarValue(node) {
54
+ const unwrapped = unwrapTypeCast(node);
55
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
56
+ return unwrapped.text;
57
+ }
58
+ if (ts.isNumericLiteral(unwrapped)) {
59
+ return Number(unwrapped.text);
60
+ }
61
+ if (ts.isPrefixUnaryExpression(unwrapped)
62
+ && (unwrapped.operator === ts.SyntaxKind.MinusToken || unwrapped.operator === ts.SyntaxKind.PlusToken)
63
+ && ts.isNumericLiteral(unwrapped.operand)) {
64
+ const magnitude = Number(unwrapped.operand.text);
65
+ return unwrapped.operator === ts.SyntaxKind.MinusToken ? -magnitude : magnitude;
66
+ }
67
+ if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
68
+ return true;
69
+ if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
70
+ return false;
71
+ return undefined;
72
+ }
73
+ function getStringLiteral(node) {
74
+ const unwrapped = unwrapTypeCast(node);
75
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
76
+ return unwrapped.text;
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Find the config's default-export object literal. Accepts the same shapes as
82
+ * the build's loader:
83
+ * 1. `export default { ... }`
84
+ * 2. `const config: CuttlefishConfig = { ... }; export default config;`
85
+ * with `as` / `satisfies` / parenthesized wrappers unwrapped.
86
+ */
87
+ function findConfigObjectLiteral(sourceFile) {
88
+ const variableDecls = new Map();
89
+ let defaultExportName;
90
+ let inlineDefaultObject;
91
+ for (const stmt of sourceFile.statements) {
92
+ if (ts.isVariableStatement(stmt)) {
93
+ for (const decl of stmt.declarationList.declarations) {
94
+ if (ts.isIdentifier(decl.name)) {
95
+ variableDecls.set(decl.name.text, decl);
96
+ }
97
+ }
98
+ }
99
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
100
+ const expr = unwrapTypeCast(stmt.expression);
101
+ if (ts.isIdentifier(expr)) {
102
+ defaultExportName = expr.text;
103
+ }
104
+ else if (ts.isObjectLiteralExpression(expr)) {
105
+ inlineDefaultObject = expr;
106
+ }
107
+ }
108
+ }
109
+ if (inlineDefaultObject)
110
+ return inlineDefaultObject;
111
+ if (defaultExportName) {
112
+ const decl = variableDecls.get(defaultExportName);
113
+ if (decl?.initializer) {
114
+ const init = unwrapTypeCast(decl.initializer);
115
+ if (ts.isObjectLiteralExpression(init))
116
+ return init;
117
+ }
118
+ }
119
+ return undefined;
120
+ }
121
+ function parseSource(sourceText) {
122
+ return ts.createSourceFile(CONFIG_FILENAME, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
123
+ }
124
+ function objectLiteralToRecord(obj) {
125
+ const result = {};
126
+ for (const prop of obj.properties) {
127
+ if (!ts.isPropertyAssignment(prop))
128
+ continue;
129
+ const key = propertyKeyName(prop);
130
+ if (!key)
131
+ continue;
132
+ const init = unwrapTypeCast(prop.initializer);
133
+ if (ts.isObjectLiteralExpression(init)) {
134
+ result[key] = objectLiteralToRecord(init);
135
+ }
136
+ else if (ts.isArrayLiteralExpression(init)) {
137
+ const items = [];
138
+ for (const elem of init.elements) {
139
+ const s = getStringLiteral(elem);
140
+ if (s === undefined)
141
+ break;
142
+ items.push(s);
143
+ }
144
+ if (items.length === init.elements.length && items.length > 0)
145
+ result[key] = items;
146
+ }
147
+ else {
148
+ const scalar = getScalarValue(init);
149
+ if (scalar !== undefined)
150
+ result[key] = scalar;
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+ /**
156
+ * Read a top-level section of the config (e.g. `display`) as a plain record.
157
+ * Returns undefined when the section is missing or not an object literal.
158
+ */
159
+ export function readConfigSection(sourceText, section) {
160
+ const obj = findConfigObjectLiteral(parseSource(sourceText));
161
+ if (!obj)
162
+ return undefined;
163
+ for (const prop of obj.properties) {
164
+ if (!ts.isPropertyAssignment(prop))
165
+ continue;
166
+ if (propertyKeyName(prop) !== section)
167
+ continue;
168
+ const init = unwrapTypeCast(prop.initializer);
169
+ if (!ts.isObjectLiteralExpression(init))
170
+ return undefined;
171
+ return objectLiteralToRecord(init);
172
+ }
173
+ return undefined;
174
+ }
175
+ /** Read the top-level `entry` scalar (the .ui/.ts entry path). */
176
+ export function readEntryPath(sourceText) {
177
+ const obj = findConfigObjectLiteral(parseSource(sourceText));
178
+ if (!obj)
179
+ return undefined;
180
+ for (const prop of obj.properties) {
181
+ if (!ts.isPropertyAssignment(prop))
182
+ continue;
183
+ if (propertyKeyName(prop) !== "entry")
184
+ continue;
185
+ const value = getStringLiteral(unwrapTypeCast(prop.initializer));
186
+ return value;
187
+ }
188
+ return undefined;
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Rendering — build the `display` object-literal text
192
+ // ---------------------------------------------------------------------------
193
+ /** Keys whose numeric values read best in hex (I2C addresses). */
194
+ const HEX_KEYS = new Set(["address", "i2cAddress"]);
195
+ function escapeString(value) {
196
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
197
+ }
198
+ function isPlainRecord(value) {
199
+ return typeof value === "object" && value !== null && !Array.isArray(value);
200
+ }
201
+ function renderValue(key, value) {
202
+ if (typeof value === "number") {
203
+ if (HEX_KEYS.has(key) && Number.isInteger(value) && value >= 0) {
204
+ return `0x${value.toString(16)}`;
205
+ }
206
+ return String(value);
207
+ }
208
+ if (typeof value === "boolean")
209
+ return value ? "true" : "false";
210
+ if (typeof value === "string")
211
+ return `'${escapeString(value)}'`;
212
+ if (Array.isArray(value)) {
213
+ return `[${value.map((item) => renderValue(key, item)).join(", ")}]`;
214
+ }
215
+ if (isPlainRecord(value)) {
216
+ const inner = Object.entries(value)
217
+ .map(([nestedKey, nestedValue]) => `${nestedKey}: ${renderValue(nestedKey, nestedValue)}`)
218
+ .join(", ");
219
+ return `{ ${inner} }`;
220
+ }
221
+ throw new Error(`Cannot render display config value for key "${key}" (${typeof value}).`);
222
+ }
223
+ /**
224
+ * Render the display section as an object-literal body (braces included).
225
+ * Key order follows insertion order of the record, matching the demos
226
+ * (profile/wiring first, touch last). Nested objects render inline.
227
+ */
228
+ export function renderDisplayBody(display, options = {}) {
229
+ const indent = options.indent ?? " ";
230
+ const closingIndent = options.closingIndent ?? "";
231
+ const eol = options.eol ?? "\n";
232
+ const lines = [];
233
+ for (const [key, value] of Object.entries(display)) {
234
+ const comment = options.comments?.[key];
235
+ if (comment) {
236
+ lines.push(`${indent}// ${comment}`);
237
+ }
238
+ lines.push(`${indent}${key}: ${renderValue(key, value)},`);
239
+ }
240
+ return `{${eol}${lines.join(eol)}${eol}${closingIndent}}`;
241
+ }
242
+ /** Render the complete `display: { ... }` property text for a property that
243
+ * starts at `options.indent`. The returned text's first line is NOT indented
244
+ * — the caller positions the property start; body lines and the closing
245
+ * brace are indented relative to `options.indent`. */
246
+ export function renderDisplayProperty(display, options = {}) {
247
+ const indent = options.indent ?? "";
248
+ return `display: ${renderDisplayBody(display, {
249
+ ...options,
250
+ indent: `${indent} `,
251
+ closingIndent: indent,
252
+ })}`;
253
+ }
254
+ // ---------------------------------------------------------------------------
255
+ // Splicing — insert or replace the display section inside the config
256
+ // ---------------------------------------------------------------------------
257
+ /** Leading whitespace of the line containing `pos` ("" when mid-line). */
258
+ function lineIndentAt(text, pos) {
259
+ const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
260
+ const before = text.slice(lineStart, pos);
261
+ return /^[ \t]*$/.test(before) ? before : "";
262
+ }
263
+ function detectLineEnding(text) {
264
+ return text.includes("\r\n") ? "\r\n" : "\n";
265
+ }
266
+ /** Skip whitespace and comments from `pos`; true when the next char is ','. */
267
+ function spanStartsWithComma(text, pos, end) {
268
+ let i = pos;
269
+ while (i < end) {
270
+ const ch = text[i];
271
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
272
+ i++;
273
+ continue;
274
+ }
275
+ if (ch === "/" && text[i + 1] === "/") {
276
+ const newline = text.indexOf("\n", i);
277
+ if (newline === -1 || newline >= end)
278
+ return false;
279
+ i = newline + 1;
280
+ continue;
281
+ }
282
+ if (ch === "/" && text[i + 1] === "*") {
283
+ const close = text.indexOf("*/", i);
284
+ if (close === -1 || close + 2 > end)
285
+ return false;
286
+ i = close + 2;
287
+ continue;
288
+ }
289
+ return ch === ",";
290
+ }
291
+ return false;
292
+ }
293
+ /**
294
+ * Insert or replace the `display` section in a cuttlefish.config.ts source
295
+ * string, preserving every other section and comment. Throws when the config
296
+ * has no recognizable default-export object literal.
297
+ */
298
+ export function upsertDisplaySection(sourceText, display, options = {}) {
299
+ const sourceFile = parseSource(sourceText);
300
+ const obj = findConfigObjectLiteral(sourceFile);
301
+ if (!obj) {
302
+ throw new Error("cuttlefish.config.ts has no recognizable config object — expected `export default { ... }` or `const config = { ... }; export default config;`.");
303
+ }
304
+ const eol = options.eol ?? detectLineEnding(sourceText);
305
+ const existing = obj.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyKeyName(prop) === "display");
306
+ if (existing) {
307
+ const indent = lineIndentAt(sourceText, existing.getStart());
308
+ const body = renderDisplayBody(display, { ...options, indent: `${indent} `, closingIndent: indent, eol });
309
+ const init = unwrapTypeCast(existing.initializer);
310
+ if (ts.isObjectLiteralExpression(init)) {
311
+ // Replace just the initializer — `display:` name and its comments stay.
312
+ return {
313
+ text: sourceText.slice(0, init.getStart()) + body + sourceText.slice(init.getEnd()),
314
+ mode: "replaced",
315
+ };
316
+ }
317
+ // `display: someExpression` — replace the whole property.
318
+ return {
319
+ text: sourceText.slice(0, existing.getStart()) + `display: ${body}` + sourceText.slice(existing.getEnd()),
320
+ mode: "replaced",
321
+ };
322
+ }
323
+ // No display section — insert before the object literal's closing brace.
324
+ const closeBrace = obj.getEnd() - 1;
325
+ const lastProp = obj.properties[obj.properties.length - 1];
326
+ const indent = lastProp
327
+ ? lineIndentAt(sourceText, lastProp.getStart())
328
+ : lineIndentAt(sourceText, obj.getStart()) + " ";
329
+ const body = renderDisplayBody(display, { ...options, indent: `${indent} `, closingIndent: indent, eol });
330
+ const closingIndent = lineIndentAt(sourceText, closeBrace);
331
+ let prefix = sourceText;
332
+ if (lastProp && !spanStartsWithComma(sourceText, lastProp.getEnd(), closeBrace)) {
333
+ prefix = sourceText.slice(0, lastProp.getEnd()) + "," + sourceText.slice(lastProp.getEnd());
334
+ }
335
+ // Recompute the brace position if a comma was inserted before it.
336
+ const insertedComma = prefix !== sourceText;
337
+ const closeBraceFinal = closeBrace + (insertedComma ? 1 : 0);
338
+ return {
339
+ text: prefix.slice(0, closeBraceFinal)
340
+ // Trailing comma keeps the multi-property style of the demos.
341
+ + `${indent}display: ${body},${eol}${closingIndent}`
342
+ + prefix.slice(closeBraceFinal),
343
+ mode: "inserted",
344
+ };
345
+ }
346
+ /**
347
+ * Syntactic sanity check for wizard output: returns the first error message
348
+ * when the text no longer parses as TypeScript, or null when it is clean.
349
+ * (Semantic checking is the build's job — this only guards the splice.)
350
+ */
351
+ export function findSyntaxError(text) {
352
+ const output = ts.transpileModule(text, {
353
+ reportDiagnostics: true,
354
+ compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext },
355
+ });
356
+ const errors = (output.diagnostics ?? []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
357
+ if (errors.length === 0)
358
+ return null;
359
+ const first = errors[0];
360
+ const message = ts.flattenDiagnosticMessageText(first.messageText, "\n");
361
+ if (typeof first.start === "number") {
362
+ const line = text.slice(0, first.start).split("\n").length;
363
+ return `line ${line}: ${message}`;
364
+ }
365
+ return message;
366
+ }
@@ -0,0 +1,75 @@
1
+ export type WizardBus = "spi" | "i2c" | "none";
2
+ export type WizardTouchKind = "none" | "spi" | "i2c" | "analog" | "adapter" | "sdl";
3
+ export interface DisplayCatalogEntry {
4
+ /** Catalog id — also the `display.profile` value for built-ins. */
5
+ id: string;
6
+ label: string;
7
+ hint: string;
8
+ /** Built-in profile name written to config (omitted for custom/sdl). */
9
+ profile?: string;
10
+ /** Driver name written when no built-in profile applies. */
11
+ driver?: string;
12
+ bus: WizardBus;
13
+ /** Wizard question defaults. */
14
+ defaults: {
15
+ cs?: number;
16
+ dc?: number;
17
+ rst?: number;
18
+ /** Backlight GPIO; undefined = don't ask, null = ask but default blank. */
19
+ backlight?: number | null;
20
+ /** Backlight pin the built-in profile drives when `backlight` is omitted. */
21
+ backlightProfileDefault?: number;
22
+ spiFrequency?: number;
23
+ address?: number;
24
+ width?: number;
25
+ height?: number;
26
+ /** Native panel size before rotation (used for capacitive touch calibration). */
27
+ nativeWidth?: number;
28
+ nativeHeight?: number;
29
+ rotation?: number;
30
+ colorOrder?: "rgb" | "bgr";
31
+ invert?: boolean;
32
+ antialias?: boolean;
33
+ };
34
+ /** Arduino libraries the driver needs (for the printed next steps). */
35
+ arduinoLibraries?: string[];
36
+ /** A wiring note printed with the summary. */
37
+ wiringNote?: string;
38
+ }
39
+ export declare const DISPLAY_CATALOG: readonly DisplayCatalogEntry[];
40
+ export interface TouchCatalogEntry {
41
+ /** `touch.library` value written to config (built-in libraries only). */
42
+ id: string;
43
+ label: string;
44
+ hint: string;
45
+ kind: WizardTouchKind;
46
+ defaults: {
47
+ cs?: number;
48
+ irq?: number;
49
+ i2cAddress?: number;
50
+ i2cFrequency?: number;
51
+ resetPin?: number;
52
+ /** Resistive raw-ADC calibration (capacitive uses panel pixel space). */
53
+ calibration?: {
54
+ xMin: number;
55
+ xMax: number;
56
+ yMin: number;
57
+ yMax: number;
58
+ };
59
+ analogPins?: {
60
+ xp: number;
61
+ yp: number;
62
+ xm: number;
63
+ ym: number;
64
+ rx: number;
65
+ };
66
+ };
67
+ arduinoLibrary?: string;
68
+ }
69
+ export declare const TOUCH_CATALOG: readonly TouchCatalogEntry[];
70
+ /**
71
+ * Collect GPIO collisions between the display wiring and the touch wiring so
72
+ * the wizard can warn before writing the config (a shared pin is almost
73
+ * always a mis-entered default rather than real hardware).
74
+ */
75
+ export declare function findPinConflicts(displayPins: Record<string, number | undefined>, touchPins: Record<string, number | undefined>): string[];
@@ -0,0 +1,207 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Display + touch hardware catalog for the @typecad/ui integration wizard.
3
+ //
4
+ // Mirrors the built-in display profiles exported by @typecad/framework-arduino
5
+ // (src/displays/*.ts — BUILT_IN_PROFILES) and the TouchLibrary union in
6
+ // @typecad/cuttlefish's display-profile.ts. The wizard asks questions and
7
+ // fills defaults from these entries; the resulting object literal is written
8
+ // into the project's cuttlefish.config.ts `display` section.
9
+ //
10
+ // Keep this catalog in sync with the framework's BUILT_IN_PROFILES when a
11
+ // display or touch library is added there.
12
+ // ---------------------------------------------------------------------------
13
+ export const DISPLAY_CATALOG = [
14
+ {
15
+ id: "ili9341-spi",
16
+ label: "ILI9341 320×240 SPI TFT",
17
+ hint: '2.2"–3.2" color TFT modules, often with resistive touch',
18
+ profile: "ili9341-spi",
19
+ bus: "spi",
20
+ defaults: {
21
+ // ESP32 VSPI wiring used by the repo demos and the framework profile.
22
+ cs: 5,
23
+ dc: 21,
24
+ rst: 22,
25
+ // The ILI9341 built-in profile drives backlight GPIO 17 when the config
26
+ // omits `backlight` — surface that in the wizard's blank-answer hint.
27
+ backlightProfileDefault: 17,
28
+ spiFrequency: 40_000_000,
29
+ width: 320,
30
+ height: 240,
31
+ rotation: 1,
32
+ colorOrder: "rgb",
33
+ antialias: true,
34
+ },
35
+ arduinoLibraries: ["Adafruit GFX Library", "Adafruit ILI9341"],
36
+ wiringNote: "Defaults assume ESP32 VSPI: SCK=18, MOSI=23, MISO=19.",
37
+ },
38
+ {
39
+ id: "st7796-spi",
40
+ label: "ST7796S 320×480 SPI TFT",
41
+ hint: '3.5"/4" color TFT modules, often with FT6336U capacitive touch',
42
+ profile: "st7796-spi",
43
+ bus: "spi",
44
+ defaults: {
45
+ // Wiring proven on the repo's ESP32-S3 demo hardware (demos/demo-st).
46
+ cs: 5,
47
+ dc: 17,
48
+ rst: 16,
49
+ backlight: null,
50
+ spiFrequency: 80_000_000,
51
+ width: 480,
52
+ height: 320,
53
+ nativeWidth: 320,
54
+ nativeHeight: 480,
55
+ rotation: 1,
56
+ colorOrder: "bgr",
57
+ invert: false,
58
+ antialias: true,
59
+ },
60
+ arduinoLibraries: ["Adafruit GFX Library", "Adafruit ST7735 and ST7789 Library"],
61
+ wiringNote: "Defaults assume ESP32 VSPI: SCK=18, MOSI=23, MISO=19. Adafruit_ST7796S.h comes from the" +
62
+ " Adafruit_ST7735_and_ST7789 fork bundled in the typecode demos (demos/demo-st/lib) —" +
63
+ " copy it into your sketchbook libraries/ if your Library Manager copy lacks the class.",
64
+ },
65
+ {
66
+ id: "ssd1309-i2c",
67
+ label: "SSD1309 128×64 monochrome OLED (I2C)",
68
+ hint: "1-bit OLED panels driven via the Adafruit_SSD1306 API",
69
+ profile: "ssd1309-i2c",
70
+ bus: "i2c",
71
+ defaults: {
72
+ address: 0x3c,
73
+ width: 128,
74
+ height: 64,
75
+ rotation: 0,
76
+ antialias: false,
77
+ },
78
+ arduinoLibraries: ["Adafruit GFX Library", "Adafruit SSD1306"],
79
+ },
80
+ {
81
+ id: "sdl",
82
+ label: "Desktop simulator window (no hardware)",
83
+ hint: "native target — renders your UI in an SDL2 window",
84
+ driver: "sdl",
85
+ bus: "none",
86
+ defaults: {
87
+ width: 320,
88
+ height: 240,
89
+ rotation: 0,
90
+ antialias: true,
91
+ },
92
+ },
93
+ {
94
+ id: "custom",
95
+ label: "Custom display — enter driver, bus, and pins manually",
96
+ hint: "any controller not listed above",
97
+ bus: "spi",
98
+ defaults: {},
99
+ },
100
+ ];
101
+ export const TOUCH_CATALOG = [
102
+ {
103
+ id: "none",
104
+ label: "No touch (display only)",
105
+ hint: "skip touch configuration",
106
+ kind: "none",
107
+ defaults: {},
108
+ },
109
+ {
110
+ id: "XPT2046_Touchscreen",
111
+ label: "XPT2046 resistive (SPI)",
112
+ hint: "the resistive controller on most ILI9341 modules",
113
+ kind: "spi",
114
+ defaults: {
115
+ cs: 15,
116
+ // Typical raw-ADC range for 320×240 modules; refine per panel.
117
+ calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
118
+ },
119
+ arduinoLibrary: "XPT2046_Touchscreen",
120
+ },
121
+ {
122
+ id: "FT6336U",
123
+ label: "FT6336U capacitive (I2C)",
124
+ hint: "on many ST7796S 320×480 modules",
125
+ kind: "i2c",
126
+ defaults: {
127
+ i2cAddress: 0x38,
128
+ i2cFrequency: 400_000,
129
+ },
130
+ // The framework's FT6336U adapter includes RAK14014_FT6336U.h.
131
+ arduinoLibrary: "RAK14014-FT6336U",
132
+ },
133
+ {
134
+ id: "GT911",
135
+ label: "GT911 capacitive (I2C)",
136
+ hint: "common on larger IPS panels",
137
+ kind: "i2c",
138
+ defaults: {
139
+ i2cAddress: 0x5d,
140
+ i2cFrequency: 400_000,
141
+ },
142
+ arduinoLibrary: "GT911",
143
+ },
144
+ {
145
+ id: "CST816S",
146
+ label: "CST816S capacitive (I2C)",
147
+ hint: "on many compact ESP32 display boards",
148
+ kind: "i2c",
149
+ defaults: {
150
+ i2cAddress: 0x15,
151
+ i2cFrequency: 400_000,
152
+ },
153
+ arduinoLibrary: "CST816S",
154
+ },
155
+ {
156
+ id: "Adafruit_TouchScreen",
157
+ label: "Adafruit 4-wire resistive (analog pins)",
158
+ hint: '2.4"/2.8" Adafruit shields with XP/YP/XM/YM analog pins',
159
+ kind: "analog",
160
+ defaults: {
161
+ analogPins: { xp: 24, yp: 25, xm: 26, ym: 27, rx: 300 },
162
+ calibration: { xMin: 100, xMax: 900, yMin: 120, yMax: 900 },
163
+ },
164
+ arduinoLibrary: "Adafruit TouchScreen",
165
+ },
166
+ {
167
+ id: "Adafruit_STMPE610",
168
+ label: "STMPE610 resistive (SPI)",
169
+ hint: 'Adafruit 2.8" TFT touch cape controller',
170
+ kind: "spi",
171
+ defaults: {
172
+ cs: 14,
173
+ calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
174
+ },
175
+ arduinoLibrary: "Adafruit STMPE610",
176
+ },
177
+ {
178
+ id: "adapter",
179
+ label: "Custom touch adapter file",
180
+ hint: "path to your own TypeScript adapter",
181
+ kind: "adapter",
182
+ defaults: {},
183
+ },
184
+ ];
185
+ /**
186
+ * Collect GPIO collisions between the display wiring and the touch wiring so
187
+ * the wizard can warn before writing the config (a shared pin is almost
188
+ * always a mis-entered default rather than real hardware).
189
+ */
190
+ export function findPinConflicts(displayPins, touchPins) {
191
+ const conflicts = [];
192
+ const claimed = new Map();
193
+ for (const [displayKey, displayValue] of Object.entries(displayPins)) {
194
+ if (displayValue === undefined)
195
+ continue;
196
+ claimed.set(displayValue, displayKey);
197
+ }
198
+ for (const [touchKey, touchValue] of Object.entries(touchPins)) {
199
+ if (touchValue === undefined)
200
+ continue;
201
+ const owner = claimed.get(touchValue);
202
+ if (owner !== undefined) {
203
+ conflicts.push(`display.${owner} and touch.${touchKey} are both wired to GPIO ${touchValue}`);
204
+ }
205
+ }
206
+ return conflicts;
207
+ }
@@ -0,0 +1,7 @@
1
+ export { DISPLAY_CATALOG, TOUCH_CATALOG, findPinConflicts, } from "./display-catalog.js";
2
+ export type { DisplayCatalogEntry, TouchCatalogEntry, WizardBus, WizardTouchKind, } from "./display-catalog.js";
3
+ export { findCuttlefishConfig, findSyntaxError, readConfigSection, readEntryPath, renderDisplayBody, renderDisplayProperty, upsertDisplaySection, } from "./config-writer.js";
4
+ export type { ConfigRecord, RenderDisplayOptions, UpsertDisplayResult, } from "./config-writer.js";
5
+ export { renderStarterUi } from "./starter-ui.js";
6
+ export { runIntegrationWizard } from "./integration-wizard.js";
7
+ export type { WizardRunResult, WizardStreams } from "./integration-wizard.js";
@@ -0,0 +1,10 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Public surface of the @typecad/ui integration wizard (`npx @typecad/ui
3
+ // --config`). Pure helpers (catalog, rendering, config splicing, starter
4
+ // template) are exported for tests and tooling; the interactive flow lives in
5
+ // integration-wizard.ts.
6
+ // ---------------------------------------------------------------------------
7
+ export { DISPLAY_CATALOG, TOUCH_CATALOG, findPinConflicts, } from "./display-catalog.js";
8
+ export { findCuttlefishConfig, findSyntaxError, readConfigSection, readEntryPath, renderDisplayBody, renderDisplayProperty, upsertDisplaySection, } from "./config-writer.js";
9
+ export { renderStarterUi } from "./starter-ui.js";
10
+ export { runIntegrationWizard } from "./integration-wizard.js";
@@ -0,0 +1,15 @@
1
+ export interface WizardRunResult {
2
+ exitCode: number;
3
+ /** Absolute path of the config that was written, when a write happened. */
4
+ configPath?: string;
5
+ }
6
+ /** Optional stream overrides so tests can drive the interactive flow. */
7
+ export interface WizardStreams {
8
+ input?: NodeJS.ReadableStream;
9
+ output?: NodeJS.WritableStream;
10
+ }
11
+ /**
12
+ * Run the interactive integration wizard. Returns a process exit code
13
+ * (0 = wrote (or deliberately declined), 1 = blocked before writing).
14
+ */
15
+ export declare function runIntegrationWizard(cwd?: string, streams?: WizardStreams): Promise<WizardRunResult>;