@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/LICENSE +21 -0
- package/dist/assets/index-DTShI_jq.js +553 -0
- package/dist/index.html +12 -0
- package/lib/index.d.ts +427 -0
- package/lib/index.js +2550 -0
- package/package.json +48 -0
- package/src/App.tsx +101 -0
- package/src/api/component-gen-client.ts +225 -0
- package/src/components/GeometryEditor.tsx +418 -0
- package/src/components/HistoryPanel.tsx +119 -0
- package/src/components/PreviewStage.tsx +67 -0
- package/src/components/ResultStage.tsx +92 -0
- package/src/components/UploadInput.tsx +121 -0
- package/src/copy/en.ts +150 -0
- package/src/copy/index.ts +50 -0
- package/src/copy/zh.ts +154 -0
- package/src/hooks/useAuthGate.ts +51 -0
- package/src/hooks/useJobRunner.ts +127 -0
- package/src/index.ts +37 -0
- package/src/main.tsx +85 -0
- package/src/pages/FootprintGenPage.tsx +185 -0
- package/src/pages/SymbolGenPage.tsx +136 -0
- package/src/ports.ts +149 -0
- package/src/styles/inject.ts +124 -0
- package/src/utils/dims.ts +266 -0
- package/src/utils/ecad.ts +91 -0
- package/src/utils/labels.ts +76 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — completed-generation result stage.
|
|
3
|
+
*
|
|
4
|
+
* Renders the preview (from `ports.artifactContent`), a download action and a
|
|
5
|
+
* note when the generation was auto-generated or degraded. DSH-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useState, type ReactElement } from 'react'
|
|
8
|
+
import type { ComponentGenPorts } from '../ports.js'
|
|
9
|
+
import type { Translate } from '../copy/index.js'
|
|
10
|
+
import { triggerDownload } from '../utils/ecad.js'
|
|
11
|
+
import { PreviewStage } from './PreviewStage.js'
|
|
12
|
+
|
|
13
|
+
export interface ResultStageProps {
|
|
14
|
+
ports: ComponentGenPorts
|
|
15
|
+
kind: 'symbol' | 'footprint'
|
|
16
|
+
result: Record<string, unknown>
|
|
17
|
+
t: Translate
|
|
18
|
+
/** bump to force a re-render of the preview for a new result. */
|
|
19
|
+
srcKey: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function artifactIdOf(result: Record<string, unknown>): string | null {
|
|
23
|
+
const art = result.artifact
|
|
24
|
+
if (art && typeof art === 'object') {
|
|
25
|
+
const id = (art as { id?: unknown }).id
|
|
26
|
+
if (typeof id === 'string' && id) return id
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function filenameOf(result: Record<string, unknown>, kind: string): string {
|
|
32
|
+
const f = result.filename
|
|
33
|
+
if (typeof f === 'string' && f) return f
|
|
34
|
+
return `generated.${kind === 'symbol' ? 'kicad_sym' : 'kicad_mod'}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ResultStage({ ports, kind, result, t, srcKey }: ResultStageProps): ReactElement {
|
|
38
|
+
const artifactId = artifactIdOf(result)
|
|
39
|
+
const [content, setContent] = useState<string | null>(null)
|
|
40
|
+
const [previewErr, setPreviewErr] = useState<string | null>(null)
|
|
41
|
+
const [downloading, setDownloading] = useState(false)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!artifactId) return
|
|
45
|
+
let cancelled = false
|
|
46
|
+
setContent(null)
|
|
47
|
+
setPreviewErr(null)
|
|
48
|
+
ports.artifactContent(artifactId)
|
|
49
|
+
.then((text) => { if (!cancelled) setContent(text) })
|
|
50
|
+
.catch((e) => { if (!cancelled) setPreviewErr(String((e as Error)?.message || e)) })
|
|
51
|
+
return () => { cancelled = true }
|
|
52
|
+
}, [artifactId, ports, srcKey])
|
|
53
|
+
|
|
54
|
+
const download = useCallback(async (): Promise<void> => {
|
|
55
|
+
setDownloading(true)
|
|
56
|
+
try {
|
|
57
|
+
if (artifactId) {
|
|
58
|
+
const text = content ?? await ports.artifactContent(artifactId)
|
|
59
|
+
triggerDownload(filenameOf(result, kind), text)
|
|
60
|
+
} else {
|
|
61
|
+
const fileUrl = result.fileUrl
|
|
62
|
+
if (typeof fileUrl === 'string' && fileUrl) {
|
|
63
|
+
window.open(fileUrl, '_blank', 'noopener')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
setDownloading(false)
|
|
68
|
+
}
|
|
69
|
+
}, [artifactId, content, ports, result, kind])
|
|
70
|
+
|
|
71
|
+
const autoGenerated = result.autoGenerated === true
|
|
72
|
+
const note = typeof result.note === 'string' ? result.note : null
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="cga-panel">
|
|
76
|
+
<div className="cga-panel__body">
|
|
77
|
+
{autoGenerated ? <div className="cga-banner cga-banner--info">{t('footprint.directGenerated')}</div> : null}
|
|
78
|
+
{content != null
|
|
79
|
+
? <PreviewStage kind={kind} content={content} srcKey={srcKey} t={t} />
|
|
80
|
+
: previewErr
|
|
81
|
+
? <div className="cga-banner cga-banner--error">{t('app.error')}{previewErr}</div>
|
|
82
|
+
: null}
|
|
83
|
+
{note ? <div className="cga-banner cga-banner--info">{note}</div> : null}
|
|
84
|
+
<div className="cga-actions">
|
|
85
|
+
<button type="button" className="cga-btn cga-btn--primary" onClick={() => void download()} disabled={downloading}>
|
|
86
|
+
{kind === 'symbol' ? t('symbol.download') : t('footprint.download')}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — image upload control (click + paste + drag).
|
|
3
|
+
*
|
|
4
|
+
* Reads a file, downscales it to a thumbnail data URL if needed (bounded by
|
|
5
|
+
* `maxBytes`), and reports both the original file and the data URL to the
|
|
6
|
+
* parent. The parent owns sending it to the server.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useRef, useState, type ReactElement } from 'react'
|
|
9
|
+
import type { Translate } from '../copy/index.js'
|
|
10
|
+
|
|
11
|
+
export interface UploadInputProps {
|
|
12
|
+
maxBytes: number
|
|
13
|
+
t: Translate
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
imageDataUrl?: string | null
|
|
16
|
+
/** original file, for the server to store into history. */
|
|
17
|
+
file?: File | null
|
|
18
|
+
onFile: (file: File | null, dataUrl: string | null) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MAX_EDGE = 1600
|
|
22
|
+
|
|
23
|
+
/** Downscale an image file to a data URL bounded in edge + bytes. */
|
|
24
|
+
export function fileToDataUrl(file: File, maxBytes: number): Promise<string> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const reader = new FileReader()
|
|
27
|
+
reader.onerror = () => reject(new Error('failed to read file'))
|
|
28
|
+
reader.onload = () => {
|
|
29
|
+
const src = String(reader.result)
|
|
30
|
+
if (src.length <= maxBytes) { resolve(src); return }
|
|
31
|
+
// Too big as-is — downscale via canvas.
|
|
32
|
+
const img = new Image()
|
|
33
|
+
img.onload = () => {
|
|
34
|
+
const scale = Math.min(1, MAX_EDGE / Math.max(img.width, img.height))
|
|
35
|
+
const canvas = document.createElement('canvas')
|
|
36
|
+
canvas.width = Math.max(1, Math.round(img.width * scale))
|
|
37
|
+
canvas.height = Math.max(1, Math.round(img.height * scale))
|
|
38
|
+
const ctx = canvas.getContext('2d')
|
|
39
|
+
if (!ctx) { reject(new Error('canvas unavailable')); return }
|
|
40
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
41
|
+
let out = canvas.toDataURL('image/jpeg', 0.82)
|
|
42
|
+
// Quality ladder until it fits (or we give up after several tries).
|
|
43
|
+
for (const q of [0.7, 0.55, 0.4]) {
|
|
44
|
+
if (out.length <= maxBytes) break
|
|
45
|
+
out = canvas.toDataURL('image/jpeg', q)
|
|
46
|
+
}
|
|
47
|
+
resolve(out)
|
|
48
|
+
}
|
|
49
|
+
img.onerror = () => reject(new Error('failed to decode image'))
|
|
50
|
+
img.src = src
|
|
51
|
+
}
|
|
52
|
+
reader.readAsDataURL(file)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function UploadInput(props: UploadInputProps): ReactElement {
|
|
57
|
+
const { maxBytes, t, disabled = false, imageDataUrl = null, file = null, onFile } = props
|
|
58
|
+
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
59
|
+
const [dragging, setDragging] = useState(false)
|
|
60
|
+
const [error, setError] = useState<string | null>(null)
|
|
61
|
+
|
|
62
|
+
const accept = useCallback(async (f: File | null): Promise<void> => {
|
|
63
|
+
if (!f) return
|
|
64
|
+
if (f.size > maxBytes) {
|
|
65
|
+
setError(t('upload.imageTooLarge'))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
setError(null)
|
|
69
|
+
try {
|
|
70
|
+
const dataUrl = await fileToDataUrl(f, maxBytes)
|
|
71
|
+
onFile(f, dataUrl)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setError(String((e as Error)?.message || e))
|
|
74
|
+
}
|
|
75
|
+
}, [maxBytes, onFile, t])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={`cga-upload${dragging ? ' cga-upload--dragging' : ''}`}
|
|
80
|
+
onClick={() => { if (!disabled) inputRef.current?.click() }}
|
|
81
|
+
onDragOver={(ev) => { ev.preventDefault(); if (!disabled) setDragging(true) }}
|
|
82
|
+
onDragLeave={() => setDragging(false)}
|
|
83
|
+
onDrop={(ev) => {
|
|
84
|
+
ev.preventDefault()
|
|
85
|
+
setDragging(false)
|
|
86
|
+
if (disabled) return
|
|
87
|
+
void accept(ev.dataTransfer.files?.[0] ?? null)
|
|
88
|
+
}}
|
|
89
|
+
onPaste={(ev) => {
|
|
90
|
+
const item = Array.from(ev.clipboardData?.items ?? []).find((i) => i.type.startsWith('image/'))
|
|
91
|
+
if (item) {
|
|
92
|
+
ev.preventDefault()
|
|
93
|
+
void accept(item.getAsFile())
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<input
|
|
98
|
+
ref={inputRef}
|
|
99
|
+
type="file"
|
|
100
|
+
accept="image/*"
|
|
101
|
+
style={{ display: 'none' }}
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
onChange={(ev) => { void accept(ev.target.files?.[0] ?? null); ev.target.value = '' }}
|
|
104
|
+
/>
|
|
105
|
+
{imageDataUrl
|
|
106
|
+
? (
|
|
107
|
+
<>
|
|
108
|
+
<img className="cga-upload__thumb" src={imageDataUrl} alt="" />
|
|
109
|
+
<div className="cga-upload__text">{file?.name ?? ''}</div>
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
: null}
|
|
113
|
+
<div className="cga-upload__text">
|
|
114
|
+
{imageDataUrl ? t('upload.replace') : t('upload.drop')}
|
|
115
|
+
<br />{t('upload.paste')}
|
|
116
|
+
</div>
|
|
117
|
+
{error ? <div className="cga-upload__text" style={{ color: 'var(--dsw-alias-state-error-primary, red)' }}>{error}</div> : null}
|
|
118
|
+
{!imageDataUrl ? <button type="button" className="cga-upload__browse">{t('upload.browse')}</button> : null}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
package/src/copy/en.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — en copy pack.
|
|
3
|
+
*
|
|
4
|
+
* Typed structurally against `ZH` (`DeepStrings`) so a missing or misshapen EN
|
|
5
|
+
* key is a compile error. zh stays the fallback everywhere.
|
|
6
|
+
*/
|
|
7
|
+
import type { ZH } from './zh.js'
|
|
8
|
+
|
|
9
|
+
type DeepStrings<T> = { [K in keyof T]: T[K] extends string ? string : DeepStrings<T[K]> }
|
|
10
|
+
|
|
11
|
+
export const EN: DeepStrings<typeof ZH> = {
|
|
12
|
+
app: {
|
|
13
|
+
title: 'Huaqiu Component Gen',
|
|
14
|
+
symbolTitle: 'Symbol Gen',
|
|
15
|
+
footprintTitle: 'Footprint Gen',
|
|
16
|
+
footprintTooltip: 'Open footprint generator',
|
|
17
|
+
symbolTooltip: 'Open symbol generator',
|
|
18
|
+
close: 'Close',
|
|
19
|
+
back: 'Back',
|
|
20
|
+
loading: 'Loading…',
|
|
21
|
+
error: 'Error',
|
|
22
|
+
unknownError: 'Unknown error',
|
|
23
|
+
},
|
|
24
|
+
upload: {
|
|
25
|
+
drop: 'Drop an image here, or click to upload',
|
|
26
|
+
paste: 'Or paste with Ctrl/⌘+V',
|
|
27
|
+
browse: 'Choose image',
|
|
28
|
+
imageTooLarge: 'Image too large; keep it under 4 MiB',
|
|
29
|
+
noImage: 'Upload an image first',
|
|
30
|
+
replace: 'Replace image',
|
|
31
|
+
uploading: 'Uploading…',
|
|
32
|
+
hint: 'Hint',
|
|
33
|
+
},
|
|
34
|
+
symbol: {
|
|
35
|
+
generate: 'Generate Symbol',
|
|
36
|
+
regenerate: 'Regenerate',
|
|
37
|
+
instructionPlaceholder: 'Optional instruction, e.g. "3-pin LDO, pin 1 is VIN"',
|
|
38
|
+
progress: 'Generating symbol…',
|
|
39
|
+
ready: 'Generated',
|
|
40
|
+
failed: 'Generation failed',
|
|
41
|
+
download: 'Download .kicad_sym',
|
|
42
|
+
save: 'Save to history',
|
|
43
|
+
saved: 'Saved to history',
|
|
44
|
+
},
|
|
45
|
+
footprint: {
|
|
46
|
+
extract: 'Extract dimensions',
|
|
47
|
+
generate: 'Generate Footprint',
|
|
48
|
+
regenerate: 'Regenerate',
|
|
49
|
+
hintPlaceholder: 'Optional package type hint, e.g. BGA / QFN / SOP',
|
|
50
|
+
extractProgress: 'Extracting package dimensions…',
|
|
51
|
+
generateProgress: 'Generating footprint…',
|
|
52
|
+
ready: 'Generated',
|
|
53
|
+
failed: 'Generation failed',
|
|
54
|
+
needsConfirmation: 'Confirm or correct the extracted package dimensions',
|
|
55
|
+
download: 'Download .kicad_mod',
|
|
56
|
+
save: 'Save to history',
|
|
57
|
+
saved: 'Saved to history',
|
|
58
|
+
directGenerated: 'A standard footprint was auto-generated; review it below',
|
|
59
|
+
autoGenerated: 'Auto-generated',
|
|
60
|
+
confirm: 'Confirm & generate',
|
|
61
|
+
cancel: 'Cancel',
|
|
62
|
+
},
|
|
63
|
+
editor: {
|
|
64
|
+
essential: 'Essential',
|
|
65
|
+
advanced: 'Advanced',
|
|
66
|
+
width: 'Width',
|
|
67
|
+
height: 'Height',
|
|
68
|
+
pinCount: 'Pin count',
|
|
69
|
+
pitch: 'Pitch',
|
|
70
|
+
packageType: 'Package type',
|
|
71
|
+
fileName: 'File name',
|
|
72
|
+
editedTag: 'Edited',
|
|
73
|
+
aiTag: 'AI-extracted',
|
|
74
|
+
dragHint: 'Drag the handles to adjust W / H',
|
|
75
|
+
validationOutOfRange: 'Out of range',
|
|
76
|
+
validationMinGtMax: 'Min > max',
|
|
77
|
+
validationInvalid: 'Invalid value',
|
|
78
|
+
unit: 'mm',
|
|
79
|
+
body: 'Body',
|
|
80
|
+
pins: '{count} pins',
|
|
81
|
+
validationIssue: '{n} value(s) need fixing',
|
|
82
|
+
validationOk: 'Values are valid',
|
|
83
|
+
issueOutOfRange: 'Out of allowed range',
|
|
84
|
+
issueMinGtMax: 'Min > max',
|
|
85
|
+
confirmLabel: 'Confirm',
|
|
86
|
+
cancelLabel: 'Cancel',
|
|
87
|
+
},
|
|
88
|
+
field: {
|
|
89
|
+
width: 'Width',
|
|
90
|
+
height: 'Height',
|
|
91
|
+
length: 'Length',
|
|
92
|
+
depth: 'Depth',
|
|
93
|
+
span: 'Span',
|
|
94
|
+
bodyWidth: 'Body width',
|
|
95
|
+
bodyLength: 'Body length',
|
|
96
|
+
bodyHeight: 'Body height',
|
|
97
|
+
boardWidth: 'Board width',
|
|
98
|
+
boardHeight: 'Board height',
|
|
99
|
+
overallWidth: 'Overall width',
|
|
100
|
+
overallLength: 'Overall length',
|
|
101
|
+
overallHeight: 'Overall height',
|
|
102
|
+
pitch: 'Pitch',
|
|
103
|
+
pitchX: 'Pitch X',
|
|
104
|
+
pitchY: 'Pitch Y',
|
|
105
|
+
leadPitch: 'Lead pitch',
|
|
106
|
+
padWidth: 'Pad width',
|
|
107
|
+
padLength: 'Pad length',
|
|
108
|
+
padHeight: 'Pad height',
|
|
109
|
+
leadWidth: 'Lead width',
|
|
110
|
+
leadLength: 'Lead length',
|
|
111
|
+
leadSpan: 'Lead span',
|
|
112
|
+
pinCount: 'Pin count',
|
|
113
|
+
rows: 'Rows',
|
|
114
|
+
columns: 'Columns',
|
|
115
|
+
standoff: 'Standoff',
|
|
116
|
+
maxOf: '{field} max',
|
|
117
|
+
minOf: '{field} min',
|
|
118
|
+
},
|
|
119
|
+
history: {
|
|
120
|
+
title: 'History',
|
|
121
|
+
empty: 'No history yet',
|
|
122
|
+
symbol: 'Symbol',
|
|
123
|
+
footprint: 'Footprint',
|
|
124
|
+
generated: 'Generated',
|
|
125
|
+
failed: 'Failed',
|
|
126
|
+
cancelled: 'Cancelled',
|
|
127
|
+
rename: 'Rename',
|
|
128
|
+
delete: 'Delete',
|
|
129
|
+
download: 'Download',
|
|
130
|
+
reopen: 'Open',
|
|
131
|
+
view: 'View history',
|
|
132
|
+
createdAt: 'Created',
|
|
133
|
+
loadMore: 'Load more',
|
|
134
|
+
noMore: 'No more',
|
|
135
|
+
},
|
|
136
|
+
auth: {
|
|
137
|
+
notLoggedIn: 'Not logged in to Huaqiu EDA',
|
|
138
|
+
login: 'Login to Huaqiu EDA',
|
|
139
|
+
loginRequired: 'This feature requires a Huaqiu EDA login',
|
|
140
|
+
loggedIn: 'Logged in',
|
|
141
|
+
},
|
|
142
|
+
status: {
|
|
143
|
+
queued: 'Queued',
|
|
144
|
+
running: 'Running',
|
|
145
|
+
needsConfirmation: 'Needs confirmation',
|
|
146
|
+
completed: 'Completed',
|
|
147
|
+
failed: 'Failed',
|
|
148
|
+
cancelled: 'Cancelled',
|
|
149
|
+
},
|
|
150
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — copy packs + translate resolver.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the `dsh-tool-symbol-footprint` house convention:
|
|
5
|
+
* - `ZH as const` → `CopyKey` → `EN: Record<CopyKey, string>` (missing EN
|
|
6
|
+
* key is a compile error; zh is the runtime fallback);
|
|
7
|
+
* - `Translate = (key, params?) => string` with `{param}` substitution;
|
|
8
|
+
* - `fieldLabel` (utils/labels.ts) resolves dimension keys through the
|
|
9
|
+
* `field.*` copy section, so labels follow the UI language.
|
|
10
|
+
*/
|
|
11
|
+
import { ZH, type CopyKey } from './zh.js'
|
|
12
|
+
import { EN } from './en.js'
|
|
13
|
+
|
|
14
|
+
export type { CopyKey } from './zh.js'
|
|
15
|
+
export { ZH } from './zh.js'
|
|
16
|
+
export { EN } from './en.js'
|
|
17
|
+
|
|
18
|
+
export type Translate = (key: string, params?: Record<string, unknown>) => string
|
|
19
|
+
|
|
20
|
+
type FlatKey = string
|
|
21
|
+
|
|
22
|
+
function lookup(pack: Record<string, unknown>, keys: FlatKey): string | undefined {
|
|
23
|
+
let node: unknown = pack
|
|
24
|
+
for (const seg of keys.split('.')) {
|
|
25
|
+
if (node && typeof node === 'object') node = (node as Record<string, unknown>)[seg]
|
|
26
|
+
else return undefined
|
|
27
|
+
}
|
|
28
|
+
return typeof node === 'string' ? node : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function translate(lang: string | undefined, key: string, params?: Record<string, unknown>): string {
|
|
32
|
+
const pack = lang?.toLowerCase().startsWith('en') ? (EN as unknown as Record<string, unknown>) : ZH as unknown as Record<string, unknown>
|
|
33
|
+
let v = lookup(pack, key)
|
|
34
|
+
if (v === undefined) v = lookup(ZH as unknown as Record<string, unknown>, key)
|
|
35
|
+
if (v === undefined) v = key
|
|
36
|
+
if (params) {
|
|
37
|
+
for (const k of Object.keys(params)) {
|
|
38
|
+
v = v.split(`{${k}}`).join(String(params[k]))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return v
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build a `(key, params?) => string` resolver for a given language. */
|
|
45
|
+
export function translateFor(lang: string | undefined): Translate {
|
|
46
|
+
return (key, params) => translate(lang, key, params)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** zh-only fallback (used when no locale is supplied). */
|
|
50
|
+
export const defaultT: Translate = (key, params) => translate('zh', key, params)
|
package/src/copy/zh.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — zh copy pack (fallback everywhere).
|
|
3
|
+
*
|
|
4
|
+
* House i18n convention (`dsh-pcb-eda`): `ZH as const` → `CopyKey` →
|
|
5
|
+
* `EN: Record<CopyKey, string>` so a missing EN key is a compile error.
|
|
6
|
+
* Punctuation is i18n too(中文全角:;).
|
|
7
|
+
*/
|
|
8
|
+
export const ZH = {
|
|
9
|
+
app: {
|
|
10
|
+
title: '华秋元器件生成',
|
|
11
|
+
symbolTitle: '符号生成',
|
|
12
|
+
footprintTitle: '封装生成',
|
|
13
|
+
footprintTooltip: '打开封装生成',
|
|
14
|
+
symbolTooltip: '打开符号生成',
|
|
15
|
+
close: '关闭',
|
|
16
|
+
back: '返回',
|
|
17
|
+
loading: '加载中…',
|
|
18
|
+
error: '出错了',
|
|
19
|
+
unknownError: '未知错误',
|
|
20
|
+
},
|
|
21
|
+
upload: {
|
|
22
|
+
drop: '拖拽图片到此处,或点击上传',
|
|
23
|
+
paste: '也可直接 Ctrl/⌘+V 粘贴图片',
|
|
24
|
+
browse: '选择图片',
|
|
25
|
+
imageTooLarge: '图片过大,请使用 4MiB 以内的图片',
|
|
26
|
+
noImage: '请先上传图片',
|
|
27
|
+
replace: '更换图片',
|
|
28
|
+
uploading: '上传中…',
|
|
29
|
+
hint: '提示',
|
|
30
|
+
},
|
|
31
|
+
symbol: {
|
|
32
|
+
generate: '生成符号',
|
|
33
|
+
regenerate: '重新生成',
|
|
34
|
+
instructionPlaceholder: '可选:补充生成说明,如“3 引脚 LDO,引脚 1 为 VIN”',
|
|
35
|
+
progress: '正在生成符号…',
|
|
36
|
+
ready: '生成完成',
|
|
37
|
+
failed: '生成失败',
|
|
38
|
+
download: '下载 .kicad_sym',
|
|
39
|
+
save: '保存到历史',
|
|
40
|
+
saved: '已保存到历史',
|
|
41
|
+
},
|
|
42
|
+
footprint: {
|
|
43
|
+
extract: '提取尺寸',
|
|
44
|
+
generate: '生成封装',
|
|
45
|
+
regenerate: '重新生成',
|
|
46
|
+
hintPlaceholder: '可选:封装类型提示,如 BGA / QFN / SOP',
|
|
47
|
+
extractProgress: '正在提取封装尺寸…',
|
|
48
|
+
generateProgress: '正在生成封装…',
|
|
49
|
+
ready: '生成完成',
|
|
50
|
+
failed: '生成失败',
|
|
51
|
+
needsConfirmation: '请确认或修改提取到的封装尺寸',
|
|
52
|
+
download: '下载 .kicad_mod',
|
|
53
|
+
save: '保存到历史',
|
|
54
|
+
saved: '已保存到历史',
|
|
55
|
+
directGenerated: '已自动生成标准封装,可预览后下载',
|
|
56
|
+
autoGenerated: '自动生成',
|
|
57
|
+
confirm: '确认并生成',
|
|
58
|
+
cancel: '取消',
|
|
59
|
+
},
|
|
60
|
+
editor: {
|
|
61
|
+
essential: '基本参数',
|
|
62
|
+
advanced: '高级参数',
|
|
63
|
+
width: '宽度',
|
|
64
|
+
height: '长度',
|
|
65
|
+
pinCount: '引脚数',
|
|
66
|
+
pitch: '间距',
|
|
67
|
+
packageType: '封装类型',
|
|
68
|
+
fileName: '文件名',
|
|
69
|
+
editedTag: '已修改',
|
|
70
|
+
aiTag: 'AI 提取',
|
|
71
|
+
dragHint: '拖动控制柄调整 W / H',
|
|
72
|
+
validationOutOfRange: '超出范围',
|
|
73
|
+
validationMinGtMax: '最小值大于最大值',
|
|
74
|
+
validationInvalid: '无效数值',
|
|
75
|
+
unit: 'mm',
|
|
76
|
+
body: '本体',
|
|
77
|
+
pins: '{count} 引脚',
|
|
78
|
+
validationIssue: '{n} 处数值需要修正',
|
|
79
|
+
validationOk: '数值有效',
|
|
80
|
+
issueOutOfRange: '超出允许范围',
|
|
81
|
+
issueMinGtMax: '最小值大于最大值',
|
|
82
|
+
confirmLabel: '确认',
|
|
83
|
+
cancelLabel: '取消',
|
|
84
|
+
},
|
|
85
|
+
field: {
|
|
86
|
+
width: '宽度',
|
|
87
|
+
height: '长度',
|
|
88
|
+
length: '长度',
|
|
89
|
+
depth: '深度',
|
|
90
|
+
span: '跨距',
|
|
91
|
+
bodyWidth: '本体宽度',
|
|
92
|
+
bodyLength: '本体长度',
|
|
93
|
+
bodyHeight: '本体高度',
|
|
94
|
+
boardWidth: '板宽',
|
|
95
|
+
boardHeight: '板高',
|
|
96
|
+
overallWidth: '总宽度',
|
|
97
|
+
overallLength: '总长度',
|
|
98
|
+
overallHeight: '总高度',
|
|
99
|
+
pitch: '间距',
|
|
100
|
+
pitchX: 'X 间距',
|
|
101
|
+
pitchY: 'Y 间距',
|
|
102
|
+
leadPitch: '引线间距',
|
|
103
|
+
padWidth: '焊盘宽',
|
|
104
|
+
padLength: '焊盘长',
|
|
105
|
+
padHeight: '焊盘高',
|
|
106
|
+
leadWidth: '引线宽',
|
|
107
|
+
leadLength: '引线长',
|
|
108
|
+
leadSpan: '引线跨距',
|
|
109
|
+
pinCount: '引脚数',
|
|
110
|
+
rows: '行数',
|
|
111
|
+
columns: '列数',
|
|
112
|
+
standoff: '离地高度',
|
|
113
|
+
maxOf: '{field} 最大值',
|
|
114
|
+
minOf: '{field} 最小值',
|
|
115
|
+
},
|
|
116
|
+
history: {
|
|
117
|
+
title: '历史记录',
|
|
118
|
+
empty: '暂无历史',
|
|
119
|
+
symbol: '符号',
|
|
120
|
+
footprint: '封装',
|
|
121
|
+
generated: '已生成',
|
|
122
|
+
failed: '失败',
|
|
123
|
+
cancelled: '已取消',
|
|
124
|
+
rename: '重命名',
|
|
125
|
+
delete: '删除',
|
|
126
|
+
download: '下载',
|
|
127
|
+
reopen: '打开',
|
|
128
|
+
view: '查看历史',
|
|
129
|
+
createdAt: '创建时间',
|
|
130
|
+
loadMore: '加载更多',
|
|
131
|
+
noMore: '没有更多了',
|
|
132
|
+
},
|
|
133
|
+
auth: {
|
|
134
|
+
notLoggedIn: '未登录华秋账号',
|
|
135
|
+
login: '登录华秋 EDA',
|
|
136
|
+
loginRequired: '该功能需要登录华秋 EDA 账号',
|
|
137
|
+
loggedIn: '已登录',
|
|
138
|
+
},
|
|
139
|
+
status: {
|
|
140
|
+
queued: '排队中',
|
|
141
|
+
running: '进行中',
|
|
142
|
+
needsConfirmation: '待确认',
|
|
143
|
+
completed: '已完成',
|
|
144
|
+
failed: '失败',
|
|
145
|
+
cancelled: '已取消',
|
|
146
|
+
},
|
|
147
|
+
} as const
|
|
148
|
+
|
|
149
|
+
/** Flat dotted key union derived from the nested `ZH` (e.g. "app.title"). */
|
|
150
|
+
export type CopyKey = LeafKeys<typeof ZH>
|
|
151
|
+
|
|
152
|
+
type LeafKeys<T> = {
|
|
153
|
+
[K in keyof T & string]: T[K] extends string ? K : `${K}.${LeafKeys<T[K]>}`
|
|
154
|
+
}[keyof T & string]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — auth gate over `ComponentGenPorts.auth`.
|
|
3
|
+
*
|
|
4
|
+
* The app only knows the public `@huaqiu/dsh-auth` surface (the host wires it):
|
|
5
|
+
* read state, subscribe to changes, trigger the existing login flow. It never
|
|
6
|
+
* implements auth itself.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
9
|
+
import type { ComponentGenPorts } from '../ports.js'
|
|
10
|
+
|
|
11
|
+
export type AuthPhase = 'unknown' | 'authenticated' | 'unauthenticated'
|
|
12
|
+
|
|
13
|
+
export interface UseAuthGateResult {
|
|
14
|
+
phase: AuthPhase
|
|
15
|
+
user: { nickname?: string } | null
|
|
16
|
+
login: () => void
|
|
17
|
+
/** re-check immediately (used after login dialog closes). */
|
|
18
|
+
refresh: () => Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useAuthGate(ports: ComponentGenPorts): UseAuthGateResult {
|
|
22
|
+
const [phase, setPhase] = useState<AuthPhase>('unknown')
|
|
23
|
+
const [user, setUser] = useState<{ nickname?: string } | null>(null)
|
|
24
|
+
|
|
25
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
26
|
+
try {
|
|
27
|
+
const ok = await ports.auth.isAuthenticated()
|
|
28
|
+
setPhase(ok ? 'authenticated' : 'unauthenticated')
|
|
29
|
+
setUser(ok ? await ports.auth.getUserInfo() : null)
|
|
30
|
+
} catch {
|
|
31
|
+
setPhase('unauthenticated')
|
|
32
|
+
setUser(null)
|
|
33
|
+
}
|
|
34
|
+
}, [ports])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
void refresh()
|
|
38
|
+
const unsub = ports.auth.onAuthStateChanged((authenticated) => {
|
|
39
|
+
setPhase(authenticated ? 'authenticated' : 'unauthenticated')
|
|
40
|
+
if (authenticated) void ports.auth.getUserInfo().then(setUser)
|
|
41
|
+
else setUser(null)
|
|
42
|
+
})
|
|
43
|
+
return unsub
|
|
44
|
+
}, [ports, refresh])
|
|
45
|
+
|
|
46
|
+
const login = useCallback(() => {
|
|
47
|
+
void ports.auth.login()
|
|
48
|
+
}, [ports])
|
|
49
|
+
|
|
50
|
+
return { phase, user, login, refresh }
|
|
51
|
+
}
|