@huaqiu/component-gen-app 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,2550 @@
1
+ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
3
+ import { BoardParser, SchematicParser } from "@huaqiu/kicad-sexpr-parser";
4
+ import { renderSymbol } from "@huaqiu/ecad-renderer/symbol";
5
+ import { renderFootprint } from "@huaqiu/ecad-renderer/footprint";
6
+ //#region src/copy/zh.ts
7
+ /**
8
+ * `@huaqiu/component-gen-app` — zh copy pack (fallback everywhere).
9
+ *
10
+ * House i18n convention (`dsh-pcb-eda`): `ZH as const` → `CopyKey` →
11
+ * `EN: Record<CopyKey, string>` so a missing EN key is a compile error.
12
+ * Punctuation is i18n too(中文全角:;).
13
+ */
14
+ const ZH = {
15
+ app: {
16
+ title: "华秋元器件生成",
17
+ symbolTitle: "符号生成",
18
+ footprintTitle: "封装生成",
19
+ footprintTooltip: "打开封装生成",
20
+ symbolTooltip: "打开符号生成",
21
+ close: "关闭",
22
+ back: "返回",
23
+ loading: "加载中…",
24
+ error: "出错了",
25
+ unknownError: "未知错误"
26
+ },
27
+ upload: {
28
+ drop: "拖拽图片到此处,或点击上传",
29
+ paste: "也可直接 Ctrl/⌘+V 粘贴图片",
30
+ browse: "选择图片",
31
+ imageTooLarge: "图片过大,请使用 4MiB 以内的图片",
32
+ noImage: "请先上传图片",
33
+ replace: "更换图片",
34
+ uploading: "上传中…",
35
+ hint: "提示"
36
+ },
37
+ symbol: {
38
+ generate: "生成符号",
39
+ regenerate: "重新生成",
40
+ instructionPlaceholder: "可选:补充生成说明,如“3 引脚 LDO,引脚 1 为 VIN”",
41
+ progress: "正在生成符号…",
42
+ ready: "生成完成",
43
+ failed: "生成失败",
44
+ download: "下载 .kicad_sym",
45
+ save: "保存到历史",
46
+ saved: "已保存到历史"
47
+ },
48
+ footprint: {
49
+ extract: "提取尺寸",
50
+ generate: "生成封装",
51
+ regenerate: "重新生成",
52
+ hintPlaceholder: "可选:封装类型提示,如 BGA / QFN / SOP",
53
+ extractProgress: "正在提取封装尺寸…",
54
+ generateProgress: "正在生成封装…",
55
+ ready: "生成完成",
56
+ failed: "生成失败",
57
+ needsConfirmation: "请确认或修改提取到的封装尺寸",
58
+ download: "下载 .kicad_mod",
59
+ save: "保存到历史",
60
+ saved: "已保存到历史",
61
+ directGenerated: "已自动生成标准封装,可预览后下载",
62
+ autoGenerated: "自动生成",
63
+ confirm: "确认并生成",
64
+ cancel: "取消"
65
+ },
66
+ editor: {
67
+ essential: "基本参数",
68
+ advanced: "高级参数",
69
+ width: "宽度",
70
+ height: "长度",
71
+ pinCount: "引脚数",
72
+ pitch: "间距",
73
+ packageType: "封装类型",
74
+ fileName: "文件名",
75
+ editedTag: "已修改",
76
+ aiTag: "AI 提取",
77
+ dragHint: "拖动控制柄调整 W / H",
78
+ validationOutOfRange: "超出范围",
79
+ validationMinGtMax: "最小值大于最大值",
80
+ validationInvalid: "无效数值",
81
+ unit: "mm",
82
+ body: "本体",
83
+ pins: "{count} 引脚",
84
+ validationIssue: "{n} 处数值需要修正",
85
+ validationOk: "数值有效",
86
+ issueOutOfRange: "超出允许范围",
87
+ issueMinGtMax: "最小值大于最大值",
88
+ confirmLabel: "确认",
89
+ cancelLabel: "取消"
90
+ },
91
+ field: {
92
+ width: "宽度",
93
+ height: "长度",
94
+ length: "长度",
95
+ depth: "深度",
96
+ span: "跨距",
97
+ bodyWidth: "本体宽度",
98
+ bodyLength: "本体长度",
99
+ bodyHeight: "本体高度",
100
+ boardWidth: "板宽",
101
+ boardHeight: "板高",
102
+ overallWidth: "总宽度",
103
+ overallLength: "总长度",
104
+ overallHeight: "总高度",
105
+ pitch: "间距",
106
+ pitchX: "X 间距",
107
+ pitchY: "Y 间距",
108
+ leadPitch: "引线间距",
109
+ padWidth: "焊盘宽",
110
+ padLength: "焊盘长",
111
+ padHeight: "焊盘高",
112
+ leadWidth: "引线宽",
113
+ leadLength: "引线长",
114
+ leadSpan: "引线跨距",
115
+ pinCount: "引脚数",
116
+ rows: "行数",
117
+ columns: "列数",
118
+ standoff: "离地高度",
119
+ maxOf: "{field} 最大值",
120
+ minOf: "{field} 最小值"
121
+ },
122
+ history: {
123
+ title: "历史记录",
124
+ empty: "暂无历史",
125
+ symbol: "符号",
126
+ footprint: "封装",
127
+ generated: "已生成",
128
+ failed: "失败",
129
+ cancelled: "已取消",
130
+ rename: "重命名",
131
+ delete: "删除",
132
+ download: "下载",
133
+ reopen: "打开",
134
+ view: "查看历史",
135
+ createdAt: "创建时间",
136
+ loadMore: "加载更多",
137
+ noMore: "没有更多了"
138
+ },
139
+ auth: {
140
+ notLoggedIn: "未登录华秋账号",
141
+ login: "登录华秋 EDA",
142
+ loginRequired: "该功能需要登录华秋 EDA 账号",
143
+ loggedIn: "已登录"
144
+ },
145
+ status: {
146
+ queued: "排队中",
147
+ running: "进行中",
148
+ needsConfirmation: "待确认",
149
+ completed: "已完成",
150
+ failed: "失败",
151
+ cancelled: "已取消"
152
+ }
153
+ };
154
+ //#endregion
155
+ //#region src/copy/en.ts
156
+ const EN = {
157
+ app: {
158
+ title: "Huaqiu Component Gen",
159
+ symbolTitle: "Symbol Gen",
160
+ footprintTitle: "Footprint Gen",
161
+ footprintTooltip: "Open footprint generator",
162
+ symbolTooltip: "Open symbol generator",
163
+ close: "Close",
164
+ back: "Back",
165
+ loading: "Loading…",
166
+ error: "Error",
167
+ unknownError: "Unknown error"
168
+ },
169
+ upload: {
170
+ drop: "Drop an image here, or click to upload",
171
+ paste: "Or paste with Ctrl/⌘+V",
172
+ browse: "Choose image",
173
+ imageTooLarge: "Image too large; keep it under 4 MiB",
174
+ noImage: "Upload an image first",
175
+ replace: "Replace image",
176
+ uploading: "Uploading…",
177
+ hint: "Hint"
178
+ },
179
+ symbol: {
180
+ generate: "Generate Symbol",
181
+ regenerate: "Regenerate",
182
+ instructionPlaceholder: "Optional instruction, e.g. \"3-pin LDO, pin 1 is VIN\"",
183
+ progress: "Generating symbol…",
184
+ ready: "Generated",
185
+ failed: "Generation failed",
186
+ download: "Download .kicad_sym",
187
+ save: "Save to history",
188
+ saved: "Saved to history"
189
+ },
190
+ footprint: {
191
+ extract: "Extract dimensions",
192
+ generate: "Generate Footprint",
193
+ regenerate: "Regenerate",
194
+ hintPlaceholder: "Optional package type hint, e.g. BGA / QFN / SOP",
195
+ extractProgress: "Extracting package dimensions…",
196
+ generateProgress: "Generating footprint…",
197
+ ready: "Generated",
198
+ failed: "Generation failed",
199
+ needsConfirmation: "Confirm or correct the extracted package dimensions",
200
+ download: "Download .kicad_mod",
201
+ save: "Save to history",
202
+ saved: "Saved to history",
203
+ directGenerated: "A standard footprint was auto-generated; review it below",
204
+ autoGenerated: "Auto-generated",
205
+ confirm: "Confirm & generate",
206
+ cancel: "Cancel"
207
+ },
208
+ editor: {
209
+ essential: "Essential",
210
+ advanced: "Advanced",
211
+ width: "Width",
212
+ height: "Height",
213
+ pinCount: "Pin count",
214
+ pitch: "Pitch",
215
+ packageType: "Package type",
216
+ fileName: "File name",
217
+ editedTag: "Edited",
218
+ aiTag: "AI-extracted",
219
+ dragHint: "Drag the handles to adjust W / H",
220
+ validationOutOfRange: "Out of range",
221
+ validationMinGtMax: "Min > max",
222
+ validationInvalid: "Invalid value",
223
+ unit: "mm",
224
+ body: "Body",
225
+ pins: "{count} pins",
226
+ validationIssue: "{n} value(s) need fixing",
227
+ validationOk: "Values are valid",
228
+ issueOutOfRange: "Out of allowed range",
229
+ issueMinGtMax: "Min > max",
230
+ confirmLabel: "Confirm",
231
+ cancelLabel: "Cancel"
232
+ },
233
+ field: {
234
+ width: "Width",
235
+ height: "Height",
236
+ length: "Length",
237
+ depth: "Depth",
238
+ span: "Span",
239
+ bodyWidth: "Body width",
240
+ bodyLength: "Body length",
241
+ bodyHeight: "Body height",
242
+ boardWidth: "Board width",
243
+ boardHeight: "Board height",
244
+ overallWidth: "Overall width",
245
+ overallLength: "Overall length",
246
+ overallHeight: "Overall height",
247
+ pitch: "Pitch",
248
+ pitchX: "Pitch X",
249
+ pitchY: "Pitch Y",
250
+ leadPitch: "Lead pitch",
251
+ padWidth: "Pad width",
252
+ padLength: "Pad length",
253
+ padHeight: "Pad height",
254
+ leadWidth: "Lead width",
255
+ leadLength: "Lead length",
256
+ leadSpan: "Lead span",
257
+ pinCount: "Pin count",
258
+ rows: "Rows",
259
+ columns: "Columns",
260
+ standoff: "Standoff",
261
+ maxOf: "{field} max",
262
+ minOf: "{field} min"
263
+ },
264
+ history: {
265
+ title: "History",
266
+ empty: "No history yet",
267
+ symbol: "Symbol",
268
+ footprint: "Footprint",
269
+ generated: "Generated",
270
+ failed: "Failed",
271
+ cancelled: "Cancelled",
272
+ rename: "Rename",
273
+ delete: "Delete",
274
+ download: "Download",
275
+ reopen: "Open",
276
+ view: "View history",
277
+ createdAt: "Created",
278
+ loadMore: "Load more",
279
+ noMore: "No more"
280
+ },
281
+ auth: {
282
+ notLoggedIn: "Not logged in to Huaqiu EDA",
283
+ login: "Login to Huaqiu EDA",
284
+ loginRequired: "This feature requires a Huaqiu EDA login",
285
+ loggedIn: "Logged in"
286
+ },
287
+ status: {
288
+ queued: "Queued",
289
+ running: "Running",
290
+ needsConfirmation: "Needs confirmation",
291
+ completed: "Completed",
292
+ failed: "Failed",
293
+ cancelled: "Cancelled"
294
+ }
295
+ };
296
+ //#endregion
297
+ //#region src/copy/index.ts
298
+ /**
299
+ * `@huaqiu/component-gen-app` — copy packs + translate resolver.
300
+ *
301
+ * Mirrors the `dsh-tool-symbol-footprint` house convention:
302
+ * - `ZH as const` → `CopyKey` → `EN: Record<CopyKey, string>` (missing EN
303
+ * key is a compile error; zh is the runtime fallback);
304
+ * - `Translate = (key, params?) => string` with `{param}` substitution;
305
+ * - `fieldLabel` (utils/labels.ts) resolves dimension keys through the
306
+ * `field.*` copy section, so labels follow the UI language.
307
+ */
308
+ function lookup(pack, keys) {
309
+ let node = pack;
310
+ for (const seg of keys.split(".")) if (node && typeof node === "object") node = node[seg];
311
+ else return void 0;
312
+ return typeof node === "string" ? node : void 0;
313
+ }
314
+ function translate(lang, key, params) {
315
+ let v = lookup(lang?.toLowerCase().startsWith("en") ? EN : ZH, key);
316
+ if (v === void 0) v = lookup(ZH, key);
317
+ if (v === void 0) v = key;
318
+ if (params) for (const k of Object.keys(params)) v = v.split(`{${k}}`).join(String(params[k]));
319
+ return v;
320
+ }
321
+ /** Build a `(key, params?) => string` resolver for a given language. */
322
+ function translateFor(lang) {
323
+ return (key, params) => translate(lang, key, params);
324
+ }
325
+ /** zh-only fallback (used when no locale is supplied). */
326
+ const defaultT = (key, params) => translate("zh", key, params);
327
+ //#endregion
328
+ //#region src/components/UploadInput.tsx
329
+ /**
330
+ * `@huaqiu/component-gen-app` — image upload control (click + paste + drag).
331
+ *
332
+ * Reads a file, downscales it to a thumbnail data URL if needed (bounded by
333
+ * `maxBytes`), and reports both the original file and the data URL to the
334
+ * parent. The parent owns sending it to the server.
335
+ */
336
+ const MAX_EDGE = 1600;
337
+ /** Downscale an image file to a data URL bounded in edge + bytes. */
338
+ function fileToDataUrl(file, maxBytes) {
339
+ return new Promise((resolve, reject) => {
340
+ const reader = new FileReader();
341
+ reader.onerror = () => reject(/* @__PURE__ */ new Error("failed to read file"));
342
+ reader.onload = () => {
343
+ const src = String(reader.result);
344
+ if (src.length <= maxBytes) {
345
+ resolve(src);
346
+ return;
347
+ }
348
+ const img = new Image();
349
+ img.onload = () => {
350
+ const scale = Math.min(1, MAX_EDGE / Math.max(img.width, img.height));
351
+ const canvas = document.createElement("canvas");
352
+ canvas.width = Math.max(1, Math.round(img.width * scale));
353
+ canvas.height = Math.max(1, Math.round(img.height * scale));
354
+ const ctx = canvas.getContext("2d");
355
+ if (!ctx) {
356
+ reject(/* @__PURE__ */ new Error("canvas unavailable"));
357
+ return;
358
+ }
359
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
360
+ let out = canvas.toDataURL("image/jpeg", .82);
361
+ for (const q of [
362
+ .7,
363
+ .55,
364
+ .4
365
+ ]) {
366
+ if (out.length <= maxBytes) break;
367
+ out = canvas.toDataURL("image/jpeg", q);
368
+ }
369
+ resolve(out);
370
+ };
371
+ img.onerror = () => reject(/* @__PURE__ */ new Error("failed to decode image"));
372
+ img.src = src;
373
+ };
374
+ reader.readAsDataURL(file);
375
+ });
376
+ }
377
+ function UploadInput(props) {
378
+ const { maxBytes, t, disabled = false, imageDataUrl = null, file = null, onFile } = props;
379
+ const inputRef = useRef(null);
380
+ const [dragging, setDragging] = useState(false);
381
+ const [error, setError] = useState(null);
382
+ const accept = useCallback(async (f) => {
383
+ if (!f) return;
384
+ if (f.size > maxBytes) {
385
+ setError(t("upload.imageTooLarge"));
386
+ return;
387
+ }
388
+ setError(null);
389
+ try {
390
+ const dataUrl = await fileToDataUrl(f, maxBytes);
391
+ onFile(f, dataUrl);
392
+ } catch (e) {
393
+ setError(String(e?.message || e));
394
+ }
395
+ }, [
396
+ maxBytes,
397
+ onFile,
398
+ t
399
+ ]);
400
+ return /* @__PURE__ */ jsxs("div", {
401
+ className: `cga-upload${dragging ? " cga-upload--dragging" : ""}`,
402
+ onClick: () => {
403
+ if (!disabled) inputRef.current?.click();
404
+ },
405
+ onDragOver: (ev) => {
406
+ ev.preventDefault();
407
+ if (!disabled) setDragging(true);
408
+ },
409
+ onDragLeave: () => setDragging(false),
410
+ onDrop: (ev) => {
411
+ ev.preventDefault();
412
+ setDragging(false);
413
+ if (disabled) return;
414
+ accept(ev.dataTransfer.files?.[0] ?? null);
415
+ },
416
+ onPaste: (ev) => {
417
+ const item = Array.from(ev.clipboardData?.items ?? []).find((i) => i.type.startsWith("image/"));
418
+ if (item) {
419
+ ev.preventDefault();
420
+ accept(item.getAsFile());
421
+ }
422
+ },
423
+ children: [
424
+ /* @__PURE__ */ jsx("input", {
425
+ ref: inputRef,
426
+ type: "file",
427
+ accept: "image/*",
428
+ style: { display: "none" },
429
+ disabled,
430
+ onChange: (ev) => {
431
+ accept(ev.target.files?.[0] ?? null);
432
+ ev.target.value = "";
433
+ }
434
+ }),
435
+ imageDataUrl ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("img", {
436
+ className: "cga-upload__thumb",
437
+ src: imageDataUrl,
438
+ alt: ""
439
+ }), /* @__PURE__ */ jsx("div", {
440
+ className: "cga-upload__text",
441
+ children: file?.name ?? ""
442
+ })] }) : null,
443
+ /* @__PURE__ */ jsxs("div", {
444
+ className: "cga-upload__text",
445
+ children: [
446
+ imageDataUrl ? t("upload.replace") : t("upload.drop"),
447
+ /* @__PURE__ */ jsx("br", {}),
448
+ t("upload.paste")
449
+ ]
450
+ }),
451
+ error ? /* @__PURE__ */ jsx("div", {
452
+ className: "cga-upload__text",
453
+ style: { color: "var(--dsw-alias-state-error-primary, red)" },
454
+ children: error
455
+ }) : null,
456
+ !imageDataUrl ? /* @__PURE__ */ jsx("button", {
457
+ type: "button",
458
+ className: "cga-upload__browse",
459
+ children: t("upload.browse")
460
+ }) : null
461
+ ]
462
+ });
463
+ }
464
+ //#endregion
465
+ //#region src/utils/ecad.ts
466
+ function wrapFootprintInBoard(src) {
467
+ return `(kicad_pcb (version 20240108) (generator "huaqiu-dsh") ${src})`;
468
+ }
469
+ function parseSymbol(source) {
470
+ const sp = new SchematicParser();
471
+ if (typeof sp.parseLibSymbols !== "function") throw new Error("parseLibSymbols is not a method on SchematicParser");
472
+ const symbols = sp.parseLibSymbols(source);
473
+ if (!symbols || symbols.length === 0) throw new Error("no symbols found in .kicad_sym source");
474
+ return symbols[0];
475
+ }
476
+ function parseFootprint(source) {
477
+ const bp = new BoardParser();
478
+ if (typeof bp.parse !== "function") throw new Error("parse is not a method on BoardParser");
479
+ const toParse = /^\s*\(\s*kicad_pcb\b/.test(source) ? source : wrapFootprintInBoard(source);
480
+ const board = bp.parse(toParse);
481
+ if (!board || !Array.isArray(board.footprints) || board.footprints.length === 0) throw new Error("no footprints found in wrapped kicad_pcb source");
482
+ return board.footprints[0];
483
+ }
484
+ /**
485
+ * Render a single generated artifact (symbol/footprint) onto a canvas. Returns
486
+ * the renderer dispose handle so the caller can release the viewer.
487
+ */
488
+ async function renderArtifactToCanvas(kind, content, canvas) {
489
+ let dispose;
490
+ if (kind === "symbol") {
491
+ const sym = parseSymbol(content);
492
+ const r = await renderSymbol(sym, {
493
+ canvas,
494
+ interactive: true
495
+ });
496
+ dispose = () => r.dispose();
497
+ } else if (kind === "footprint") {
498
+ const fp = parseFootprint(content);
499
+ const r = await renderFootprint(fp, {
500
+ canvas,
501
+ interactive: true
502
+ });
503
+ dispose = () => r.dispose();
504
+ } else throw new Error(`unsupported preview kind: ${kind}`);
505
+ return () => {
506
+ try {
507
+ dispose?.();
508
+ } catch {}
509
+ };
510
+ }
511
+ /** Size a canvas to its CSS box (device-pixel-ratio aware). */
512
+ function sizeCanvasFor(canvas) {
513
+ const dpr = window.devicePixelRatio || 1;
514
+ const cssW = canvas.clientWidth || 720;
515
+ const cssH = canvas.clientHeight || 320;
516
+ canvas.width = Math.max(100, Math.floor(cssW * dpr));
517
+ canvas.height = Math.max(100, Math.floor(cssH * dpr));
518
+ }
519
+ /** Trigger a browser download of a text artifact. */
520
+ function triggerDownload(filename, text, mime = "text/plain;charset=utf-8") {
521
+ try {
522
+ const blob = new Blob([text], { type: mime });
523
+ const url = URL.createObjectURL(blob);
524
+ const a = document.createElement("a");
525
+ a.href = url;
526
+ a.download = filename;
527
+ document.body.appendChild(a);
528
+ a.click();
529
+ a.remove();
530
+ setTimeout(() => {
531
+ try {
532
+ URL.revokeObjectURL(url);
533
+ } catch {}
534
+ }, 2e3);
535
+ } catch (err) {
536
+ console.warn("[hq-cga] download failed", err);
537
+ }
538
+ }
539
+ //#endregion
540
+ //#region src/components/PreviewStage.tsx
541
+ /**
542
+ * `@huaqiu/component-gen-app` — ECAD canvas preview of a generated artifact.
543
+ *
544
+ * The host supplies the artifact text via `ports.artifactContent`; this
545
+ * component renders it with the bundled ecad-renderer and releases the viewer
546
+ * on unmount / src change.
547
+ */
548
+ function PreviewStage({ kind, content, srcKey, t }) {
549
+ const canvasRef = useRef(null);
550
+ const [view, setView] = useState({
551
+ view: "loading",
552
+ message: ""
553
+ });
554
+ useEffect(() => {
555
+ let cancelled = false;
556
+ let disposeViewer = null;
557
+ setView({
558
+ view: "loading",
559
+ message: ""
560
+ });
561
+ (async () => {
562
+ try {
563
+ const canvas = canvasRef.current;
564
+ if (!canvas) return;
565
+ await new Promise((resolve) => {
566
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(resolve);
567
+ else setTimeout(resolve, 16);
568
+ });
569
+ if (cancelled || canvas !== canvasRef.current) return;
570
+ sizeCanvasFor(canvas);
571
+ disposeViewer = await renderArtifactToCanvas(kind ?? "symbol", content, canvas);
572
+ if (cancelled || canvas !== canvasRef.current) return;
573
+ setView({
574
+ view: "ready",
575
+ message: ""
576
+ });
577
+ } catch (e) {
578
+ if (!cancelled) {
579
+ console.warn("[hq-component-gen] preview render failed", e);
580
+ setView({
581
+ view: "error",
582
+ message: String(e?.message || e)
583
+ });
584
+ }
585
+ }
586
+ })();
587
+ return () => {
588
+ cancelled = true;
589
+ try {
590
+ disposeViewer?.();
591
+ } catch {}
592
+ };
593
+ }, [
594
+ srcKey,
595
+ kind,
596
+ content
597
+ ]);
598
+ const overlay = view.view === "loading" ? /* @__PURE__ */ jsx("div", {
599
+ className: "hq-genhit__stage-msg",
600
+ children: t("app.loading")
601
+ }) : view.view === "error" ? /* @__PURE__ */ jsxs("div", {
602
+ className: "hq-genhit__stage-msg hq-genhit__stage-msg--error",
603
+ children: [t("app.error"), view.message]
604
+ }) : null;
605
+ return /* @__PURE__ */ jsxs("div", {
606
+ className: `hq-genhit__stage hq-genhit__stage--${kind ?? "symbol"}`,
607
+ children: [/* @__PURE__ */ jsx("canvas", {
608
+ ref: canvasRef,
609
+ className: "hq-genhit__canvas"
610
+ }), overlay]
611
+ });
612
+ }
613
+ //#endregion
614
+ //#region src/components/ResultStage.tsx
615
+ /**
616
+ * `@huaqiu/component-gen-app` — completed-generation result stage.
617
+ *
618
+ * Renders the preview (from `ports.artifactContent`), a download action and a
619
+ * note when the generation was auto-generated or degraded. DSH-agnostic.
620
+ */
621
+ function artifactIdOf(result) {
622
+ const art = result.artifact;
623
+ if (art && typeof art === "object") {
624
+ const id = art.id;
625
+ if (typeof id === "string" && id) return id;
626
+ }
627
+ return null;
628
+ }
629
+ function filenameOf(result, kind) {
630
+ const f = result.filename;
631
+ if (typeof f === "string" && f) return f;
632
+ return `generated.${kind === "symbol" ? "kicad_sym" : "kicad_mod"}`;
633
+ }
634
+ function ResultStage({ ports, kind, result, t, srcKey }) {
635
+ const artifactId = artifactIdOf(result);
636
+ const [content, setContent] = useState(null);
637
+ const [previewErr, setPreviewErr] = useState(null);
638
+ const [downloading, setDownloading] = useState(false);
639
+ useEffect(() => {
640
+ if (!artifactId) return;
641
+ let cancelled = false;
642
+ setContent(null);
643
+ setPreviewErr(null);
644
+ ports.artifactContent(artifactId).then((text) => {
645
+ if (!cancelled) setContent(text);
646
+ }).catch((e) => {
647
+ if (!cancelled) setPreviewErr(String(e?.message || e));
648
+ });
649
+ return () => {
650
+ cancelled = true;
651
+ };
652
+ }, [
653
+ artifactId,
654
+ ports,
655
+ srcKey
656
+ ]);
657
+ const download = useCallback(async () => {
658
+ setDownloading(true);
659
+ try {
660
+ if (artifactId) {
661
+ const text = content ?? await ports.artifactContent(artifactId);
662
+ triggerDownload(filenameOf(result, kind), text);
663
+ } else {
664
+ const fileUrl = result.fileUrl;
665
+ if (typeof fileUrl === "string" && fileUrl) window.open(fileUrl, "_blank", "noopener");
666
+ }
667
+ } finally {
668
+ setDownloading(false);
669
+ }
670
+ }, [
671
+ artifactId,
672
+ content,
673
+ ports,
674
+ result,
675
+ kind
676
+ ]);
677
+ const autoGenerated = result.autoGenerated === true;
678
+ const note = typeof result.note === "string" ? result.note : null;
679
+ return /* @__PURE__ */ jsx("div", {
680
+ className: "cga-panel",
681
+ children: /* @__PURE__ */ jsxs("div", {
682
+ className: "cga-panel__body",
683
+ children: [
684
+ autoGenerated ? /* @__PURE__ */ jsx("div", {
685
+ className: "cga-banner cga-banner--info",
686
+ children: t("footprint.directGenerated")
687
+ }) : null,
688
+ content != null ? /* @__PURE__ */ jsx(PreviewStage, {
689
+ kind,
690
+ content,
691
+ srcKey,
692
+ t
693
+ }) : previewErr ? /* @__PURE__ */ jsxs("div", {
694
+ className: "cga-banner cga-banner--error",
695
+ children: [t("app.error"), previewErr]
696
+ }) : null,
697
+ note ? /* @__PURE__ */ jsx("div", {
698
+ className: "cga-banner cga-banner--info",
699
+ children: note
700
+ }) : null,
701
+ /* @__PURE__ */ jsx("div", {
702
+ className: "cga-actions",
703
+ children: /* @__PURE__ */ jsx("button", {
704
+ type: "button",
705
+ className: "cga-btn cga-btn--primary",
706
+ onClick: () => void download(),
707
+ disabled: downloading,
708
+ children: kind === "symbol" ? t("symbol.download") : t("footprint.download")
709
+ })
710
+ })
711
+ ]
712
+ })
713
+ });
714
+ }
715
+ //#endregion
716
+ //#region src/hooks/useAuthGate.ts
717
+ /**
718
+ * `@huaqiu/component-gen-app` — auth gate over `ComponentGenPorts.auth`.
719
+ *
720
+ * The app only knows the public `@huaqiu/dsh-auth` surface (the host wires it):
721
+ * read state, subscribe to changes, trigger the existing login flow. It never
722
+ * implements auth itself.
723
+ */
724
+ function useAuthGate(ports) {
725
+ const [phase, setPhase] = useState("unknown");
726
+ const [user, setUser] = useState(null);
727
+ const refresh = useCallback(async () => {
728
+ try {
729
+ const ok = await ports.auth.isAuthenticated();
730
+ setPhase(ok ? "authenticated" : "unauthenticated");
731
+ setUser(ok ? await ports.auth.getUserInfo() : null);
732
+ } catch {
733
+ setPhase("unauthenticated");
734
+ setUser(null);
735
+ }
736
+ }, [ports]);
737
+ useEffect(() => {
738
+ refresh();
739
+ return ports.auth.onAuthStateChanged((authenticated) => {
740
+ setPhase(authenticated ? "authenticated" : "unauthenticated");
741
+ if (authenticated) ports.auth.getUserInfo().then(setUser);
742
+ else setUser(null);
743
+ });
744
+ }, [ports, refresh]);
745
+ return {
746
+ phase,
747
+ user,
748
+ login: useCallback(() => {
749
+ ports.auth.login();
750
+ }, [ports]),
751
+ refresh
752
+ };
753
+ }
754
+ //#endregion
755
+ //#region src/hooks/useJobRunner.ts
756
+ /**
757
+ * `@huaqiu/component-gen-app` — reusable generation job runner.
758
+ *
759
+ * Encapsulates the start → SSE-stream → terminal-state lifecycle for one
760
+ * page's active generation. The page calls `run(req)` and reacts to `phase`.
761
+ * `ports.jobEvents` is expected to replay the job's current state first, so a
762
+ * job that finished before subscription still lands correctly.
763
+ */
764
+ function useJobRunner(ports) {
765
+ const [phase, setPhase] = useState("idle");
766
+ const [progress, setProgress] = useState("");
767
+ const [dimensions, setDimensions] = useState(null);
768
+ const [pkgType, setPkgType] = useState(null);
769
+ const [fileName, setFileName] = useState(null);
770
+ const [result, setResult] = useState({});
771
+ const [error, setError] = useState("");
772
+ const [jobId, setJobId] = useState(null);
773
+ const unsubRef = useRef(null);
774
+ const cleanup = useCallback(() => {
775
+ unsubRef.current?.();
776
+ unsubRef.current = null;
777
+ }, []);
778
+ useEffect(() => cleanup, [cleanup]);
779
+ const onEvent = useCallback((e) => {
780
+ switch (e.type) {
781
+ case "progress":
782
+ setProgress(e.message);
783
+ break;
784
+ case "needs_confirmation":
785
+ setPhase("needs_confirmation");
786
+ setDimensions(e.dimensions);
787
+ setPkgType(e.pkgType ?? null);
788
+ setFileName(e.fileName ?? null);
789
+ break;
790
+ case "completed":
791
+ setPhase("completed");
792
+ setResult(e.job.result ?? {});
793
+ setProgress("");
794
+ break;
795
+ case "failed":
796
+ setPhase("failed");
797
+ setError(e.error);
798
+ setResult(e.result ?? {});
799
+ setProgress("");
800
+ break;
801
+ case "cancelled":
802
+ setPhase("cancelled");
803
+ setProgress("");
804
+ }
805
+ }, []);
806
+ return {
807
+ phase,
808
+ progress,
809
+ dimensions,
810
+ pkgType,
811
+ fileName,
812
+ result,
813
+ error,
814
+ jobId,
815
+ run: useCallback(async (req) => {
816
+ cleanup();
817
+ setPhase("running");
818
+ setProgress("");
819
+ setDimensions(null);
820
+ setPkgType(null);
821
+ setFileName(null);
822
+ setResult({});
823
+ setError("");
824
+ const job = await ports.startJob(req);
825
+ setJobId(job.id);
826
+ unsubRef.current = ports.jobEvents(job.id, onEvent);
827
+ }, [
828
+ ports,
829
+ cleanup,
830
+ onEvent
831
+ ]),
832
+ cancel: useCallback(async () => {
833
+ const id = jobId;
834
+ if (!id) return;
835
+ try {
836
+ await ports.abortJob(id);
837
+ } catch {}
838
+ cleanup();
839
+ setPhase("cancelled");
840
+ }, [
841
+ ports,
842
+ jobId,
843
+ cleanup
844
+ ]),
845
+ clear: useCallback(() => {
846
+ cleanup();
847
+ setPhase("idle");
848
+ setProgress("");
849
+ setDimensions(null);
850
+ setResult({});
851
+ setError("");
852
+ setJobId(null);
853
+ }, [cleanup]),
854
+ loadHistory: useCallback((entry) => {
855
+ cleanup();
856
+ setPhase("completed");
857
+ setProgress("");
858
+ setDimensions(null);
859
+ setPkgType(entry.input?.packageType ?? null);
860
+ setFileName(null);
861
+ setResult({
862
+ artifact: { id: entry.result?.artifactId },
863
+ filename: entry.result?.filename,
864
+ ...entry.input?.dimensions ? { dimensions: entry.input.dimensions } : {}
865
+ });
866
+ setError("");
867
+ setJobId(`history:${entry.id}`);
868
+ }, [cleanup])
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region src/pages/SymbolGenPage.tsx
873
+ /**
874
+ * `@huaqiu/component-gen-app` — Symbol generation page.
875
+ *
876
+ * Upload an image (required by the symbol-from-image generator) + optional
877
+ * instruction → `runGenerateSymbol` via the component-gen server → live
878
+ * progress → preview + download + history.
879
+ */
880
+ function SymbolGenPage({ ports, t, reopen = null }) {
881
+ const auth = useAuthGate(ports);
882
+ const runner = useJobRunner(ports);
883
+ const [config, setConfig] = useState(null);
884
+ const [imageDataUrl, setImageDataUrl] = useState(null);
885
+ const [file, setFile] = useState(null);
886
+ const [instruction, setInstruction] = useState("");
887
+ const runnerRef = useRef(runner);
888
+ runnerRef.current = runner;
889
+ const portsRef = useRef(ports);
890
+ portsRef.current = ports;
891
+ useEffect(() => {
892
+ if (!reopen) return;
893
+ const { entry } = reopen;
894
+ runnerRef.current.loadHistory(entry);
895
+ if (entry.input?.imageId) portsRef.current.inputImage(entry.input.imageId).then((dataUrl) => setImageDataUrl(dataUrl)).catch(() => {});
896
+ if (entry.input?.instruction) setInstruction(entry.input.instruction);
897
+ }, [reopen]);
898
+ useEffect(() => {
899
+ ports.config().then(setConfig).catch(() => {});
900
+ }, [ports]);
901
+ const maxBytes = config?.limits.imageBytes ?? 4194304;
902
+ const authed = auth.phase === "authenticated";
903
+ const canGenerate = authed && !!imageDataUrl && runner.phase !== "running";
904
+ const generate = () => {
905
+ if (!imageDataUrl) return;
906
+ runner.run({
907
+ kind: "symbol",
908
+ input: {
909
+ imageDataUrl,
910
+ ...instruction.trim() ? { instruction: instruction.trim() } : {}
911
+ }
912
+ });
913
+ };
914
+ const resultKey = useMemo(() => `${runner.jobId ?? "none"}:${runner.phase}`, [runner.jobId, runner.phase]);
915
+ return /* @__PURE__ */ jsxs("div", {
916
+ className: "cga-app",
917
+ children: [/* @__PURE__ */ jsx("div", {
918
+ className: "cga-panel",
919
+ children: /* @__PURE__ */ jsxs("div", {
920
+ className: "cga-panel__body",
921
+ children: [
922
+ auth.phase === "unknown" ? /* @__PURE__ */ jsxs("div", {
923
+ className: "cga-progress",
924
+ children: [/* @__PURE__ */ jsx("span", { className: "cga-spinner" }), t("app.loading")]
925
+ }) : null,
926
+ auth.phase === "unauthenticated" ? /* @__PURE__ */ jsxs("div", {
927
+ className: "cga-auth",
928
+ children: [/* @__PURE__ */ jsx("span", { children: t("auth.loginRequired") }), /* @__PURE__ */ jsx("button", {
929
+ type: "button",
930
+ className: "cga-btn",
931
+ onClick: auth.login,
932
+ children: t("auth.login")
933
+ })]
934
+ }) : null,
935
+ /* @__PURE__ */ jsx(UploadInput, {
936
+ maxBytes,
937
+ t,
938
+ disabled: !authed || runner.phase === "running",
939
+ imageDataUrl,
940
+ file,
941
+ onFile: (f, url) => {
942
+ setFile(f);
943
+ setImageDataUrl(url);
944
+ }
945
+ }),
946
+ /* @__PURE__ */ jsxs("div", {
947
+ className: "cga-field",
948
+ children: [/* @__PURE__ */ jsx("label", {
949
+ className: "cga-field__label",
950
+ children: t("symbol.instructionPlaceholder")
951
+ }), /* @__PURE__ */ jsx("input", {
952
+ className: "cga-field__input",
953
+ type: "text",
954
+ value: instruction,
955
+ disabled: !authed || runner.phase === "running",
956
+ onChange: (ev) => setInstruction(ev.target.value)
957
+ })]
958
+ }),
959
+ /* @__PURE__ */ jsxs("div", {
960
+ className: "cga-actions",
961
+ children: [/* @__PURE__ */ jsx("button", {
962
+ type: "button",
963
+ className: "cga-btn cga-btn--primary",
964
+ disabled: !canGenerate,
965
+ onClick: generate,
966
+ children: runner.phase === "running" ? t("symbol.progress") : runner.phase === "completed" ? t("symbol.regenerate") : t("symbol.generate")
967
+ }), runner.phase === "running" ? /* @__PURE__ */ jsx("button", {
968
+ type: "button",
969
+ className: "cga-btn",
970
+ onClick: () => void runner.cancel(),
971
+ children: t("editor.cancelLabel")
972
+ }) : null]
973
+ }),
974
+ runner.phase === "running" ? /* @__PURE__ */ jsxs("div", {
975
+ className: "cga-progress",
976
+ children: [/* @__PURE__ */ jsx("span", { className: "cga-spinner" }), runner.progress || t("symbol.progress")]
977
+ }) : null,
978
+ runner.phase === "failed" ? /* @__PURE__ */ jsxs("div", {
979
+ className: "cga-banner cga-banner--error",
980
+ children: [
981
+ t("symbol.failed"),
982
+ ": ",
983
+ runner.error,
984
+ runner.result?.status === "needs_auth" ? ` (${t("auth.loginRequired")})` : ""
985
+ ]
986
+ }) : null
987
+ ]
988
+ })
989
+ }), runner.phase === "completed" ? /* @__PURE__ */ jsx(ResultStage, {
990
+ ports,
991
+ kind: "symbol",
992
+ result: runner.result,
993
+ t,
994
+ srcKey: resultKey
995
+ }) : null]
996
+ });
997
+ }
998
+ //#endregion
999
+ //#region src/utils/dims.ts
1000
+ /**
1001
+ * Pure geometry/dimension model for the interactive footprint dimension
1002
+ * editor (rich-hit). Two-way-bound: the SVG package silhouette and the numeric
1003
+ * inputs are two views of the same `values` map. Drag a handle → values
1004
+ * change; type in an input → geometry re-renders. Only "confirm"/"cancel" go
1005
+ * back to the model.
1006
+ */
1007
+ /** Per-key mm bounds used to keep edited values sane. */
1008
+ const DIM_BOUNDS = {
1009
+ W: {
1010
+ min: .1,
1011
+ max: 500
1012
+ },
1013
+ L: {
1014
+ min: .1,
1015
+ max: 500
1016
+ },
1017
+ width: {
1018
+ min: .1,
1019
+ max: 500
1020
+ },
1021
+ height: {
1022
+ min: .1,
1023
+ max: 500
1024
+ },
1025
+ bodyWidth: {
1026
+ min: .1,
1027
+ max: 500
1028
+ },
1029
+ bodyLength: {
1030
+ min: .1,
1031
+ max: 500
1032
+ },
1033
+ pitch: {
1034
+ min: .05,
1035
+ max: 100
1036
+ }
1037
+ };
1038
+ function dimensionBounds(key) {
1039
+ return DIM_BOUNDS[key] ?? {
1040
+ min: .01,
1041
+ max: 1e3
1042
+ };
1043
+ }
1044
+ function clampDimension(value, min, max) {
1045
+ const n = Number(value);
1046
+ if (!Number.isFinite(n)) return min;
1047
+ if (n < min) return min;
1048
+ if (n > max) return max;
1049
+ return n;
1050
+ }
1051
+ /** Parse a dimension string (allows a trailing unit like "6.2mm"). Null when invalid. */
1052
+ function parseDimension(text) {
1053
+ const m = text.trim().match(/^([+-]?(\d+(\.\d+)?|\.\d+))([^0-9]*)$/);
1054
+ if (!m) return null;
1055
+ const n = Number(m[1]);
1056
+ return Number.isFinite(n) ? n : null;
1057
+ }
1058
+ /** Format a number for display: max 2 decimals, no trailing zeros. */
1059
+ function formatDimension(value) {
1060
+ if (typeof value !== "number" || !Number.isFinite(value)) return "";
1061
+ return String(Math.round(value * 100) / 100);
1062
+ }
1063
+ /**
1064
+ * Pick the geometry keys among a dimension map: a width-like key and a
1065
+ * height-like key (rendered as the draggable board outline) and the rest.
1066
+ */
1067
+ function pickGeometry(dimensions) {
1068
+ let widthKey = null;
1069
+ let heightKey = null;
1070
+ const numericKeys = [];
1071
+ const widthCandidates = [
1072
+ "W",
1073
+ "width",
1074
+ "Width",
1075
+ "D",
1076
+ "d_max",
1077
+ "d_min",
1078
+ "bodyWidth",
1079
+ "body_width",
1080
+ "bodyLength",
1081
+ "body_length",
1082
+ "boardWidth",
1083
+ "E",
1084
+ "e_max",
1085
+ "e_min"
1086
+ ];
1087
+ const heightCandidates = [
1088
+ "H",
1089
+ "L",
1090
+ "height",
1091
+ "Height",
1092
+ "Length",
1093
+ "E",
1094
+ "e_max",
1095
+ "e_min",
1096
+ "bodyLength",
1097
+ "body_length",
1098
+ "bodyWidth",
1099
+ "body_width",
1100
+ "boardHeight",
1101
+ "D",
1102
+ "d_max",
1103
+ "d_min"
1104
+ ];
1105
+ for (const k of Object.keys(dimensions)) {
1106
+ const v = dimensions[k];
1107
+ if ((typeof v === "number" ? v : typeof v === "string" ? parseDimension(v) : null) == null) continue;
1108
+ numericKeys.push(k);
1109
+ }
1110
+ for (const c of widthCandidates) if (!widthKey && dimensions[c] != null) widthKey = c;
1111
+ for (const c of heightCandidates) if (c !== widthKey && dimensions[c] != null) {
1112
+ heightKey = c;
1113
+ break;
1114
+ }
1115
+ const otherKeys = numericKeys.filter((k) => k !== widthKey && k !== heightKey);
1116
+ return {
1117
+ widthKey,
1118
+ heightKey,
1119
+ otherKeys,
1120
+ numericKeys
1121
+ };
1122
+ }
1123
+ /** Normalize a dimension map into numeric values + geometry key picks. */
1124
+ function normalizeDimensions(dimensions) {
1125
+ const values = {};
1126
+ if (dimensions && typeof dimensions === "object" && !Array.isArray(dimensions)) for (const k of Object.keys(dimensions)) {
1127
+ const v = dimensions[k];
1128
+ const n = typeof v === "number" ? v : typeof v === "string" ? parseDimension(v) : null;
1129
+ if (n != null) values[k] = n;
1130
+ }
1131
+ return {
1132
+ values,
1133
+ ...pickGeometry(values)
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Fit a W×H rectangle into a viewBox, preserving aspect ratio and leaving
1138
+ * padding for the dimension lines. Returns {x,y,w,h} in viewBox units.
1139
+ */
1140
+ function rectFromValues(values, widthKey, heightKey, viewW, viewH, pad) {
1141
+ let W = widthKey && values[widthKey] != null ? values[widthKey] : 1;
1142
+ let H = heightKey && values[heightKey] != null ? values[heightKey] : 1;
1143
+ if (W <= 0) W = 1;
1144
+ if (H <= 0) H = 1;
1145
+ const availW = Math.max(1, viewW - pad * 2);
1146
+ const availH = Math.max(1, viewH - pad * 2);
1147
+ const scale = Math.min(availW / W, availH / H);
1148
+ const w = W * scale;
1149
+ const h = H * scale;
1150
+ return {
1151
+ x: (viewW - w) / 2,
1152
+ y: (viewH - h) / 2,
1153
+ w,
1154
+ h
1155
+ };
1156
+ }
1157
+ /** First positive numeric value across alias keys, else the fallback. */
1158
+ function numVal(values, keys, fallback) {
1159
+ for (const key of keys) {
1160
+ const v = values[key];
1161
+ const n = typeof v === "number" ? v : typeof v === "string" ? parseDimension(v) : null;
1162
+ if (n != null && Number.isFinite(n) && n > 0) return n;
1163
+ }
1164
+ return fallback;
1165
+ }
1166
+ /** Pin count from the dimension map (aliases: pin_count, pins, pinCount, n_max…). */
1167
+ function pinCountOf(values, fallback) {
1168
+ const n = numVal(values, [
1169
+ "pin_count",
1170
+ "pins",
1171
+ "pinCount",
1172
+ "n_max",
1173
+ "n"
1174
+ ], -1);
1175
+ return n >= 0 ? Math.max(2, Math.round(n)) : fallback;
1176
+ }
1177
+ /** BGA ball-grid rows/columns (aliases: rows, columns, cols). */
1178
+ function bgaGrid(values, fallback) {
1179
+ let rows = Math.round(numVal(values, ["rows", "row"], fallback));
1180
+ let cols = Math.round(numVal(values, [
1181
+ "columns",
1182
+ "col",
1183
+ "cols"
1184
+ ], fallback));
1185
+ if (!(rows > 0)) rows = fallback;
1186
+ if (!(cols > 0)) cols = fallback;
1187
+ return {
1188
+ rows,
1189
+ cols
1190
+ };
1191
+ }
1192
+ /** Human-readable package-family label for the editor's info row. */
1193
+ function pkgFamilyLabel(pkgType) {
1194
+ return String(pkgType || "").toUpperCase();
1195
+ }
1196
+ const PITCH_KEYS = /* @__PURE__ */ new Set([
1197
+ "pitch",
1198
+ "pitch_d",
1199
+ "pitch_e",
1200
+ "lead_pitch",
1201
+ "pitch_x",
1202
+ "e"
1203
+ ]);
1204
+ const PIN_KEYS = /* @__PURE__ */ new Set([
1205
+ "pin_count",
1206
+ "pins",
1207
+ "pinCount",
1208
+ "total_pins"
1209
+ ]);
1210
+ /**
1211
+ * Split the numeric keys into the small "essential" set (body W/H, pitch,
1212
+ * pin count) and the rest, which fold into a collapsible "Advanced" section.
1213
+ */
1214
+ function classifyDimensions(numericKeys, widthKey, heightKey) {
1215
+ const essential = [];
1216
+ const advanced = [];
1217
+ for (const k of numericKeys) if (k === widthKey || k === heightKey || PITCH_KEYS.has(k) || PIN_KEYS.has(k)) {
1218
+ if (!essential.includes(k)) essential.push(k);
1219
+ } else if (!advanced.includes(k)) advanced.push(k);
1220
+ return {
1221
+ essential,
1222
+ advanced
1223
+ };
1224
+ }
1225
+ /**
1226
+ * Find the min/max tolerance partner for a body dimension key, e.g.
1227
+ * `d_max` → `d_min`. Returns { min, max } or null when no partner exists.
1228
+ */
1229
+ function toleranceOf(values, key) {
1230
+ if (!key || !values || typeof values !== "object") return null;
1231
+ let lo = null;
1232
+ let hi = null;
1233
+ if (/_(max|min)$/.test(key)) {
1234
+ const base = key.replace(/_(max|min)$/, "");
1235
+ const loKey = `${base}_min`;
1236
+ const hiKey = `${base}_max`;
1237
+ const loV = values[loKey];
1238
+ const hiV = values[hiKey];
1239
+ if (loV != null) lo = loV;
1240
+ if (hiV != null) hi = hiV;
1241
+ } else {
1242
+ const loV = values[`${key}_min`];
1243
+ const hiV = values[`${key}_max`];
1244
+ if (loV != null) lo = loV;
1245
+ if (hiV != null) hi = hiV;
1246
+ }
1247
+ if (lo == null && hi == null) return null;
1248
+ return {
1249
+ min: lo != null ? lo : hi,
1250
+ max: hi != null ? hi : lo
1251
+ };
1252
+ }
1253
+ /** Structural validation of the dimension set. */
1254
+ function validateDimensions(values) {
1255
+ const issues = [];
1256
+ for (const key of Object.keys(values)) {
1257
+ const v = values[key];
1258
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1259
+ issues.push({
1260
+ key,
1261
+ code: "invalid"
1262
+ });
1263
+ continue;
1264
+ }
1265
+ const b = dimensionBounds(key);
1266
+ if (v < b.min || v > b.max) issues.push({
1267
+ key,
1268
+ code: "out_of_range"
1269
+ });
1270
+ }
1271
+ for (const [lo, hi] of [
1272
+ ["d_min", "d_max"],
1273
+ ["e_min", "e_max"],
1274
+ ["a_min", "a_max"],
1275
+ ["b_min", "b_max"],
1276
+ ["l_min", "l_max"]
1277
+ ]) {
1278
+ const loV = values[lo];
1279
+ const hiV = values[hi];
1280
+ if (loV != null && hiV != null && loV > hiV) issues.push({
1281
+ key: hi,
1282
+ code: "min_gt_max"
1283
+ });
1284
+ }
1285
+ return issues;
1286
+ }
1287
+ /** Compact summary used in the package info row: body W×H, pitch and pins. */
1288
+ function summaryOf(values, widthKey, heightKey, t) {
1289
+ const parts = [];
1290
+ const bodyW = widthKey && values[widthKey] != null ? formatDimension(values[widthKey]) : null;
1291
+ const bodyH = heightKey && values[heightKey] != null ? formatDimension(values[heightKey]) : null;
1292
+ if (bodyW != null && bodyH != null) parts.push(`${t("card.editor.body")} ${bodyW} \u00d7 ${bodyH} ${t("card.editor.unit")}`);
1293
+ const pitch = numVal(values, [
1294
+ "pitch",
1295
+ "pitch_d",
1296
+ "pitch_e",
1297
+ "lead_pitch",
1298
+ "pitch_x",
1299
+ "e"
1300
+ ], -1);
1301
+ if (pitch >= 0) parts.push(`${t("card.editor.pitch")} ${formatDimension(pitch)} ${t("card.editor.unit")}`);
1302
+ return parts;
1303
+ }
1304
+ //#endregion
1305
+ //#region src/utils/labels.ts
1306
+ /** Humanize a camelCase / snake_case key for display. */
1307
+ function humanizeKey(key) {
1308
+ return String(key).replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^\w/, (c) => c.toUpperCase());
1309
+ }
1310
+ /** canonical (lowercased, separators stripped) dimension key → copy key. */
1311
+ const FIELD_LABEL_KEY = {
1312
+ w: "field.width",
1313
+ width: "field.width",
1314
+ h: "field.height",
1315
+ height: "field.height",
1316
+ l: "field.length",
1317
+ length: "field.length",
1318
+ d: "field.depth",
1319
+ depth: "field.depth",
1320
+ e: "field.span",
1321
+ span: "field.span",
1322
+ bodywidth: "field.bodyWidth",
1323
+ bodylength: "field.bodyLength",
1324
+ bodyheight: "field.bodyHeight",
1325
+ boardwidth: "field.boardWidth",
1326
+ boardheight: "field.boardHeight",
1327
+ overallwidth: "field.overallWidth",
1328
+ overalllength: "field.overallLength",
1329
+ overallheight: "field.overallHeight",
1330
+ pitch: "field.pitch",
1331
+ pitchx: "field.pitchX",
1332
+ pitchy: "field.pitchY",
1333
+ pitchd: "field.pitch",
1334
+ pitche: "field.pitch",
1335
+ leadpitch: "field.leadPitch",
1336
+ padwidth: "field.padWidth",
1337
+ padlength: "field.padLength",
1338
+ padheight: "field.padHeight",
1339
+ leadwidth: "field.leadWidth",
1340
+ leadlength: "field.leadLength",
1341
+ leadspan: "field.leadSpan",
1342
+ pincount: "field.pinCount",
1343
+ pins: "field.pinCount",
1344
+ totalpins: "field.pinCount",
1345
+ n: "field.pinCount",
1346
+ nmax: "field.pinCount",
1347
+ rows: "field.rows",
1348
+ row: "field.rows",
1349
+ columns: "field.columns",
1350
+ column: "field.columns",
1351
+ col: "field.columns",
1352
+ cols: "field.columns",
1353
+ standoff: "field.standoff"
1354
+ };
1355
+ /** Localized label for a dimension key (`w`/`width` → 宽度, …). */
1356
+ function fieldLabel(key, t) {
1357
+ const raw = String(key);
1358
+ const bounds = /^(.*?)_(max|min)$/i.exec(raw);
1359
+ if (bounds) {
1360
+ const base = fieldLabel(bounds[1], t);
1361
+ return t(bounds[2].toLowerCase() === "max" ? "field.maxOf" : "field.minOf", { field: base });
1362
+ }
1363
+ const canonical = raw.toLowerCase().replace(/[\s_-]+/g, "");
1364
+ const copyKey = FIELD_LABEL_KEY[canonical];
1365
+ return copyKey ? t(copyKey) : humanizeKey(raw);
1366
+ }
1367
+ //#endregion
1368
+ //#region src/components/GeometryEditor.tsx
1369
+ /**
1370
+ * `@huaqiu/component-gen-app` — interactive package-dimension editor.
1371
+ *
1372
+ * Faithful port of the `dsh-tool-symbol-footprint` rich-HIT geometry editor
1373
+ * (packageSilhouette + pointerToViewBox + the two-way-bound W/H handle + field
1374
+ * editor). DSH-agnostic: `onConfirm(values, edited)` / `onCancel()` are the
1375
+ * only ways back to the caller — the app itself is the driver (single-HIL).
1376
+ */
1377
+ function pointerToViewBox(svg, clientX, clientY, viewW, viewH) {
1378
+ const rect = svg.getBoundingClientRect();
1379
+ return {
1380
+ x: rect.width > 0 ? (clientX - rect.left) * (viewW / rect.width) : 0,
1381
+ y: rect.height > 0 ? (clientY - rect.top) * (viewH / rect.height) : 0
1382
+ };
1383
+ }
1384
+ const COUNT_KEYS = /* @__PURE__ */ new Set([
1385
+ "pin_count",
1386
+ "pins",
1387
+ "pinCount",
1388
+ "rows",
1389
+ "row",
1390
+ "columns",
1391
+ "column",
1392
+ "cols",
1393
+ "n_max",
1394
+ "n",
1395
+ "count",
1396
+ "total_pins"
1397
+ ]);
1398
+ function packageSilhouette(pkgType, values, geom) {
1399
+ const type = String(pkgType || "").toLowerCase();
1400
+ const bodyX = geom.x;
1401
+ const bodyY = geom.y;
1402
+ const bodyW = geom.w;
1403
+ const bodyH = geom.h;
1404
+ const padL = Math.max(2, Math.min(7, Math.round(Math.min(bodyW, bodyH) * .07)));
1405
+ const nodes = [];
1406
+ nodes.push(/* @__PURE__ */ jsx("rect", {
1407
+ className: "hq-genhit__body",
1408
+ x: bodyX,
1409
+ y: bodyY,
1410
+ width: bodyW,
1411
+ height: bodyH,
1412
+ rx: 2
1413
+ }, "body"));
1414
+ const pins = pinCountOf(values, 8);
1415
+ const perSide = Math.max(2, Math.ceil(pins / 2));
1416
+ const side = (edge) => {
1417
+ const arr = [];
1418
+ if (edge === "L" || edge === "R") {
1419
+ const len = Math.min(padL, bodyW * .22);
1420
+ const x0 = edge === "L" ? bodyX - len : bodyX + bodyW;
1421
+ for (let i = 0; i < perSide; i++) {
1422
+ const y = bodyY + (i + .5) * (bodyH / perSide);
1423
+ const ph = Math.max(1.6, bodyH / perSide * .55);
1424
+ arr.push(/* @__PURE__ */ jsx("rect", {
1425
+ className: "hq-genhit__pad",
1426
+ x: x0,
1427
+ y: y - ph / 2,
1428
+ width: len,
1429
+ height: ph,
1430
+ rx: 1
1431
+ }, edge + i));
1432
+ }
1433
+ } else {
1434
+ const len2 = Math.min(padL, bodyH * .22);
1435
+ const y0 = edge === "T" ? bodyY - len2 : bodyY + bodyH;
1436
+ for (let j = 0; j < perSide; j++) {
1437
+ const x = bodyX + (j + .5) * (bodyW / perSide);
1438
+ const pw = Math.max(1.6, bodyW / perSide * .55);
1439
+ arr.push(/* @__PURE__ */ jsx("rect", {
1440
+ className: "hq-genhit__pad",
1441
+ x: x - pw / 2,
1442
+ y: y0,
1443
+ width: pw,
1444
+ height: len2,
1445
+ rx: 1
1446
+ }, edge + j));
1447
+ }
1448
+ }
1449
+ return arr;
1450
+ };
1451
+ if (type === "bga") {
1452
+ const g = bgaGrid(values, Math.max(2, Math.round(Math.sqrt(pins))));
1453
+ const ballR = Math.max(.9, Math.min(bodyW, bodyH) / (Math.max(g.rows, g.cols) * 3.4));
1454
+ for (let r = 0; r < g.rows; r++) for (let c = 0; c < g.cols; c++) nodes.push(/* @__PURE__ */ jsx("circle", {
1455
+ className: "hq-genhit__ball",
1456
+ cx: bodyX + (c + .5) * (bodyW / g.cols),
1457
+ cy: bodyY + (r + .5) * (bodyH / g.rows),
1458
+ r: ballR
1459
+ }, `ball${r}_${c}`));
1460
+ } else if (type === "qfn" || type === "son") {
1461
+ const ep = Math.min(bodyW * .55, bodyH * .55);
1462
+ nodes.push(/* @__PURE__ */ jsx("rect", {
1463
+ className: "hq-genhit__epad",
1464
+ x: bodyX + (bodyW - ep) / 2,
1465
+ y: bodyY + (bodyH - ep) / 2,
1466
+ width: ep,
1467
+ height: ep,
1468
+ rx: 1
1469
+ }, "epad"));
1470
+ const edges = type === "son" ? ["L", "R"] : [
1471
+ "L",
1472
+ "R",
1473
+ "T",
1474
+ "B"
1475
+ ];
1476
+ for (const e of edges) nodes.push(...side(e));
1477
+ } else if (type === "qfp" || type === "plcc") nodes.push(...side("L"), ...side("R"), ...side("T"), ...side("B"));
1478
+ else nodes.push(...side("L"), ...side("R"));
1479
+ return nodes;
1480
+ }
1481
+ /** dims.ts `summaryOf` addresses the plugin's `card.editor.*` keys — remap. */
1482
+ function editorT(t) {
1483
+ return (key, params) => t(key.replace(/^card\.editor\./, "editor."), params);
1484
+ }
1485
+ const GeometryEditor = memo(function GeometryEditor(props) {
1486
+ const { dimensions, pkgType = null, fileName = null, disabled = false, t, onConfirm, onCancel } = props;
1487
+ const normalized = useMemo(() => normalizeDimensions(dimensions), [dimensions]);
1488
+ const { widthKey, heightKey } = normalized;
1489
+ const [values, setValues] = useState(() => normalized.values);
1490
+ const [fieldText, setFieldText] = useState(() => {
1491
+ const m = {};
1492
+ for (const k of normalized.numericKeys) m[k] = formatDimension(normalized.values[k]);
1493
+ return m;
1494
+ });
1495
+ const [invalid, setInvalid] = useState({});
1496
+ const [edited, setEdited] = useState({});
1497
+ const [advancedOpen, setAdvancedOpen] = useState(false);
1498
+ const [drag, setDrag] = useState(null);
1499
+ const svgRef = useRef(null);
1500
+ const VIEW_W = 360;
1501
+ const VIEW_H = 230;
1502
+ const geom = rectFromValues(values, widthKey, heightKey, VIEW_W, VIEW_H, 34);
1503
+ const labelFor = (key) => fieldLabel(key, t);
1504
+ const fieldUnit = (key) => COUNT_KEYS.has(key) ? "" : t("editor.unit");
1505
+ const withKey = (obj, key, value) => {
1506
+ const out = { ...obj };
1507
+ out[key] = value;
1508
+ return out;
1509
+ };
1510
+ const withKeyBool = (obj, key, value) => {
1511
+ const out = { ...obj };
1512
+ out[key] = value;
1513
+ return out;
1514
+ };
1515
+ function startDrag(mode) {
1516
+ return (ev) => {
1517
+ if (disabled || !svgRef.current) return;
1518
+ ev.preventDefault();
1519
+ const view = pointerToViewBox(svgRef.current, ev.clientX, ev.clientY, VIEW_W, VIEW_H);
1520
+ setDrag({
1521
+ mode,
1522
+ startX: view.x,
1523
+ startY: view.y,
1524
+ startW: widthKey && values[widthKey] != null ? values[widthKey] : 1,
1525
+ startH: heightKey && values[heightKey] != null ? values[heightKey] : 1,
1526
+ rectW: geom.w,
1527
+ rectH: geom.h
1528
+ });
1529
+ try {
1530
+ svgRef.current.setPointerCapture(ev.pointerId);
1531
+ } catch {}
1532
+ };
1533
+ }
1534
+ function onPointerMove(ev) {
1535
+ if (!drag || !svgRef.current) return;
1536
+ const view = pointerToViewBox(svgRef.current, ev.clientX, ev.clientY, VIEW_W, VIEW_H);
1537
+ let next = values;
1538
+ let nextEdited = edited;
1539
+ if (drag.mode === "W" || drag.mode === "WH") {
1540
+ if (widthKey) {
1541
+ const bW = dimensionBounds(widthKey);
1542
+ const newW = clampDimension(drag.startW + (view.x - drag.startX) * (drag.startW / drag.rectW), bW.min, bW.max);
1543
+ next = withKey(next, widthKey, newW);
1544
+ nextEdited = withKeyBool(nextEdited, widthKey, true);
1545
+ }
1546
+ }
1547
+ if (drag.mode === "H" || drag.mode === "WH") {
1548
+ if (heightKey) {
1549
+ const bH = dimensionBounds(heightKey);
1550
+ const newH = clampDimension(drag.startH + (view.y - drag.startY) * (drag.startH / drag.rectH), bH.min, bH.max);
1551
+ next = withKey(next, heightKey, newH);
1552
+ nextEdited = withKeyBool(nextEdited, heightKey, true);
1553
+ }
1554
+ }
1555
+ if (next !== values) setValues(next);
1556
+ if (nextEdited !== edited) setEdited(nextEdited);
1557
+ }
1558
+ function endDrag() {
1559
+ setDrag(null);
1560
+ }
1561
+ function onFieldChange(key, ev) {
1562
+ const raw = ev.target.value;
1563
+ setFieldText((prev) => ({
1564
+ ...prev,
1565
+ [key]: raw
1566
+ }));
1567
+ const n = parseDimension(raw);
1568
+ if (n == null) {
1569
+ setInvalid((prev) => ({
1570
+ ...prev,
1571
+ [key]: true
1572
+ }));
1573
+ return;
1574
+ }
1575
+ const bounds = dimensionBounds(key);
1576
+ setInvalid((prev) => ({
1577
+ ...prev,
1578
+ [key]: false
1579
+ }));
1580
+ setValues((prev) => withKey(prev, key, clampDimension(n, bounds.min, bounds.max)));
1581
+ setEdited((prev) => withKeyBool(prev, key, true));
1582
+ }
1583
+ function onFieldBlur(key) {
1584
+ setFieldText((prev) => ({
1585
+ ...prev,
1586
+ [key]: formatDimension(values[key])
1587
+ }));
1588
+ setInvalid((prev) => ({
1589
+ ...prev,
1590
+ [key]: false
1591
+ }));
1592
+ }
1593
+ function focusField(key) {
1594
+ if (disabled || typeof document === "undefined") return;
1595
+ const el = document.querySelector(`.hq-genhit__field-input[data-field="${key}"]`);
1596
+ if (el && el.focus) {
1597
+ el.focus();
1598
+ el.scrollIntoView({
1599
+ block: "nearest",
1600
+ behavior: "smooth"
1601
+ });
1602
+ }
1603
+ }
1604
+ const hasInvalid = Object.values(invalid).some(Boolean);
1605
+ const topY = geom.y - 16;
1606
+ const leftX = geom.x - 16;
1607
+ const wMid = geom.x + geom.w / 2;
1608
+ const hMid = geom.y + geom.h / 2;
1609
+ const wLabel = widthKey && values[widthKey] != null ? `${formatDimension(values[widthKey])} ${t("editor.unit")}` : "";
1610
+ const hLabel = heightKey && values[heightKey] != null ? `${formatDimension(values[heightKey])} ${t("editor.unit")}` : "";
1611
+ const arrow = (pts) => /* @__PURE__ */ jsx("polygon", {
1612
+ className: "hq-genhit__arrow",
1613
+ points: pts
1614
+ });
1615
+ const tolText = (key) => {
1616
+ const tol = toleranceOf(values, key);
1617
+ if (!tol || tol.min == null || tol.max == null || tol.min === tol.max) return "";
1618
+ return `${formatDimension(tol.min)}\u2013${formatDimension(tol.max)} ${t("editor.unit")}`;
1619
+ };
1620
+ const wTolText = tolText(widthKey);
1621
+ const hTolText = tolText(heightKey);
1622
+ const groups = classifyDimensions(normalized.numericKeys, widthKey, heightKey);
1623
+ const essentialKeys = [];
1624
+ if (widthKey) essentialKeys.push(widthKey);
1625
+ if (heightKey) essentialKeys.push(heightKey);
1626
+ for (const ek of groups.essential) if (ek !== widthKey && ek !== heightKey) essentialKeys.push(ek);
1627
+ const advancedKeys = groups.advanced;
1628
+ const renderField = (key) => {
1629
+ const bounds = dimensionBounds(key);
1630
+ return /* @__PURE__ */ jsxs("div", {
1631
+ className: `hq-genhit__field${invalid[key] ? " hq-genhit__field--invalid" : ""}`,
1632
+ title: invalid[key] ? t("editor.validationInvalid") : void 0,
1633
+ children: [
1634
+ /* @__PURE__ */ jsx("label", {
1635
+ className: `hq-genhit__field-label${edited[key] ? " hq-genhit__field-label--edited" : ""}`,
1636
+ onClick: () => focusField(key),
1637
+ children: labelFor(key)
1638
+ }),
1639
+ edited[key] ? /* @__PURE__ */ jsx("span", {
1640
+ className: "hq-genhit__field-tag hq-genhit__field-tag--edited",
1641
+ children: t("editor.editedTag")
1642
+ }) : /* @__PURE__ */ jsx("span", {
1643
+ className: "hq-genhit__field-tag hq-genhit__field-tag--ai",
1644
+ children: t("editor.aiTag")
1645
+ }),
1646
+ /* @__PURE__ */ jsx("input", {
1647
+ className: "hq-genhit__field-input",
1648
+ "data-field": key,
1649
+ type: "number",
1650
+ inputMode: "decimal",
1651
+ min: bounds.min,
1652
+ max: bounds.max,
1653
+ step: .1,
1654
+ value: fieldText[key] == null ? "" : fieldText[key],
1655
+ disabled,
1656
+ onChange: (ev) => onFieldChange(key, ev),
1657
+ onBlur: () => onFieldBlur(key)
1658
+ }),
1659
+ /* @__PURE__ */ jsx("span", {
1660
+ className: "hq-genhit__field-unit",
1661
+ children: fieldUnit(key)
1662
+ })
1663
+ ]
1664
+ }, key);
1665
+ };
1666
+ const issues = validateDimensions(values);
1667
+ const seen = /* @__PURE__ */ new Set();
1668
+ const issueTexts = issues.filter((it) => {
1669
+ if (seen.has(it.key)) return false;
1670
+ seen.add(it.key);
1671
+ return true;
1672
+ }).map((it) => {
1673
+ const codeLabel = it.code === "min_gt_max" ? t("editor.issueMinGtMax") : it.code === "out_of_range" ? t("editor.issueOutOfRange") : t("editor.validationInvalid");
1674
+ return `${labelFor(it.key)}: ${codeLabel}`;
1675
+ });
1676
+ const totalIssues = issueTexts.length + (hasInvalid ? 1 : 0);
1677
+ const canEditGeometry = !!(widthKey && heightKey);
1678
+ const summaryParts = summaryOf(values, widthKey, heightKey, editorT(t));
1679
+ return /* @__PURE__ */ jsxs("div", {
1680
+ className: "hq-genhit__editor",
1681
+ children: [
1682
+ canEditGeometry ? /* @__PURE__ */ jsxs("svg", {
1683
+ ref: svgRef,
1684
+ className: "hq-genhit__geom",
1685
+ viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
1686
+ onPointerMove,
1687
+ onPointerUp: endDrag,
1688
+ onPointerCancel: endDrag,
1689
+ children: [
1690
+ /* @__PURE__ */ jsx("line", {
1691
+ className: "hq-genhit__dimline",
1692
+ x1: geom.x,
1693
+ y1: topY,
1694
+ x2: geom.x + geom.w,
1695
+ y2: topY
1696
+ }),
1697
+ arrow(`${geom.x},${topY} ${geom.x + 7},${topY - 3} ${geom.x + 7},${topY + 3}`),
1698
+ arrow(`${geom.x + geom.w},${topY} ${geom.x + geom.w - 7},${topY - 3} ${geom.x + geom.w - 7},${topY + 3}`),
1699
+ /* @__PURE__ */ jsx("text", {
1700
+ className: `hq-genhit__dimlabel${widthKey ? " hq-genhit__dimlabel--clickable" : ""}`,
1701
+ x: wMid,
1702
+ y: topY - 8,
1703
+ textAnchor: "middle",
1704
+ onClick: widthKey ? () => focusField(widthKey) : void 0,
1705
+ children: wLabel
1706
+ }),
1707
+ wTolText ? /* @__PURE__ */ jsx("text", {
1708
+ className: "hq-genhit__tol",
1709
+ x: wMid,
1710
+ y: topY + 12,
1711
+ textAnchor: "middle",
1712
+ children: wTolText
1713
+ }) : null,
1714
+ /* @__PURE__ */ jsx("line", {
1715
+ className: "hq-genhit__dimline",
1716
+ x1: leftX,
1717
+ y1: geom.y,
1718
+ x2: leftX,
1719
+ y2: geom.y + geom.h
1720
+ }),
1721
+ arrow(`${leftX},${geom.y} ${leftX - 3},${geom.y + 7} ${leftX + 3},${geom.y + 7}`),
1722
+ arrow(`${leftX},${geom.y + geom.h} ${leftX - 3},${geom.y + geom.h - 7} ${leftX + 3},${geom.y + geom.h - 7}`),
1723
+ /* @__PURE__ */ jsx("text", {
1724
+ className: `hq-genhit__dimlabel${heightKey ? " hq-genhit__dimlabel--clickable" : ""}`,
1725
+ x: leftX - 8,
1726
+ y: hMid,
1727
+ textAnchor: "middle",
1728
+ transform: `rotate(-90 ${leftX - 8} ${hMid})`,
1729
+ onClick: heightKey ? () => focusField(heightKey) : void 0,
1730
+ children: hLabel
1731
+ }),
1732
+ hTolText ? /* @__PURE__ */ jsx("text", {
1733
+ className: "hq-genhit__tol",
1734
+ x: leftX + 12,
1735
+ y: hMid,
1736
+ textAnchor: "middle",
1737
+ transform: `rotate(-90 ${leftX + 12} ${hMid})`,
1738
+ children: hTolText
1739
+ }) : null,
1740
+ packageSilhouette(pkgType, values, geom).map((node, ni) => /* @__PURE__ */ jsx(Fragment, { children: node }, node.key ?? `sil${ni}`)),
1741
+ /* @__PURE__ */ jsx("circle", {
1742
+ className: "hq-genhit__handle",
1743
+ cx: geom.x + geom.w,
1744
+ cy: geom.y + geom.h / 2,
1745
+ r: 6,
1746
+ onPointerDown: startDrag("W")
1747
+ }),
1748
+ /* @__PURE__ */ jsx("circle", {
1749
+ className: "hq-genhit__handle hq-genhit__handle--h",
1750
+ cx: geom.x + geom.w / 2,
1751
+ cy: geom.y + geom.h,
1752
+ r: 6,
1753
+ onPointerDown: startDrag("H")
1754
+ }),
1755
+ /* @__PURE__ */ jsx("circle", {
1756
+ className: "hq-genhit__handle hq-genhit__handle--wh",
1757
+ cx: geom.x + geom.w,
1758
+ cy: geom.y + geom.h,
1759
+ r: 7,
1760
+ onPointerDown: startDrag("WH")
1761
+ })
1762
+ ]
1763
+ }) : null,
1764
+ canEditGeometry ? /* @__PURE__ */ jsx("div", {
1765
+ className: "hq-genhit__drag-hint",
1766
+ children: t("editor.dragHint")
1767
+ }) : null,
1768
+ /* @__PURE__ */ jsxs("div", {
1769
+ className: "hq-genhit__pkg",
1770
+ children: [
1771
+ pkgFamilyLabel(pkgType) ? /* @__PURE__ */ jsx("span", {
1772
+ className: "hq-genhit__badge hq-genhit__badge--pkg",
1773
+ children: pkgFamilyLabel(pkgType)
1774
+ }) : null,
1775
+ /* @__PURE__ */ jsx("span", {
1776
+ className: "hq-genhit__pkg-meta",
1777
+ children: t("editor.pins", { count: pinCountOf(values, 0) })
1778
+ }),
1779
+ summaryParts.map((part, pi) => /* @__PURE__ */ jsxs("span", {
1780
+ className: "hq-genhit__pkg-meta hq-genhit__pkg-meta--sep",
1781
+ children: ["· ", part]
1782
+ }, `sum${pi}`))
1783
+ ]
1784
+ }),
1785
+ essentialKeys.length > 0 ? /* @__PURE__ */ jsx("div", {
1786
+ className: "hq-genhit__fields",
1787
+ children: essentialKeys.map(renderField)
1788
+ }) : null,
1789
+ advancedKeys.length > 0 ? /* @__PURE__ */ jsxs("div", {
1790
+ className: "hq-genhit__adv",
1791
+ children: [/* @__PURE__ */ jsx("button", {
1792
+ type: "button",
1793
+ className: "hq-genhit__adv-toggle",
1794
+ onClick: () => setAdvancedOpen(!advancedOpen),
1795
+ "aria-expanded": advancedOpen ? "true" : "false",
1796
+ children: (advancedOpen ? "▾ " : "▸ ") + t("editor.advanced") + ` (${advancedKeys.length})`
1797
+ }), advancedOpen ? /* @__PURE__ */ jsx("div", {
1798
+ className: "hq-genhit__fields hq-genhit__fields--adv",
1799
+ children: advancedKeys.map(renderField)
1800
+ }) : null]
1801
+ }) : null,
1802
+ /* @__PURE__ */ jsx("div", {
1803
+ className: `hq-genhit__validation${totalIssues > 0 ? " hq-genhit__validation--warn" : ""}`,
1804
+ children: totalIssues > 0 ? /* @__PURE__ */ jsxs("span", { children: [t("editor.validationIssue", { n: totalIssues }), issueTexts.length > 0 ? /* @__PURE__ */ jsx("span", {
1805
+ className: "hq-genhit__validation-detail",
1806
+ children: issueTexts[0]
1807
+ }) : null] }) : /* @__PURE__ */ jsx("span", { children: t("editor.validationOk") })
1808
+ }),
1809
+ /* @__PURE__ */ jsxs("div", {
1810
+ className: "hq-genhit__actions",
1811
+ children: [/* @__PURE__ */ jsxs("button", {
1812
+ type: "button",
1813
+ className: "hq-genhit__act",
1814
+ onClick: () => onConfirm(values, edited),
1815
+ disabled: disabled || hasInvalid,
1816
+ children: ["✓ ", t("editor.confirmLabel")]
1817
+ }), /* @__PURE__ */ jsxs("button", {
1818
+ type: "button",
1819
+ className: "hq-genhit__act",
1820
+ onClick: onCancel,
1821
+ disabled,
1822
+ children: ["✕ ", t("editor.cancelLabel")]
1823
+ })]
1824
+ })
1825
+ ]
1826
+ });
1827
+ });
1828
+ //#endregion
1829
+ //#region src/pages/FootprintGenPage.tsx
1830
+ /**
1831
+ * `@huaqiu/component-gen-app` — Footprint generation page.
1832
+ *
1833
+ * Two workflows, both driven by the app itself (single-HIL):
1834
+ * 1. needs_confirmation — extract → dimension editor → confirm →
1835
+ * generate-footprint from the human-approved values.
1836
+ * 2. direct generation — extract returns a standard footprint immediately
1837
+ * (fast path) → preview + download.
1838
+ */
1839
+ function FootprintGenPage({ ports, t, reopen = null }) {
1840
+ const auth = useAuthGate(ports);
1841
+ const runner = useJobRunner(ports);
1842
+ const [config, setConfig] = useState(null);
1843
+ const [imageDataUrl, setImageDataUrl] = useState(null);
1844
+ const [file, setFile] = useState(null);
1845
+ const [hint, setHint] = useState("");
1846
+ const runnerRef = useRef(runner);
1847
+ runnerRef.current = runner;
1848
+ const portsRef = useRef(ports);
1849
+ portsRef.current = ports;
1850
+ useEffect(() => {
1851
+ if (!reopen) return;
1852
+ const { entry } = reopen;
1853
+ runnerRef.current.loadHistory(entry);
1854
+ if (entry.input?.imageId) portsRef.current.inputImage(entry.input.imageId).then((dataUrl) => setImageDataUrl(dataUrl)).catch(() => {});
1855
+ if (entry.input?.packageType) setHint(entry.input.packageType);
1856
+ }, [reopen]);
1857
+ useEffect(() => {
1858
+ ports.config().then(setConfig).catch(() => {});
1859
+ }, [ports]);
1860
+ const maxBytes = config?.limits.imageBytes ?? 4194304;
1861
+ const authed = auth.phase === "authenticated";
1862
+ const busy = runner.phase === "running";
1863
+ const extract = useCallback(() => {
1864
+ if (!imageDataUrl) return;
1865
+ runner.run({
1866
+ kind: "extract-footprint",
1867
+ input: {
1868
+ imageDataUrl,
1869
+ ...hint.trim() ? { packageType: hint.trim() } : {}
1870
+ }
1871
+ });
1872
+ }, [
1873
+ imageDataUrl,
1874
+ hint,
1875
+ runner
1876
+ ]);
1877
+ const confirmDimensions = useCallback((values, edited) => {
1878
+ runner.run({
1879
+ kind: "generate-footprint",
1880
+ input: {
1881
+ imageDataUrl: imageDataUrl ?? void 0,
1882
+ packageType: (runner.pkgType ?? hint.trim()) || void 0,
1883
+ fileName: runner.fileName ?? void 0,
1884
+ dimensions: values,
1885
+ edited
1886
+ }
1887
+ });
1888
+ }, [
1889
+ runner,
1890
+ hint,
1891
+ imageDataUrl
1892
+ ]);
1893
+ const cancelConfirm = useCallback(() => {
1894
+ runner.clear();
1895
+ }, [runner]);
1896
+ const resultKey = useMemo(() => `${runner.jobId ?? "none"}:${runner.phase}`, [runner.jobId, runner.phase]);
1897
+ return /* @__PURE__ */ jsxs("div", {
1898
+ className: "cga-app",
1899
+ children: [/* @__PURE__ */ jsx("div", {
1900
+ className: "cga-panel",
1901
+ children: /* @__PURE__ */ jsxs("div", {
1902
+ className: "cga-panel__body",
1903
+ children: [
1904
+ auth.phase === "unknown" ? /* @__PURE__ */ jsxs("div", {
1905
+ className: "cga-progress",
1906
+ children: [/* @__PURE__ */ jsx("span", { className: "cga-spinner" }), t("app.loading")]
1907
+ }) : null,
1908
+ auth.phase === "unauthenticated" ? /* @__PURE__ */ jsxs("div", {
1909
+ className: "cga-auth",
1910
+ children: [/* @__PURE__ */ jsx("span", { children: t("auth.loginRequired") }), /* @__PURE__ */ jsx("button", {
1911
+ type: "button",
1912
+ className: "cga-btn",
1913
+ onClick: auth.login,
1914
+ children: t("auth.login")
1915
+ })]
1916
+ }) : null,
1917
+ runner.phase !== "needs_confirmation" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1918
+ /* @__PURE__ */ jsx(UploadInput, {
1919
+ maxBytes,
1920
+ t,
1921
+ disabled: !authed || busy,
1922
+ imageDataUrl,
1923
+ file,
1924
+ onFile: (f, url) => {
1925
+ setFile(f);
1926
+ setImageDataUrl(url);
1927
+ }
1928
+ }),
1929
+ /* @__PURE__ */ jsxs("div", {
1930
+ className: "cga-field",
1931
+ children: [/* @__PURE__ */ jsx("label", {
1932
+ className: "cga-field__label",
1933
+ children: t("footprint.hintPlaceholder")
1934
+ }), /* @__PURE__ */ jsx("input", {
1935
+ className: "cga-field__input",
1936
+ type: "text",
1937
+ value: hint,
1938
+ disabled: !authed || busy,
1939
+ onChange: (ev) => setHint(ev.target.value)
1940
+ })]
1941
+ }),
1942
+ /* @__PURE__ */ jsxs("div", {
1943
+ className: "cga-actions",
1944
+ children: [/* @__PURE__ */ jsx("button", {
1945
+ type: "button",
1946
+ className: "cga-btn cga-btn--primary",
1947
+ disabled: !authed || !imageDataUrl || busy,
1948
+ onClick: extract,
1949
+ children: busy ? t("footprint.extractProgress") : t("footprint.extract")
1950
+ }), busy ? /* @__PURE__ */ jsx("button", {
1951
+ type: "button",
1952
+ className: "cga-btn",
1953
+ onClick: () => void runner.cancel(),
1954
+ children: t("editor.cancelLabel")
1955
+ }) : null]
1956
+ })
1957
+ ] }) : null,
1958
+ runner.phase === "running" && runner.jobId ? /* @__PURE__ */ jsxs("div", {
1959
+ className: "cga-progress",
1960
+ children: [/* @__PURE__ */ jsx("span", { className: "cga-spinner" }), runner.progress || t("footprint.extractProgress")]
1961
+ }) : null,
1962
+ runner.phase === "needs_confirmation" && runner.dimensions ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1963
+ className: "cga-banner cga-banner--info",
1964
+ children: t("footprint.needsConfirmation")
1965
+ }), /* @__PURE__ */ jsx(GeometryEditor, {
1966
+ dimensions: runner.dimensions,
1967
+ pkgType: runner.pkgType,
1968
+ fileName: runner.fileName,
1969
+ disabled: false,
1970
+ t,
1971
+ onConfirm: confirmDimensions,
1972
+ onCancel: cancelConfirm
1973
+ })] }) : null,
1974
+ runner.phase === "failed" ? /* @__PURE__ */ jsxs("div", {
1975
+ className: "cga-banner cga-banner--error",
1976
+ children: [
1977
+ t("footprint.failed"),
1978
+ ": ",
1979
+ runner.error,
1980
+ runner.result?.status === "needs_auth" ? ` (${t("auth.loginRequired")})` : ""
1981
+ ]
1982
+ }) : null
1983
+ ]
1984
+ })
1985
+ }), runner.phase === "completed" ? /* @__PURE__ */ jsx(ResultStage, {
1986
+ ports,
1987
+ kind: "footprint",
1988
+ result: runner.result,
1989
+ t,
1990
+ srcKey: resultKey
1991
+ }) : null]
1992
+ });
1993
+ }
1994
+ //#endregion
1995
+ //#region src/components/HistoryPanel.tsx
1996
+ /**
1997
+ * `@huaqiu/component-gen-app` — history panel.
1998
+ *
1999
+ * Reads `ports.history()` with cursor pagination; renders entries with
2000
+ * reopen / download / delete. Stays DSH-agnostic — actions resolve through
2001
+ * the ports, and artifact text comes from `ports.artifactContent`.
2002
+ */
2003
+ const PAGE = 12;
2004
+ function HistoryPanel({ ports, t, activeKind = null, onReopen }) {
2005
+ const [entries, setEntries] = useState([]);
2006
+ const [cursor, setCursor] = useState(null);
2007
+ const [loading, setLoading] = useState(false);
2008
+ const [done, setDone] = useState(false);
2009
+ const [busy, setBusy] = useState(null);
2010
+ const load = useCallback(async (nextCursor) => {
2011
+ setLoading(true);
2012
+ try {
2013
+ const page = await ports.history({
2014
+ limit: PAGE,
2015
+ cursor: nextCursor
2016
+ });
2017
+ if (!nextCursor) setEntries(page.entries);
2018
+ else setEntries((prev) => [...prev, ...page.entries]);
2019
+ setCursor(page.nextCursor ?? null);
2020
+ if (!page.nextCursor) setDone(true);
2021
+ } catch (e) {
2022
+ console.warn("[hq-component-gen] history load failed", e);
2023
+ setDone(true);
2024
+ } finally {
2025
+ setLoading(false);
2026
+ }
2027
+ }, [ports]);
2028
+ useEffect(() => {
2029
+ load(null);
2030
+ }, [load]);
2031
+ const doDelete = useCallback(async (entry) => {
2032
+ setBusy(entry.id);
2033
+ try {
2034
+ await ports.deleteHistory(entry.id);
2035
+ setEntries((prev) => prev.filter((e) => e.id !== entry.id));
2036
+ } finally {
2037
+ setBusy(null);
2038
+ }
2039
+ }, [ports]);
2040
+ const doDownload = useCallback(async (entry) => {
2041
+ if (!entry.result?.artifactId) return;
2042
+ setBusy(entry.id);
2043
+ try {
2044
+ const content = await ports.artifactContent(entry.result.artifactId);
2045
+ triggerDownload(entry.result.filename || `${entry.result.artifactId}.${entry.kind === "symbol" ? "kicad_sym" : "kicad_mod"}`, content);
2046
+ } catch (e) {
2047
+ console.warn("[hq-component-gen] history download failed", e);
2048
+ } finally {
2049
+ setBusy(null);
2050
+ }
2051
+ }, [ports]);
2052
+ const visible = activeKind ? entries.filter((e) => e.kind === activeKind) : entries;
2053
+ return /* @__PURE__ */ jsxs("div", {
2054
+ className: "cga-history",
2055
+ children: [
2056
+ visible.length === 0 && !loading ? /* @__PURE__ */ jsx("div", {
2057
+ className: "cga-upload__text",
2058
+ children: t("history.empty")
2059
+ }) : null,
2060
+ visible.map((entry) => /* @__PURE__ */ jsxs("div", {
2061
+ className: "cga-history__item",
2062
+ children: [/* @__PURE__ */ jsxs("div", {
2063
+ className: "cga-history__meta",
2064
+ children: [/* @__PURE__ */ jsxs("span", {
2065
+ className: "cga-history__title",
2066
+ children: [
2067
+ entry.kind === "symbol" ? t("history.symbol") : t("history.footprint"),
2068
+ " — ",
2069
+ entry.result?.filename ?? entry.id.slice(0, 8)
2070
+ ]
2071
+ }), /* @__PURE__ */ jsxs("span", {
2072
+ className: "cga-history__sub",
2073
+ children: [
2074
+ new Date(entry.createdAt).toLocaleString(),
2075
+ " · ",
2076
+ t(`history.${entry.status}`)
2077
+ ]
2078
+ })]
2079
+ }), /* @__PURE__ */ jsxs("div", {
2080
+ style: {
2081
+ display: "flex",
2082
+ gap: 4
2083
+ },
2084
+ children: [entry.status === "generated" && entry.result?.artifactId ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2085
+ type: "button",
2086
+ className: "cga-history__act",
2087
+ disabled: busy === entry.id,
2088
+ onClick: () => onReopen(entry),
2089
+ children: t("history.reopen")
2090
+ }), /* @__PURE__ */ jsx("button", {
2091
+ type: "button",
2092
+ className: "cga-history__act",
2093
+ disabled: busy === entry.id,
2094
+ onClick: () => void doDownload(entry),
2095
+ children: t("history.download")
2096
+ })] }) : null, /* @__PURE__ */ jsx("button", {
2097
+ type: "button",
2098
+ className: "cga-history__act",
2099
+ disabled: busy === entry.id,
2100
+ onClick: () => void doDelete(entry),
2101
+ children: t("history.delete")
2102
+ })]
2103
+ })]
2104
+ }, entry.id)),
2105
+ !done && visible.length > 0 ? /* @__PURE__ */ jsx("button", {
2106
+ type: "button",
2107
+ className: "cga-btn",
2108
+ disabled: loading,
2109
+ onClick: () => void load(cursor),
2110
+ children: loading ? t("app.loading") : t("history.loadMore")
2111
+ }) : null,
2112
+ done && visible.length > 0 ? /* @__PURE__ */ jsx("div", {
2113
+ className: "cga-upload__text",
2114
+ children: t("history.noMore")
2115
+ }) : null
2116
+ ]
2117
+ });
2118
+ }
2119
+ //#endregion
2120
+ //#region src/App.tsx
2121
+ /**
2122
+ * `@huaqiu/component-gen-app` — the app shell.
2123
+ *
2124
+ * Renders one of the two generation pages (the host opens the page directly).
2125
+ * History lives in a modal dialog opened from the header's "View history"
2126
+ * action (mirroring dsh's settings-dialog header action beside the close
2127
+ * button); reopening an entry closes the dialog and loads the artifact back
2128
+ * into the page. Completely DSH-agnostic — everything the app needs comes
2129
+ * through `ComponentGenPorts`.
2130
+ */
2131
+ /** Modal history dialog — chrome follows dsh's Modal primitive. */
2132
+ function HistoryDialog({ ports, page, t, onReopen, onClose }) {
2133
+ return /* @__PURE__ */ jsxs("div", {
2134
+ className: "cga-history-dialog",
2135
+ role: "presentation",
2136
+ children: [/* @__PURE__ */ jsx("div", {
2137
+ className: "cga-history-dialog__mask",
2138
+ "aria-hidden": "true",
2139
+ onClick: onClose
2140
+ }), /* @__PURE__ */ jsxs("div", {
2141
+ className: "cga-history-dialog__panel",
2142
+ role: "dialog",
2143
+ "aria-modal": "true",
2144
+ "aria-label": t("history.title"),
2145
+ children: [/* @__PURE__ */ jsxs("div", {
2146
+ className: "cga-history-dialog__head",
2147
+ children: [/* @__PURE__ */ jsx("span", {
2148
+ className: "cga-history-dialog__title",
2149
+ children: t("history.title")
2150
+ }), /* @__PURE__ */ jsx("button", {
2151
+ type: "button",
2152
+ className: "cga-app__head-close",
2153
+ "aria-label": t("app.close"),
2154
+ onClick: onClose,
2155
+ children: /* @__PURE__ */ jsx("svg", {
2156
+ width: "16",
2157
+ height: "16",
2158
+ viewBox: "0 0 16 16",
2159
+ fill: "none",
2160
+ stroke: "currentColor",
2161
+ strokeWidth: "1.5",
2162
+ strokeLinecap: "round",
2163
+ "aria-hidden": "true",
2164
+ children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
2165
+ })
2166
+ })]
2167
+ }), /* @__PURE__ */ jsx("div", {
2168
+ className: "cga-history-dialog__body",
2169
+ children: /* @__PURE__ */ jsx(HistoryPanel, {
2170
+ ports,
2171
+ t,
2172
+ activeKind: page,
2173
+ onReopen
2174
+ })
2175
+ })]
2176
+ })]
2177
+ });
2178
+ }
2179
+ function ComponentGenApp(props) {
2180
+ const { ports, page, lang, onClose } = props;
2181
+ const t = useMemo(() => translateFor(lang), [lang]);
2182
+ const [historyOpen, setHistoryOpen] = useState(false);
2183
+ const [reopenReq, setReopenReq] = useState(null);
2184
+ const pageReopen = reopenReq && reopenReq.entry.kind === page ? reopenReq : null;
2185
+ const openHistory = (entry) => {
2186
+ setHistoryOpen(false);
2187
+ setReopenReq((prev) => ({
2188
+ n: (prev?.n ?? 0) + 1,
2189
+ entry
2190
+ }));
2191
+ };
2192
+ return /* @__PURE__ */ jsxs("div", {
2193
+ className: "cga-app",
2194
+ children: [
2195
+ /* @__PURE__ */ jsxs("div", {
2196
+ className: "cga-app__head",
2197
+ children: [/* @__PURE__ */ jsx("span", {
2198
+ className: "cga-app__head-title",
2199
+ children: page === "symbol" ? t("app.symbolTitle") : t("app.footprintTitle")
2200
+ }), /* @__PURE__ */ jsxs("div", {
2201
+ className: "cga-app__head-actions",
2202
+ children: [/* @__PURE__ */ jsx("button", {
2203
+ type: "button",
2204
+ className: "cga-btn cga-btn--outline",
2205
+ onClick: () => setHistoryOpen(true),
2206
+ children: t("history.view")
2207
+ }), onClose ? /* @__PURE__ */ jsx("button", {
2208
+ type: "button",
2209
+ className: "cga-app__head-close",
2210
+ "aria-label": t("app.close"),
2211
+ onClick: onClose,
2212
+ children: /* @__PURE__ */ jsx("svg", {
2213
+ width: "16",
2214
+ height: "16",
2215
+ viewBox: "0 0 16 16",
2216
+ fill: "none",
2217
+ stroke: "currentColor",
2218
+ strokeWidth: "1.5",
2219
+ strokeLinecap: "round",
2220
+ "aria-hidden": "true",
2221
+ children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
2222
+ })
2223
+ }) : null]
2224
+ })]
2225
+ }),
2226
+ page === "symbol" ? /* @__PURE__ */ jsx(SymbolGenPage, {
2227
+ ports,
2228
+ t,
2229
+ reopen: pageReopen
2230
+ }) : /* @__PURE__ */ jsx(FootprintGenPage, {
2231
+ ports,
2232
+ t,
2233
+ reopen: pageReopen
2234
+ }),
2235
+ historyOpen ? /* @__PURE__ */ jsx(HistoryDialog, {
2236
+ ports,
2237
+ page,
2238
+ t,
2239
+ onReopen: openHistory,
2240
+ onClose: () => setHistoryOpen(false)
2241
+ }) : null
2242
+ ]
2243
+ });
2244
+ }
2245
+ //#endregion
2246
+ //#region src/api/component-gen-client.ts
2247
+ const DEFAULT_LIMITS = { imageBytes: 4194304 };
2248
+ async function readJson(res) {
2249
+ if (!res.ok) {
2250
+ let detail = `HTTP ${res.status}`;
2251
+ try {
2252
+ const body = await res.json();
2253
+ if (body?.error) detail = body.error;
2254
+ if (body?.detail) detail = `${detail}: ${String(body.detail)}`;
2255
+ } catch {}
2256
+ throw new Error(detail);
2257
+ }
2258
+ return await res.json();
2259
+ }
2260
+ /** The shared fetch client. */
2261
+ function createHttpPorts(options) {
2262
+ const base = options.base.replace(/\/+$/, "");
2263
+ const doFetch = options.doFetch ?? globalThis.fetch.bind(globalThis);
2264
+ const artifactsBase = (options.artifactsBase ?? defaultArtifactsBase(base)).replace(/\/+$/, "");
2265
+ const url = (p) => `${base}${p}`;
2266
+ const artifactUrl = (p) => `${artifactsBase}${p}`;
2267
+ return {
2268
+ async config() {
2269
+ const cfg = await readJson(await doFetch(url("/config"), { headers: { accept: "application/json" } }));
2270
+ return {
2271
+ hostMode: cfg.hostMode ?? false,
2272
+ capabilities: cfg.capabilities ?? {
2273
+ symbol: true,
2274
+ footprint: true
2275
+ },
2276
+ limits: {
2277
+ ...DEFAULT_LIMITS,
2278
+ ...cfg.limits ?? {}
2279
+ }
2280
+ };
2281
+ },
2282
+ async startJob(req, signal) {
2283
+ const res = await doFetch(url("/jobs"), {
2284
+ method: "POST",
2285
+ headers: { "content-type": "application/json" },
2286
+ body: JSON.stringify(req),
2287
+ signal
2288
+ });
2289
+ if (res.status === 202) {
2290
+ const body = await res.json();
2291
+ return {
2292
+ id: String(body.jobId ?? ""),
2293
+ kind: req.kind,
2294
+ status: "queued",
2295
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2296
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2297
+ };
2298
+ }
2299
+ return readJson(res);
2300
+ },
2301
+ jobEvents(jobId, onEvent) {
2302
+ const controller = new AbortController();
2303
+ const run = async () => {
2304
+ try {
2305
+ const res = await doFetch(url(`/jobs/${encodeURIComponent(jobId)}/events`), {
2306
+ headers: { accept: "text/event-stream" },
2307
+ signal: controller.signal
2308
+ });
2309
+ if (!res.ok || !res.body) {
2310
+ onEvent({
2311
+ type: "failed",
2312
+ error: `events HTTP ${res.status}`,
2313
+ at: (/* @__PURE__ */ new Date()).toISOString()
2314
+ });
2315
+ return;
2316
+ }
2317
+ const reader = res.body.getReader();
2318
+ const decoder = new TextDecoder();
2319
+ let buf = "";
2320
+ for (;;) {
2321
+ const { done, value } = await reader.read();
2322
+ if (done) break;
2323
+ buf += decoder.decode(value, { stream: true });
2324
+ let idx;
2325
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
2326
+ const frame = buf.slice(0, idx);
2327
+ buf = buf.slice(idx + 2);
2328
+ const event = parseEvent(frame);
2329
+ if (event) onEvent(event);
2330
+ }
2331
+ }
2332
+ } catch (err) {
2333
+ if (err?.name === "AbortError") return;
2334
+ onEvent({
2335
+ type: "failed",
2336
+ error: String(err?.message || err),
2337
+ at: (/* @__PURE__ */ new Date()).toISOString()
2338
+ });
2339
+ }
2340
+ };
2341
+ run();
2342
+ return () => controller.abort();
2343
+ },
2344
+ async abortJob(jobId) {
2345
+ await doFetch(url(`/jobs/${encodeURIComponent(jobId)}`), { method: "DELETE" });
2346
+ },
2347
+ async history(query) {
2348
+ const params = new URLSearchParams();
2349
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
2350
+ if (query.cursor) params.set("cursor", query.cursor);
2351
+ const qs = params.toString();
2352
+ return readJson(await doFetch(url(`/history${qs ? `?${qs}` : ""}`), { headers: { accept: "application/json" } }));
2353
+ },
2354
+ async historyEntry(id) {
2355
+ const res = await doFetch(url(`/history/${encodeURIComponent(id)}`), { headers: { accept: "application/json" } });
2356
+ if (res.status === 404) return null;
2357
+ return readJson(res);
2358
+ },
2359
+ async patchHistory(id, patch) {
2360
+ return readJson(await doFetch(url(`/history/${encodeURIComponent(id)}`), {
2361
+ method: "PATCH",
2362
+ headers: { "content-type": "application/json" },
2363
+ body: JSON.stringify(patch)
2364
+ }));
2365
+ },
2366
+ async deleteHistory(id) {
2367
+ await doFetch(url(`/history/${encodeURIComponent(id)}`), { method: "DELETE" });
2368
+ },
2369
+ async artifactContent(artifactId) {
2370
+ const res = await doFetch(artifactUrl(`/${encodeURIComponent(artifactId)}/content`), { headers: { accept: "text/plain" } });
2371
+ if (!res.ok) throw new Error(`artifact content HTTP ${res.status}`);
2372
+ return res.text();
2373
+ },
2374
+ async inputImage(imageId) {
2375
+ const res = await doFetch(url(`/history/${encodeURIComponent(imageId)}/image`), { headers: { accept: "image/*" } });
2376
+ if (!res.ok) throw new Error(`input image HTTP ${res.status}`);
2377
+ const blob = await res.blob();
2378
+ return new Promise((resolve, reject) => {
2379
+ const reader = new FileReader();
2380
+ reader.onload = () => resolve(String(reader.result));
2381
+ reader.onerror = () => reject(/* @__PURE__ */ new Error("failed to read input image"));
2382
+ reader.readAsDataURL(blob);
2383
+ });
2384
+ },
2385
+ auth: options.auth ?? createPassthroughAuth()
2386
+ };
2387
+ }
2388
+ /** Derive the artifacts base from the component-gen base path. */
2389
+ function defaultArtifactsBase(componentGenBase) {
2390
+ return componentGenBase.replace(/\/component-gen\/?$/, "/artifacts");
2391
+ }
2392
+ /** Parse one SSE frame (event: / data: lines) into a JobEvent. */
2393
+ function parseEvent(frame) {
2394
+ let eventName = "message";
2395
+ const dataLines = [];
2396
+ for (const line of frame.split("\n")) if (line.startsWith("event:")) eventName = line.slice(6).trim();
2397
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
2398
+ const data = dataLines.join("\n");
2399
+ if (!data) return null;
2400
+ try {
2401
+ const raw = JSON.parse(data);
2402
+ if (eventName === "needs_confirmation" && "dimensions" in raw) return raw;
2403
+ if (eventName === "completed" && "job" in raw) return raw;
2404
+ if (eventName === "failed") return raw;
2405
+ if (eventName === "cancelled") return raw;
2406
+ return raw;
2407
+ } catch {
2408
+ return null;
2409
+ }
2410
+ }
2411
+ /** Default auth port: optimistic (used when the host supplies no auth). */
2412
+ function createPassthroughAuth() {
2413
+ return {
2414
+ async isAuthenticated() {
2415
+ return true;
2416
+ },
2417
+ async getUserInfo() {
2418
+ return null;
2419
+ },
2420
+ async login() {},
2421
+ onAuthStateChanged() {
2422
+ return () => {};
2423
+ }
2424
+ };
2425
+ }
2426
+ //#endregion
2427
+ //#region src/styles/inject.ts
2428
+ /**
2429
+ * `@huaqiu/component-gen-app` — scoped stylesheet.
2430
+ *
2431
+ * Injected as a `<style>` tag (never a CSS import) so both build surfaces work:
2432
+ * the standalone vite bundle AND the DSH client-module bundle (tsdown has no
2433
+ * CSS pipeline). Reuses the `hq-genhit__*` class vocabulary (shared with
2434
+ * `dsh-tool-symbol-footprint`) plus an `cga-*` app-shell layer. Uses the same
2435
+ * DSW design tokens so the app follows the host palette in DSH.
2436
+ */
2437
+ const APP_STYLE_ID = "hq-cga-styles";
2438
+ const APP_PLUGIN_ID = "@huaqiu/component-gen-app";
2439
+ const CSS = `
2440
+ /* ── shared with dsh-tool-symbol-footprint (editor + preview) ─────────────── */
2441
+ .hq-genhit__stage { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; box-sizing: border-box; background: var(--dsw-alias-markdown-code-block, #0a1929); overflow: hidden; }
2442
+ .hq-genhit__stage--symbol, .hq-genhit__stage--footprint { height: 300px; }
2443
+ .hq-genhit__canvas { display: block; width: 100%; height: 100%; background: var(--dsw-alias-markdown-code-block, #0a1929); }
2444
+ .hq-genhit__stage-msg { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 10px 14px; box-sizing: border-box; font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-secondary, currentColor); text-align: center; }
2445
+ .hq-genhit__editor { padding: 0 12px 4px; }
2446
+ .hq-genhit__geom { display: block; width: 100%; height: auto; touch-action: none; }
2447
+ .hq-genhit__body { fill: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.08)); stroke: var(--dsw-alias-label-primary, currentColor); stroke-width: 1.5; }
2448
+ .hq-genhit__pad { fill: var(--dsw-alias-accent-primary, currentColor); opacity: 0.85; }
2449
+ .hq-genhit__ball { fill: var(--dsw-alias-accent-primary, currentColor); opacity: 0.85; }
2450
+ .hq-genhit__epad { fill: var(--dsw-alias-accent-primary, currentColor); opacity: 0.25; stroke: var(--dsw-alias-accent-primary, currentColor); stroke-width: 1; stroke-dasharray: 2 2; }
2451
+ .hq-genhit__pkg { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; padding: 0 0 8px; }
2452
+ .hq-genhit__badge--pkg { border-color: var(--dsw-alias-accent-primary, currentColor); color: var(--dsw-alias-accent-primary, currentColor); }
2453
+ .hq-genhit__pkg-meta { font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-secondary, currentColor); }
2454
+ .hq-genhit__pkg-meta--sep { opacity: 0.85; }
2455
+ .hq-genhit__dimline { stroke: var(--dsw-alias-label-secondary, currentColor); stroke-width: 1; }
2456
+ .hq-genhit__dimlabel { fill: var(--dsw-alias-label-secondary, currentColor); font: 11px/1 system-ui, sans-serif; }
2457
+ .hq-genhit__dimlabel--clickable { cursor: pointer; }
2458
+ .hq-genhit__dimlabel--clickable:hover { fill: var(--dsw-alias-accent-primary, currentColor); text-decoration: underline; }
2459
+ .hq-genhit__tol { fill: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary, currentColor)); font: 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; }
2460
+ .hq-genhit__arrow { fill: var(--dsw-alias-label-secondary, currentColor); }
2461
+ .hq-genhit__handle { fill: var(--dsw-alias-accent-primary, currentColor); stroke: var(--dsw-alias-bg-layer-1, #fff); stroke-width: 1.5; cursor: ew-resize; }
2462
+ .hq-genhit__handle--h { cursor: ns-resize; }
2463
+ .hq-genhit__handle--wh { cursor: nwse-resize; }
2464
+ .hq-genhit__drag-hint { padding: 0 2px 6px; font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary, currentColor)); }
2465
+ .hq-genhit__fields { display: flex; flex-wrap: wrap; gap: 8px 12px; padding: 0 0 10px; }
2466
+ .hq-genhit__field { display: inline-flex; align-items: center; gap: 6px; }
2467
+ .hq-genhit__field-label { font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-secondary, currentColor); }
2468
+ .hq-genhit__field-label--edited { color: var(--dsw-alias-accent-primary, currentColor); }
2469
+ .hq-genhit__field-tag { font: 10px/1.2 system-ui, sans-serif; padding: 1px 4px; border-radius: 4px; }
2470
+ .hq-genhit__field-tag--ai { color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary, currentColor)); border: 1px solid var(--dsw-alias-border-l1, currentColor); }
2471
+ .hq-genhit__field-tag--edited { color: var(--dsw-alias-accent-primary, currentColor); border: 1px solid var(--dsw-alias-accent-primary, currentColor); }
2472
+ .hq-genhit__field-input { width: 72px; padding: 3px 6px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1, currentColor); background: var(--dsw-alias-bg-layer-1, transparent); color: var(--dsw-alias-label-primary, currentColor); font: var(--dsw-font-xxs-12, 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace); }
2473
+ .hq-genhit__field--invalid .hq-genhit__field-input { border-color: var(--dsw-alias-state-error-primary, currentColor); }
2474
+ .hq-genhit__field-unit { font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-secondary, currentColor); }
2475
+ .hq-genhit__adv { padding: 0 0 8px; }
2476
+ .hq-genhit__adv-toggle { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--dsw-alias-border-l1, currentColor); border-radius: 6px; padding: 3px 8px; background: var(--dsw-alias-bg-layer-1, transparent); color: var(--dsw-alias-label-secondary, currentColor); font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); cursor: pointer; }
2477
+ .hq-genhit__fields--adv { padding-top: 8px; }
2478
+ .hq-genhit__validation { display: flex; align-items: baseline; gap: 6px; padding: 6px 10px; margin: 0 0 10px; border-radius: 6px; font: var(--dsw-font-xxs-12, 12px/1.4 system-ui, sans-serif); color: var(--dsw-alias-label-secondary, currentColor); background: var(--dsw-alias-interactive-bg-hover, transparent); }
2479
+ .hq-genhit__validation--warn { color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-error-primary, currentColor)); background: var(--dsw-alias-state-warning-bg, transparent); }
2480
+ .hq-genhit__validation-detail { opacity: 0.85; }
2481
+
2482
+ /* ── app shell (cga-*) — tokens/geometry follow dsh's Modal + settings dialog
2483
+ (mask-1/mask-blur, layer-2 + elevation, r24 dialog, 28x28 r8 close, 13px/500
2484
+ labels, r8 inputs on bg-layer-3 with border-l4, capsule sm buttons) ─────── */
2485
+ .cga-app { display: flex; flex-direction: column; gap: 12px; width: 100%; box-sizing: border-box; font: var(--dsw-font-s-14, 14px/1.5 system-ui, sans-serif); color: var(--dsw-alias-label-primary, currentColor); }
2486
+ .cga-app__head { display: flex; align-items: center; gap: 8px; }
2487
+ .cga-app__head-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary, currentColor); }
2488
+ .cga-app__head-close { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin-left: auto; padding: 0; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, currentColor); cursor: pointer; }
2489
+ .cga-app__head-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.08)); }
2490
+ .cga-panel { border: 0.5px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2)); border-radius: 8px; background: var(--dsw-alias-bg-layer-1, transparent); overflow: hidden; }
2491
+ .cga-panel__body { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
2492
+ .cga-upload { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 18px 12px; border: 1px dashed var(--dsw-alias-border-l3, currentColor); border-radius: 8px; cursor: pointer; }
2493
+ .cga-upload--dragging { border-color: var(--dsw-alias-brand-primary, var(--dsw-alias-accent-primary, currentColor)); background: var(--dsw-alias-interactive-bg-hover, transparent); }
2494
+ .cga-upload__thumb { max-width: 160px; max-height: 120px; border-radius: 6px; object-fit: contain; }
2495
+ .cga-upload__text { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary, currentColor)); text-align: center; }
2496
+ .cga-upload__browse { margin-top: 2px; height: 28px; padding: 0 10px; border: 0.5px solid var(--dsw-alias-border-l3, currentColor); border-radius: 14px; background: transparent; color: var(--dsw-alias-label-primary, currentColor); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }
2497
+ .cga-upload__browse:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.08)); }
2498
+ .cga-field { display: flex; flex-direction: column; gap: 6px; }
2499
+ .cga-field__label { font-size: 13px; font-weight: 500; line-height: 1.5; color: var(--dsw-alias-label-primary, currentColor); }
2500
+ .cga-field__input, .cga-field__select { height: 34px; padding: 0 12px; border: 0.5px solid var(--dsw-alias-border-l4, currentColor); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, transparent); color: var(--dsw-alias-label-primary, currentColor); font: inherit; font-size: 13px; line-height: 1.5; }
2501
+ .cga-field__input:focus-visible, .cga-field__select:focus-visible { outline: none; border-color: var(--dsw-alias-brand-primary, var(--dsw-alias-accent-primary, currentColor)); }
2502
+ .cga-field__input::placeholder { color: var(--dsw-alias-label-dimmed, var(--dsw-alias-label-tertiary, currentColor)); }
2503
+ .cga-actions { display: flex; flex-wrap: wrap; gap: 8px; }
2504
+ .cga-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 28px; padding: 0 10px; border: none; border-radius: 14px; background: transparent; color: var(--dsw-alias-label-primary, currentColor); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }
2505
+ .cga-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.08)); }
2506
+ .cga-btn:disabled { opacity: 0.4; cursor: default; }
2507
+ .cga-btn--primary { background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-accent-primary, currentColor)); border-color: transparent; color: var(--dsw-alias-label-primary-foreground, #fff); }
2508
+ .cga-btn--primary:hover { background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-accent-primary, currentColor)); }
2509
+ .cga-btn--primary:disabled { opacity: 0.6; }
2510
+ .cga-btn--outline { border: 0.5px solid var(--dsw-alias-border-l3, currentColor); }
2511
+ .cga-app__head-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
2512
+ .cga-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; font-size: 12px; line-height: 1.5; }
2513
+ .cga-banner--info { color: var(--dsw-alias-label-secondary, currentColor); background: var(--dsw-alias-interactive-bg-hover, transparent); }
2514
+ .cga-banner--warn { color: var(--dsw-alias-state-warning-primary, currentColor); background: var(--dsw-alias-state-warning-bg, transparent); }
2515
+ .cga-banner--error { color: var(--dsw-alias-state-error-primary, currentColor); background: var(--dsw-alias-state-error-bg, transparent); }
2516
+ .cga-progress { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary, currentColor); }
2517
+ .cga-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--dsw-alias-border-l3, currentColor); border-top-color: var(--dsw-alias-brand-primary, var(--dsw-alias-accent-primary, currentColor)); animation: cga-spin 0.8s linear infinite; flex: none; }
2518
+ @keyframes cga-spin { to { transform: rotate(360deg); } }
2519
+ .cga-history { display: flex; flex-direction: column; gap: 6px; }
2520
+ .cga-history__item { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border: 0.5px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2)); border-radius: 8px; }
2521
+ .cga-history__meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
2522
+ .cga-history__title { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, currentColor); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2523
+ .cga-history__sub { font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary, currentColor)); }
2524
+ .cga-history__act { border: 0; background: none; color: var(--dsw-alias-brand-primary, var(--dsw-alias-accent-primary, currentColor)); font: inherit; font-size: 12px; line-height: 1.5; cursor: pointer; padding: 2px 4px; }
2525
+ .cga-history__act:hover { color: var(--dsw-alias-label-primary, currentColor); }
2526
+ .cga-auth { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 10px; border-radius: 8px; background: var(--dsw-alias-interactive-bg-hover, transparent); font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-secondary, currentColor); }
2527
+
2528
+ /* History modal — same chrome as dsh's Modal/settings dialog. */
2529
+ .cga-history-dialog { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 24px; box-sizing: border-box; }
2530
+ .cga-history-dialog__mask { position: absolute; inset: 0; background: var(--dsw-alias-bg-mask-1); backdrop-filter: var(--dsw-mask-blur); }
2531
+ .cga-history-dialog__panel { position: relative; z-index: 1; display: flex; flex-direction: column; width: min(480px, 100%); max-height: min(560px, calc(100vh - 48px)); overflow: hidden; border-radius: 24px; background: var(--dsw-alias-bg-layer-2); box-shadow: var(--dsw-elevation-prominent); }
2532
+ .cga-history-dialog__head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 20px 14px 8px 24px; }
2533
+ .cga-history-dialog__title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary, currentColor); }
2534
+ .cga-history-dialog__body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 24px 24px; }
2535
+ `;
2536
+ function injectAppStyles() {
2537
+ if (typeof document === "undefined") return;
2538
+ if (document.getElementById("hq-cga-styles")) return;
2539
+ const style = document.createElement("style");
2540
+ style.id = APP_STYLE_ID;
2541
+ style.setAttribute("data-plugin", APP_PLUGIN_ID);
2542
+ style.textContent = CSS;
2543
+ document.head.appendChild(style);
2544
+ }
2545
+ function removeAppStyles() {
2546
+ if (typeof document === "undefined") return;
2547
+ document.getElementById(APP_STYLE_ID)?.remove();
2548
+ }
2549
+ //#endregion
2550
+ export { APP_STYLE_ID, ComponentGenApp, EN, FootprintGenPage, GeometryEditor, HistoryPanel, PreviewStage, ResultStage, SymbolGenPage, UploadInput, ZH, createHttpPorts, defaultArtifactsBase, defaultT, fileToDataUrl, injectAppStyles, removeAppStyles, translate, translateFor };