@jlcpcb/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/api/easyeda-community.d.ts +36 -0
  3. package/dist/api/easyeda-community.d.ts.map +1 -0
  4. package/dist/api/easyeda.d.ts +23 -0
  5. package/dist/api/easyeda.d.ts.map +1 -0
  6. package/dist/api/index.d.ts +7 -0
  7. package/dist/api/index.d.ts.map +1 -0
  8. package/dist/api/jlc.d.ts +41 -0
  9. package/dist/api/jlc.d.ts.map +1 -0
  10. package/dist/assets/search.html +528 -0
  11. package/dist/browser/index.d.ts +8 -0
  12. package/dist/browser/index.d.ts.map +1 -0
  13. package/dist/browser/kicad-renderer.d.ts +13 -0
  14. package/dist/browser/kicad-renderer.d.ts.map +1 -0
  15. package/dist/browser/sexpr-parser.d.ts +84 -0
  16. package/dist/browser/sexpr-parser.d.ts.map +1 -0
  17. package/dist/constants/design-rules.d.ts +34 -0
  18. package/dist/constants/design-rules.d.ts.map +1 -0
  19. package/dist/constants/footprints.d.ts +134 -0
  20. package/dist/constants/footprints.d.ts.map +1 -0
  21. package/dist/constants/index.d.ts +7 -0
  22. package/dist/constants/index.d.ts.map +1 -0
  23. package/dist/constants/kicad.d.ts +67 -0
  24. package/dist/constants/kicad.d.ts.map +1 -0
  25. package/dist/converter/category-router.d.ts +47 -0
  26. package/dist/converter/category-router.d.ts.map +1 -0
  27. package/dist/converter/footprint-mapper.d.ts +40 -0
  28. package/dist/converter/footprint-mapper.d.ts.map +1 -0
  29. package/dist/converter/footprint-mapper.test.d.ts +2 -0
  30. package/dist/converter/footprint-mapper.test.d.ts.map +1 -0
  31. package/dist/converter/footprint.d.ts +116 -0
  32. package/dist/converter/footprint.d.ts.map +1 -0
  33. package/dist/converter/global-lib-table.d.ts +29 -0
  34. package/dist/converter/global-lib-table.d.ts.map +1 -0
  35. package/dist/converter/index.d.ts +12 -0
  36. package/dist/converter/index.d.ts.map +1 -0
  37. package/dist/converter/lib-table.d.ts +61 -0
  38. package/dist/converter/lib-table.d.ts.map +1 -0
  39. package/dist/converter/svg-arc.d.ts +45 -0
  40. package/dist/converter/svg-arc.d.ts.map +1 -0
  41. package/dist/converter/symbol-templates.d.ts +34 -0
  42. package/dist/converter/symbol-templates.d.ts.map +1 -0
  43. package/dist/converter/symbol.d.ts +223 -0
  44. package/dist/converter/symbol.d.ts.map +1 -0
  45. package/dist/converter/value-normalizer.d.ts +33 -0
  46. package/dist/converter/value-normalizer.d.ts.map +1 -0
  47. package/dist/http/index.d.ts +5 -0
  48. package/dist/http/index.d.ts.map +1 -0
  49. package/dist/http/routes.d.ts +12 -0
  50. package/dist/http/routes.d.ts.map +1 -0
  51. package/dist/http/server.d.ts +18 -0
  52. package/dist/http/server.d.ts.map +1 -0
  53. package/dist/index.d.ts +13 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +9633 -0
  56. package/dist/parsers/easyeda-shapes.d.ts +115 -0
  57. package/dist/parsers/easyeda-shapes.d.ts.map +1 -0
  58. package/dist/parsers/http-client.d.ts +16 -0
  59. package/dist/parsers/http-client.d.ts.map +1 -0
  60. package/dist/parsers/index.d.ts +11 -0
  61. package/dist/parsers/index.d.ts.map +1 -0
  62. package/dist/parsers/utils.d.ts +17 -0
  63. package/dist/parsers/utils.d.ts.map +1 -0
  64. package/dist/services/component-service.d.ts +31 -0
  65. package/dist/services/component-service.d.ts.map +1 -0
  66. package/dist/services/fix-service.d.ts +40 -0
  67. package/dist/services/fix-service.d.ts.map +1 -0
  68. package/dist/services/index.d.ts +8 -0
  69. package/dist/services/index.d.ts.map +1 -0
  70. package/dist/services/library-service.d.ts +112 -0
  71. package/dist/services/library-service.d.ts.map +1 -0
  72. package/dist/types/component.d.ts +56 -0
  73. package/dist/types/component.d.ts.map +1 -0
  74. package/dist/types/easyeda-community.d.ts +74 -0
  75. package/dist/types/easyeda-community.d.ts.map +1 -0
  76. package/dist/types/easyeda.d.ts +326 -0
  77. package/dist/types/easyeda.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +12 -0
  79. package/dist/types/index.d.ts.map +1 -0
  80. package/dist/types/jlc.d.ts +78 -0
  81. package/dist/types/jlc.d.ts.map +1 -0
  82. package/dist/types/kicad.d.ts +141 -0
  83. package/dist/types/kicad.d.ts.map +1 -0
  84. package/dist/types/mcp.d.ts +66 -0
  85. package/dist/types/mcp.d.ts.map +1 -0
  86. package/dist/types/project.d.ts +60 -0
  87. package/dist/types/project.d.ts.map +1 -0
  88. package/dist/utils/conversion.d.ts +59 -0
  89. package/dist/utils/conversion.d.ts.map +1 -0
  90. package/dist/utils/file-system.d.ts +59 -0
  91. package/dist/utils/file-system.d.ts.map +1 -0
  92. package/dist/utils/index.d.ts +8 -0
  93. package/dist/utils/index.d.ts.map +1 -0
  94. package/dist/utils/logger.d.ts +26 -0
  95. package/dist/utils/logger.d.ts.map +1 -0
  96. package/dist/utils/validation.d.ts +259 -0
  97. package/dist/utils/validation.d.ts.map +1 -0
  98. package/package.json +5 -3
  99. package/scripts/build-search-page.ts +68 -0
  100. package/src/assets/search-built.html +528 -0
  101. package/src/assets/search.html +458 -0
  102. package/src/browser/index.ts +389 -0
  103. package/src/browser/kicad-renderer.ts +813 -0
  104. package/src/browser/sexpr-parser.ts +333 -0
  105. package/src/converter/footprint-mapper.test.ts +159 -0
  106. package/src/converter/footprint-mapper.ts +42 -134
  107. package/src/converter/footprint.ts +208 -36
  108. package/src/converter/global-lib-table.ts +71 -0
  109. package/src/http/index.ts +5 -0
  110. package/src/http/routes.ts +266 -0
  111. package/src/http/server.ts +83 -0
  112. package/src/index.ts +3 -0
  113. package/src/parsers/easyeda-shapes.ts +2 -1
  114. package/src/services/library-service.ts +73 -22
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Browser app for EasyEDA Component Browser
3
+ * Handles search, rendering, and user interactions
4
+ *
5
+ * Uses KiCad S-expression renderer for symbol/footprint previews
6
+ */
7
+
8
+ import { renderSymbolSvg, renderFootprintSvg } from './kicad-renderer.js'
9
+
10
+ // Types
11
+ interface SearchResult {
12
+ uuid: string
13
+ title: string
14
+ thumb: string
15
+ description: string
16
+ tags: string[]
17
+ package: string
18
+ packageUuid?: string
19
+ manufacturer?: string
20
+ owner: {
21
+ uuid: string
22
+ username: string
23
+ nickname: string
24
+ avatar?: string
25
+ }
26
+ docType: number
27
+ }
28
+
29
+ interface Pagination {
30
+ page: number
31
+ limit: number
32
+ total: number
33
+ totalPages: number
34
+ hasNext: boolean
35
+ hasPrev: boolean
36
+ }
37
+
38
+ interface SearchResponse {
39
+ results: SearchResult[]
40
+ pagination: Pagination
41
+ }
42
+
43
+ /**
44
+ * Component data returned by the API
45
+ * Contains KiCad S-expression strings for rendering
46
+ */
47
+ interface ComponentData {
48
+ uuid: string
49
+ title: string
50
+ description: string
51
+ symbolSexpr: string // KiCad symbol S-expression
52
+ footprintSexpr: string // KiCad footprint S-expression
53
+ model3d?: {
54
+ name: string
55
+ uuid: string
56
+ }
57
+ }
58
+
59
+ // State
60
+ let currentPage = 1
61
+ let currentQuery = ''
62
+ let currentSource = 'user'
63
+ let isLoading = false
64
+ let debounceTimer: number | null = null
65
+
66
+ // DOM elements
67
+ const searchInput = document.getElementById('search-input') as HTMLInputElement
68
+ const sourceSelect = document.getElementById('source-select') as HTMLSelectElement
69
+ const searchBtn = document.getElementById('search-btn') as HTMLButtonElement
70
+ const resultsGrid = document.getElementById('results-grid') as HTMLDivElement
71
+ const paginationDiv = document.getElementById('pagination') as HTMLDivElement
72
+ const loadingDiv = document.getElementById('loading') as HTMLDivElement
73
+ const modal = document.getElementById('preview-modal') as HTMLDivElement
74
+ const modalContent = document.getElementById('modal-content') as HTMLDivElement
75
+ const modalClose = document.getElementById('modal-close') as HTMLButtonElement
76
+
77
+ // Initialize
78
+ function init() {
79
+ // Check for server-injected query first
80
+ const initialQuery = (window as unknown as { __INITIAL_QUERY__?: string }).__INITIAL_QUERY__
81
+ if (initialQuery) {
82
+ searchInput.value = initialQuery
83
+ currentQuery = initialQuery
84
+ performSearch()
85
+ } else {
86
+ // Fallback to URL param
87
+ const params = new URLSearchParams(window.location.search)
88
+ const q = params.get('q')
89
+ if (q) {
90
+ searchInput.value = q
91
+ currentQuery = q
92
+ performSearch()
93
+ }
94
+ }
95
+
96
+ // Event listeners
97
+ searchInput.addEventListener('input', handleSearchInput)
98
+ searchInput.addEventListener('keydown', (e) => {
99
+ if (e.key === 'Enter') {
100
+ e.preventDefault()
101
+ performSearch()
102
+ }
103
+ })
104
+ searchBtn.addEventListener('click', performSearch)
105
+ sourceSelect.addEventListener('change', () => {
106
+ currentSource = sourceSelect.value
107
+ if (currentQuery) performSearch()
108
+ })
109
+ modalClose.addEventListener('click', closeModal)
110
+ modal.addEventListener('click', (e) => {
111
+ if (e.target === modal) closeModal()
112
+ })
113
+ document.addEventListener('keydown', (e) => {
114
+ if (e.key === 'Escape') closeModal()
115
+ })
116
+ }
117
+
118
+ // Debounced search input
119
+ function handleSearchInput() {
120
+ if (debounceTimer) clearTimeout(debounceTimer)
121
+ debounceTimer = window.setTimeout(() => {
122
+ const query = searchInput.value.trim()
123
+ if (query && query !== currentQuery) {
124
+ currentQuery = query
125
+ currentPage = 1
126
+ performSearch()
127
+ }
128
+ }, 300)
129
+ }
130
+
131
+ // Perform search
132
+ async function performSearch() {
133
+ const query = searchInput.value.trim()
134
+ if (!query || isLoading) return
135
+
136
+ currentQuery = query
137
+ isLoading = true
138
+ showLoading(true)
139
+
140
+ try {
141
+ const params = new URLSearchParams({
142
+ q: query,
143
+ source: currentSource,
144
+ page: String(currentPage),
145
+ limit: '20',
146
+ })
147
+
148
+ const response = await fetch(`/api/search?${params}`)
149
+ if (!response.ok) throw new Error('Search failed')
150
+
151
+ const data: SearchResponse = await response.json()
152
+ renderResults(data.results)
153
+ renderPagination(data.pagination)
154
+ } catch (error) {
155
+ console.error('Search error:', error)
156
+ resultsGrid.innerHTML = `<div class="error">Search failed. Please try again.</div>`
157
+ } finally {
158
+ isLoading = false
159
+ showLoading(false)
160
+ }
161
+ }
162
+
163
+ // Render search results
164
+ function renderResults(results: SearchResult[]) {
165
+ if (results.length === 0) {
166
+ resultsGrid.innerHTML = `<div class="no-results">No components found. Try a different search term.</div>`
167
+ return
168
+ }
169
+
170
+ resultsGrid.innerHTML = results.map(result => `
171
+ <div class="card" data-uuid="${result.uuid}">
172
+ <div class="card-images">
173
+ <div class="image-container symbol-container" data-uuid="${result.uuid}">
174
+ <div class="symbol-placeholder">Loading...</div>
175
+ <div class="image-label">Symbol</div>
176
+ </div>
177
+ <div class="image-container footprint-container" data-uuid="${result.uuid}">
178
+ <div class="footprint-placeholder">Loading...</div>
179
+ <div class="image-label">Footprint</div>
180
+ </div>
181
+ </div>
182
+ <div class="card-info">
183
+ <div class="card-title" title="${escapeHtml(result.title)}">${escapeHtml(result.title)}</div>
184
+ <div class="card-package">Package: ${escapeHtml(result.package || 'Unknown')}</div>
185
+ <div class="card-owner">By: ${escapeHtml(result.owner.nickname || result.owner.username)}</div>
186
+ </div>
187
+ <div class="card-uuid">
188
+ <input type="text" value="${result.uuid}" readonly />
189
+ <button class="copy-btn" data-uuid="${result.uuid}" title="Copy UUID">
190
+ <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
191
+ </button>
192
+ </div>
193
+ </div>
194
+ `).join('')
195
+
196
+ // Add copy button handlers
197
+ document.querySelectorAll('.copy-btn').forEach(btn => {
198
+ btn.addEventListener('click', (e) => {
199
+ e.stopPropagation()
200
+ const uuid = (btn as HTMLElement).dataset.uuid
201
+ if (uuid) copyToClipboard(uuid, btn as HTMLElement)
202
+ })
203
+ })
204
+
205
+ // Add hover handlers for enlarging
206
+ document.querySelectorAll('.image-container').forEach(container => {
207
+ container.addEventListener('click', async () => {
208
+ const uuid = (container as HTMLElement).dataset.uuid
209
+ if (uuid) showPreviewModal(uuid)
210
+ })
211
+ })
212
+
213
+ // Load symbol and footprint previews for each card
214
+ results.forEach(result => loadComponentPreviews(result.uuid))
215
+ }
216
+
217
+ // Load and render both symbol and footprint previews for a card
218
+ async function loadComponentPreviews(uuid: string) {
219
+ const symbolContainer = document.querySelector(`.symbol-container[data-uuid="${uuid}"]`)
220
+ const footprintContainer = document.querySelector(`.footprint-container[data-uuid="${uuid}"]`)
221
+ if (!symbolContainer && !footprintContainer) return
222
+
223
+ try {
224
+ const response = await fetch(`/api/component/${uuid}`)
225
+ if (!response.ok) {
226
+ if (symbolContainer) {
227
+ symbolContainer.innerHTML = '<div class="no-preview">Error</div><div class="image-label">Symbol</div>'
228
+ }
229
+ if (footprintContainer) {
230
+ footprintContainer.innerHTML = '<div class="no-preview">Error</div><div class="image-label">Footprint</div>'
231
+ }
232
+ return
233
+ }
234
+
235
+ const data: ComponentData = await response.json()
236
+
237
+ // Render symbol from KiCad S-expression
238
+ if (symbolContainer) {
239
+ if (data.symbolSexpr) {
240
+ const svg = renderSymbolSvg(data.symbolSexpr)
241
+ symbolContainer.innerHTML = svg + '<div class="image-label">Symbol</div>'
242
+ } else {
243
+ symbolContainer.innerHTML = '<div class="no-preview">No preview</div><div class="image-label">Symbol</div>'
244
+ }
245
+ }
246
+
247
+ // Render footprint from KiCad S-expression
248
+ if (footprintContainer) {
249
+ if (data.footprintSexpr) {
250
+ const svg = renderFootprintSvg(data.footprintSexpr)
251
+ footprintContainer.innerHTML = svg + '<div class="image-label">Footprint</div>'
252
+ } else {
253
+ footprintContainer.innerHTML = '<div class="no-preview">No preview</div><div class="image-label">Footprint</div>'
254
+ }
255
+ }
256
+ } catch {
257
+ if (symbolContainer) {
258
+ symbolContainer.innerHTML = '<div class="no-preview">Error</div><div class="image-label">Symbol</div>'
259
+ }
260
+ if (footprintContainer) {
261
+ footprintContainer.innerHTML = '<div class="no-preview">Error</div><div class="image-label">Footprint</div>'
262
+ }
263
+ }
264
+ }
265
+
266
+ // Show preview modal
267
+ async function showPreviewModal(uuid: string) {
268
+ modal.classList.remove('hidden')
269
+
270
+ modalContent.innerHTML = '<div class="modal-loading">Loading...</div>'
271
+
272
+ try {
273
+ const response = await fetch(`/api/component/${uuid}`)
274
+ if (!response.ok) throw new Error('Failed to fetch component')
275
+
276
+ const data: ComponentData = await response.json()
277
+
278
+ // Generate SVG previews from KiCad S-expressions
279
+ const symbolSvg = data.symbolSexpr ? renderSymbolSvg(data.symbolSexpr) : ''
280
+ const footprintSvg = data.footprintSexpr ? renderFootprintSvg(data.footprintSexpr) : ''
281
+
282
+ modalContent.innerHTML = `
283
+ <div class="modal-header">
284
+ <h2>${escapeHtml(data.title)}</h2>
285
+ <div class="modal-uuid">
286
+ <code>${data.uuid}</code>
287
+ <button class="copy-btn modal-copy" data-uuid="${data.uuid}">Copy UUID</button>
288
+ </div>
289
+ </div>
290
+ <div class="modal-previews">
291
+ <div class="modal-preview">
292
+ <h3>Symbol (KiCad)</h3>
293
+ <div class="preview-svg">${symbolSvg || '<div class="no-preview">No symbol preview</div>'}</div>
294
+ </div>
295
+ <div class="modal-preview">
296
+ <h3>Footprint (KiCad)</h3>
297
+ <div class="preview-svg">${footprintSvg || '<div class="no-preview">No footprint preview</div>'}</div>
298
+ </div>
299
+ </div>
300
+ <div class="modal-info">
301
+ <p><strong>Description:</strong> ${escapeHtml(data.description || 'N/A')}</p>
302
+ ${data.model3d ? '<p><strong>3D Model:</strong> Available</p>' : ''}
303
+ </div>
304
+ `
305
+
306
+ // Add copy handler for modal
307
+ const modalCopyBtn = modalContent.querySelector('.modal-copy')
308
+ if (modalCopyBtn) {
309
+ modalCopyBtn.addEventListener('click', () => {
310
+ copyToClipboard(data.uuid, modalCopyBtn as HTMLElement)
311
+ })
312
+ }
313
+ } catch (error) {
314
+ console.error('Modal error:', error)
315
+ modalContent.innerHTML = '<div class="modal-error">Failed to load component details</div>'
316
+ }
317
+ }
318
+
319
+ // Close modal
320
+ function closeModal() {
321
+ modal.classList.add('hidden')
322
+ }
323
+
324
+ // Render pagination
325
+ function renderPagination(pagination: Pagination) {
326
+ if (pagination.totalPages <= 1) {
327
+ paginationDiv.innerHTML = ''
328
+ return
329
+ }
330
+
331
+ paginationDiv.innerHTML = `
332
+ <button class="page-btn" ${pagination.hasPrev ? '' : 'disabled'} data-page="${pagination.page - 1}">
333
+ ← Prev
334
+ </button>
335
+ <span class="page-info">Page ${pagination.page} of ${pagination.totalPages}</span>
336
+ <button class="page-btn" ${pagination.hasNext ? '' : 'disabled'} data-page="${pagination.page + 1}">
337
+ Next →
338
+ </button>
339
+ `
340
+
341
+ paginationDiv.querySelectorAll('.page-btn').forEach(btn => {
342
+ btn.addEventListener('click', () => {
343
+ const page = parseInt((btn as HTMLElement).dataset.page || '1', 10)
344
+ if (page > 0) {
345
+ currentPage = page
346
+ performSearch()
347
+ window.scrollTo(0, 0)
348
+ }
349
+ })
350
+ })
351
+ }
352
+
353
+ // Copy to clipboard
354
+ async function copyToClipboard(text: string, btn: HTMLElement) {
355
+ try {
356
+ await navigator.clipboard.writeText(text)
357
+ btn.classList.add('copied')
358
+ setTimeout(() => btn.classList.remove('copied'), 1500)
359
+ } catch {
360
+ // Fallback for older browsers
361
+ const input = document.createElement('input')
362
+ input.value = text
363
+ document.body.appendChild(input)
364
+ input.select()
365
+ document.execCommand('copy')
366
+ document.body.removeChild(input)
367
+ btn.classList.add('copied')
368
+ setTimeout(() => btn.classList.remove('copied'), 1500)
369
+ }
370
+ }
371
+
372
+ // Show/hide loading
373
+ function showLoading(show: boolean) {
374
+ loadingDiv.classList.toggle('hidden', !show)
375
+ }
376
+
377
+ // Escape HTML
378
+ function escapeHtml(str: string): string {
379
+ const div = document.createElement('div')
380
+ div.textContent = str
381
+ return div.innerHTML
382
+ }
383
+
384
+ // Initialize on DOM ready
385
+ if (document.readyState === 'loading') {
386
+ document.addEventListener('DOMContentLoaded', init)
387
+ } else {
388
+ init()
389
+ }