@jlcpcb/mcp 0.1.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.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@jlcpcb/mcp",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "MCP server for JLC/EasyEDA component sourcing, library fetching, and conversion to KiCad format",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/l3wi/jlc-cli.git",
10
+ "directory": "packages/jlc-mcp"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "bin": {
16
+ "jlc-mcp": "./dist/index.js"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "scripts": {
21
+ "build": "bun build ./src/index.ts --outdir ./dist --target node && bun run build:search",
22
+ "build:search": "bun run scripts/build-search-page.ts",
23
+ "start": "bun run ./src/index.ts",
24
+ "dev": "bun --watch ./src/index.ts",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "bun test",
27
+ "clean": "rm -rf dist"
28
+ },
29
+ "keywords": [
30
+ "mcp",
31
+ "jlcpcb",
32
+ "jlc",
33
+ "easyeda",
34
+ "kicad",
35
+ "electronics",
36
+ "components",
37
+ "pcb"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "@jlcpcb/core": "workspace:*",
44
+ "zod": "^3.22.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0"
48
+ }
49
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build script for the EasyEDA Component Browser
4
+ * Bundles browser TypeScript and inlines it into the HTML template
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
8
+ import { join, dirname } from 'path'
9
+
10
+ const ROOT = dirname(dirname(import.meta.path))
11
+ const SRC_DIR = join(ROOT, 'src')
12
+ const DIST_DIR = join(ROOT, 'dist')
13
+ const ASSETS_DIR = join(DIST_DIR, 'assets')
14
+
15
+ async function buildSearchPage() {
16
+ console.log('Building search page...')
17
+
18
+ // Ensure dist/assets directory exists
19
+ if (!existsSync(ASSETS_DIR)) {
20
+ mkdirSync(ASSETS_DIR, { recursive: true })
21
+ }
22
+
23
+ // Bundle browser TypeScript
24
+ console.log('Bundling browser code...')
25
+ const result = await Bun.build({
26
+ entrypoints: [join(SRC_DIR, 'browser/index.ts')],
27
+ target: 'browser',
28
+ minify: true,
29
+ sourcemap: 'none',
30
+ })
31
+
32
+ if (!result.success) {
33
+ console.error('Build failed:')
34
+ for (const log of result.logs) {
35
+ console.error(log)
36
+ }
37
+ process.exit(1)
38
+ }
39
+
40
+ // Get bundled JavaScript
41
+ const bundledJs = await result.outputs[0].text()
42
+ console.log(`Bundled JS size: ${(bundledJs.length / 1024).toFixed(1)} KB`)
43
+
44
+ // Read HTML template
45
+ const templatePath = join(SRC_DIR, 'assets/search.html')
46
+ const template = readFileSync(templatePath, 'utf-8')
47
+
48
+ // Replace placeholder with bundled JS
49
+ // Use function replacer to avoid $& being interpreted as special replacement pattern
50
+ const html = template.replace('/* {{INLINE_JS}} */', () => bundledJs)
51
+
52
+ // Write output
53
+ const outputPath = join(ASSETS_DIR, 'search.html')
54
+ writeFileSync(outputPath, html)
55
+ console.log(`Written: ${outputPath}`)
56
+
57
+ // Also copy to src/assets for development (routes.ts looks there too)
58
+ const devOutputPath = join(SRC_DIR, 'assets/search-built.html')
59
+ writeFileSync(devOutputPath, html)
60
+ console.log(`Written: ${devOutputPath}`)
61
+
62
+ console.log('Search page build complete!')
63
+ }
64
+
65
+ buildSearchPage().catch((error) => {
66
+ console.error('Build error:', error)
67
+ process.exit(1)
68
+ })
@@ -0,0 +1,528 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EasyEDA Component Browser</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ :root {
15
+ --bg-primary: #1a1a1a;
16
+ --bg-secondary: #2a2a2a;
17
+ --bg-card: #333333;
18
+ --text-primary: #ffffff;
19
+ --text-secondary: #aaaaaa;
20
+ --accent: #4a9eff;
21
+ --accent-hover: #6ab0ff;
22
+ --success: #22c55e;
23
+ --error: #ef4444;
24
+ --border: #444444;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ min-height: 100vh;
32
+ }
33
+
34
+ header {
35
+ background: var(--bg-secondary);
36
+ padding: 20px;
37
+ border-bottom: 1px solid var(--border);
38
+ position: sticky;
39
+ top: 0;
40
+ z-index: 100;
41
+ }
42
+
43
+ .header-content {
44
+ max-width: 1400px;
45
+ margin: 0 auto;
46
+ }
47
+
48
+ h1 {
49
+ font-size: 1.5rem;
50
+ margin-bottom: 16px;
51
+ color: var(--text-primary);
52
+ }
53
+
54
+ .search-bar {
55
+ display: flex;
56
+ gap: 12px;
57
+ flex-wrap: wrap;
58
+ }
59
+
60
+ #search-input {
61
+ flex: 1;
62
+ min-width: 200px;
63
+ padding: 12px 16px;
64
+ font-size: 16px;
65
+ background: var(--bg-primary);
66
+ border: 1px solid var(--border);
67
+ border-radius: 8px;
68
+ color: var(--text-primary);
69
+ outline: none;
70
+ }
71
+
72
+ #search-input:focus {
73
+ border-color: var(--accent);
74
+ }
75
+
76
+ #source-select {
77
+ padding: 12px 16px;
78
+ font-size: 16px;
79
+ background: var(--bg-primary);
80
+ border: 1px solid var(--border);
81
+ border-radius: 8px;
82
+ color: var(--text-primary);
83
+ cursor: pointer;
84
+ }
85
+
86
+ #search-btn {
87
+ padding: 12px 24px;
88
+ font-size: 16px;
89
+ background: var(--accent);
90
+ border: none;
91
+ border-radius: 8px;
92
+ color: white;
93
+ cursor: pointer;
94
+ font-weight: 500;
95
+ transition: background 0.2s;
96
+ }
97
+
98
+ #search-btn:hover {
99
+ background: var(--accent-hover);
100
+ }
101
+
102
+ main {
103
+ max-width: 1400px;
104
+ margin: 0 auto;
105
+ padding: 24px;
106
+ }
107
+
108
+ #loading {
109
+ display: flex;
110
+ justify-content: center;
111
+ padding: 48px;
112
+ }
113
+
114
+ #loading.hidden {
115
+ display: none;
116
+ }
117
+
118
+ .spinner {
119
+ width: 40px;
120
+ height: 40px;
121
+ border: 3px solid var(--border);
122
+ border-top-color: var(--accent);
123
+ border-radius: 50%;
124
+ animation: spin 1s linear infinite;
125
+ }
126
+
127
+ @keyframes spin {
128
+ to { transform: rotate(360deg); }
129
+ }
130
+
131
+ #results-grid {
132
+ display: grid;
133
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
134
+ gap: 20px;
135
+ }
136
+
137
+ .card {
138
+ background: var(--bg-card);
139
+ border-radius: 12px;
140
+ overflow: hidden;
141
+ border: 1px solid var(--border);
142
+ transition: transform 0.2s, border-color 0.2s;
143
+ }
144
+
145
+ .card:hover {
146
+ transform: translateY(-2px);
147
+ border-color: var(--accent);
148
+ }
149
+
150
+ .card-images {
151
+ display: flex;
152
+ gap: 2px;
153
+ background: var(--border);
154
+ }
155
+
156
+ .image-container {
157
+ flex: 1;
158
+ aspect-ratio: 1;
159
+ background: #000;
160
+ display: flex;
161
+ flex-direction: column;
162
+ align-items: center;
163
+ justify-content: center;
164
+ position: relative;
165
+ cursor: pointer;
166
+ overflow: hidden;
167
+ }
168
+
169
+ .image-container img,
170
+ .image-container svg {
171
+ max-width: 100%;
172
+ max-height: 100%;
173
+ object-fit: contain;
174
+ }
175
+
176
+ .image-label {
177
+ position: absolute;
178
+ bottom: 4px;
179
+ left: 4px;
180
+ font-size: 10px;
181
+ color: var(--text-secondary);
182
+ background: rgba(0,0,0,0.7);
183
+ padding: 2px 6px;
184
+ border-radius: 4px;
185
+ }
186
+
187
+ .symbol-placeholder,
188
+ .footprint-placeholder,
189
+ .no-preview {
190
+ color: var(--text-secondary);
191
+ font-size: 12px;
192
+ }
193
+
194
+ .card-info {
195
+ padding: 12px;
196
+ }
197
+
198
+ .card-title {
199
+ font-weight: 600;
200
+ margin-bottom: 8px;
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ }
205
+
206
+ .card-package,
207
+ .card-owner {
208
+ font-size: 13px;
209
+ color: var(--text-secondary);
210
+ margin-bottom: 4px;
211
+ }
212
+
213
+ .card-uuid {
214
+ display: flex;
215
+ padding: 8px 12px;
216
+ background: var(--bg-secondary);
217
+ gap: 8px;
218
+ }
219
+
220
+ .card-uuid input {
221
+ flex: 1;
222
+ background: var(--bg-primary);
223
+ border: 1px solid var(--border);
224
+ border-radius: 4px;
225
+ padding: 6px 10px;
226
+ font-size: 11px;
227
+ font-family: monospace;
228
+ color: var(--text-secondary);
229
+ }
230
+
231
+ .copy-btn {
232
+ background: var(--accent);
233
+ border: none;
234
+ border-radius: 4px;
235
+ padding: 6px 10px;
236
+ cursor: pointer;
237
+ color: white;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ transition: background 0.2s;
242
+ }
243
+
244
+ .copy-btn:hover {
245
+ background: var(--accent-hover);
246
+ }
247
+
248
+ .copy-btn.copied {
249
+ background: var(--success);
250
+ }
251
+
252
+ #pagination {
253
+ display: flex;
254
+ justify-content: center;
255
+ align-items: center;
256
+ gap: 16px;
257
+ margin-top: 32px;
258
+ }
259
+
260
+ .page-btn {
261
+ padding: 10px 20px;
262
+ background: var(--bg-card);
263
+ border: 1px solid var(--border);
264
+ border-radius: 8px;
265
+ color: var(--text-primary);
266
+ cursor: pointer;
267
+ transition: background 0.2s, border-color 0.2s;
268
+ }
269
+
270
+ .page-btn:hover:not(:disabled) {
271
+ background: var(--bg-secondary);
272
+ border-color: var(--accent);
273
+ }
274
+
275
+ .page-btn:disabled {
276
+ opacity: 0.5;
277
+ cursor: not-allowed;
278
+ }
279
+
280
+ .page-info {
281
+ color: var(--text-secondary);
282
+ }
283
+
284
+ .no-results,
285
+ .error {
286
+ grid-column: 1 / -1;
287
+ text-align: center;
288
+ padding: 48px;
289
+ color: var(--text-secondary);
290
+ }
291
+
292
+ .error {
293
+ color: var(--error);
294
+ }
295
+
296
+ /* Modal */
297
+ #preview-modal {
298
+ position: fixed;
299
+ inset: 0;
300
+ background: rgba(0, 0, 0, 0.8);
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ z-index: 1000;
305
+ padding: 20px;
306
+ }
307
+
308
+ #preview-modal.hidden {
309
+ display: none;
310
+ }
311
+
312
+ .modal-wrapper {
313
+ background: var(--bg-secondary);
314
+ border-radius: 16px;
315
+ max-width: 900px;
316
+ width: 100%;
317
+ max-height: 90vh;
318
+ overflow: auto;
319
+ position: relative;
320
+ }
321
+
322
+ #modal-close {
323
+ position: absolute;
324
+ top: 16px;
325
+ right: 16px;
326
+ background: var(--bg-card);
327
+ border: 1px solid var(--border);
328
+ border-radius: 8px;
329
+ padding: 8px 12px;
330
+ color: var(--text-primary);
331
+ cursor: pointer;
332
+ font-size: 18px;
333
+ z-index: 10;
334
+ }
335
+
336
+ #modal-close:hover {
337
+ background: var(--bg-primary);
338
+ }
339
+
340
+ #modal-content {
341
+ padding: 24px;
342
+ }
343
+
344
+ .modal-loading {
345
+ text-align: center;
346
+ padding: 48px;
347
+ color: var(--text-secondary);
348
+ }
349
+
350
+ .modal-error {
351
+ text-align: center;
352
+ padding: 48px;
353
+ color: var(--error);
354
+ }
355
+
356
+ .modal-header {
357
+ margin-bottom: 24px;
358
+ }
359
+
360
+ .modal-header h2 {
361
+ margin-bottom: 12px;
362
+ }
363
+
364
+ .modal-uuid {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 12px;
368
+ }
369
+
370
+ .modal-uuid code {
371
+ background: var(--bg-primary);
372
+ padding: 8px 12px;
373
+ border-radius: 6px;
374
+ font-size: 14px;
375
+ color: var(--text-secondary);
376
+ }
377
+
378
+ .modal-copy {
379
+ padding: 8px 16px;
380
+ font-size: 14px;
381
+ }
382
+
383
+ .modal-previews {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
386
+ gap: 24px;
387
+ margin-bottom: 24px;
388
+ }
389
+
390
+ .modal-preview h3 {
391
+ margin-bottom: 12px;
392
+ font-size: 14px;
393
+ color: var(--text-secondary);
394
+ }
395
+
396
+ .preview-svg {
397
+ background: #000;
398
+ border-radius: 8px;
399
+ aspect-ratio: 1;
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
403
+ overflow: hidden;
404
+ }
405
+
406
+ .preview-svg svg {
407
+ width: 100%;
408
+ height: 100%;
409
+ }
410
+
411
+ .preview-svg .no-preview {
412
+ color: var(--text-secondary);
413
+ }
414
+
415
+ .modal-info {
416
+ display: flex;
417
+ gap: 24px;
418
+ color: var(--text-secondary);
419
+ font-size: 14px;
420
+ }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <header>
425
+ <div class="header-content">
426
+ <h1>EasyEDA Component Browser</h1>
427
+ <div class="search-bar">
428
+ <input type="text" id="search-input" placeholder="Search components (e.g., ESP32, STM32, LM7805)..." autofocus />
429
+ <select id="source-select">
430
+ <option value="user">Community</option>
431
+ <option value="lcsc">LCSC</option>
432
+ <option value="all">All Sources</option>
433
+ </select>
434
+ <button id="search-btn">Search</button>
435
+ </div>
436
+ </div>
437
+ </header>
438
+
439
+ <main>
440
+ <div id="loading" class="hidden">
441
+ <div class="spinner"></div>
442
+ </div>
443
+ <div id="results-grid"></div>
444
+ <div id="pagination"></div>
445
+ </main>
446
+
447
+ <div id="preview-modal" class="hidden">
448
+ <div class="modal-wrapper">
449
+ <button id="modal-close">&times;</button>
450
+ <div id="modal-content"></div>
451
+ </div>
452
+ </div>
453
+
454
+ <script>
455
+ function DJ(Z){let $=[],J=0;while(J<Z.length){let K=Z[J];if(/\s/.test(K)){J++;continue}if(K==="("||K===")"){$.push(K),J++;continue}if(K==='"'){let j="";J++;while(J<Z.length)if(Z[J]==="\\"&&J+1<Z.length)j+=Z[J+1],J+=2;else if(Z[J]==='"'){J++;break}else j+=Z[J],J++;$.push(`"${j}"`);continue}let U="";while(J<Z.length&&!/[\s()]/.test(Z[J]))U+=Z[J],J++;if(U)$.push(U)}return $}function ZJ(Z){if(Z.length===0)return{expr:[],remaining:[]};let $=Z[0];if($==="("){let K=[],U=Z.slice(1);while(U.length>0&&U[0]!==")"){let{expr:j,remaining:V}=ZJ(U);K.push(j),U=V}if(U[0]===")")U=U.slice(1);return{expr:K,remaining:U}}if($===")")return{expr:[],remaining:Z.slice(1)};let J=$;if(J.startsWith('"')&&J.endsWith('"'))J=J.slice(1,-1);return{expr:J,remaining:Z.slice(1)}}function p(Z){let $=DJ(Z),{expr:J}=ZJ($);return J}function k(Z){return Array.isArray(Z)}function E(Z){return typeof Z==="string"}function R(Z){if(k(Z)&&Z.length>0&&E(Z[0]))return Z[0];return}function N(Z,$){if(!k(Z))return;for(let J of Z)if(k(J)&&R(J)===$)return J;return}function _(Z,$){if(!k(Z))return[];let J=[];for(let K of Z)if(k(K)&&R(K)===$)J.push(K);return J}function y(Z,$){let J=N(Z,$);if(J&&J.length>=2&&E(J[1]))return J[1];return}function S(Z,$){let J=y(Z,$);if(J!==void 0){let K=parseFloat(J);if(!isNaN(K))return K}return}function F(Z,$){let J=N(Z,$);if(J&&J.length>=3){let K=parseFloat(E(J[1])?J[1]:""),U=parseFloat(E(J[2])?J[2]:"");if(!isNaN(K)&&!isNaN(U))return{x:K,y:U}}return}function g(Z,$){let J=N(Z,$);if(J&&J.length>=3){let K=parseFloat(E(J[1])?J[1]:""),U=parseFloat(E(J[2])?J[2]:"");if(!isNaN(K)&&!isNaN(U)){let j={x:K,y:U};if(J.length>=4&&E(J[3])){let V=parseFloat(J[3]);if(!isNaN(V))j.rotation=V}return j}}return}function l(Z){let $=N(Z,"size");if($&&$.length>=3){let J=parseFloat(E($[1])?$[1]:""),K=parseFloat(E($[2])?$[2]:"");if(!isNaN(J)&&!isNaN(K))return{width:J,height:K}}return}function A(Z){let $=N(Z,"stroke");if(!$)return;let J=S($,"width")??0.254,K=y($,"type")??"default";return{width:J,type:K}}function m(Z){let $=N(Z,"fill");if(!$)return;return y($,"type")}function o(Z){let $=N(Z,"pts");if(!$)return[];let J=[];for(let K of $)if(k(K)&&R(K)==="xy"&&K.length>=3){let U=parseFloat(E(K[1])?K[1]:""),j=parseFloat(E(K[2])?K[2]:"");if(!isNaN(U)&&!isNaN(j))J.push({x:U,y:j})}return J}function T(Z){let $=y(Z,"layer");if($)return[$];let J=N(Z,"layers");if(!J)return[];let K=[];for(let U=1;U<J.length;U++)if(E(J[U]))K.push(J[U]);return K}var z={background:"#FFFFF8",body:"#840000",bodyFill:"#FFFFC4",pin:"#840000",pinText:"#008484",text:"#000000"},Y={background:"#000000",fCu:"#CC0000",bCu:"#0066CC",fSilkS:"#00FFFF",bSilkS:"#FF00FF",fFab:"#C4A000",fMask:"#660066",drill:"#666666",courtyard:"#444444",edgeCuts:"#C4C400"},v=10;function C(Z){return Z.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function d(Z){if(!Z||Z.trim()==="")return f("No symbol data");try{let $=p(Z);if(!k($)||R($)!=="symbol")return f("Invalid symbol format");let J=[],K={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0},U=_($,"symbol");for(let D of U){for(let q of _(D,"rectangle")){let H=QJ(q,K);if(H)J.push(H)}for(let q of _(D,"polyline")){let H=GJ(q,K);if(H)J.push(H)}for(let q of _(D,"circle")){let H=HJ(q,K);if(H)J.push(H)}for(let q of _(D,"arc")){let H=qJ(q,K);if(H)J.push(H)}for(let q of _(D,"pin")){let H=EJ(q,K);if(H)J.push(H)}}let j=5;if(!isFinite(K.minX))K.minX=-20,K.maxX=20,K.minY=-20,K.maxY=20;let V=(K.maxX-K.minX+j*2)*v,W=(K.maxY-K.minY+j*2)*v;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${(K.minX-j)*v} ${(-K.maxY-j)*v} ${V} ${W}`}" width="100%" height="100%" style="background-color: ${z.background}">
456
+ <g transform="scale(${v}, ${-v})">
457
+ ${J.join(`
458
+ `)}
459
+ </g>
460
+ </svg>`}catch($){return f(`Parse error: ${$}`)}}function QJ(Z,$){let J=F(Z,"start"),K=F(Z,"end");if(!J||!K)return"";let U=A(Z),V=m(Z)==="background"?z.bodyFill:"none";X($,J.x,J.y),X($,K.x,K.y);let W=Math.min(J.x,K.x),Q=Math.min(J.y,K.y),D=Math.abs(K.x-J.x),q=Math.abs(K.y-J.y);return`<rect x="${W}" y="${Q}" width="${D}" height="${q}" fill="${V}" stroke="${z.body}" stroke-width="${U?.width??0.254}"/>`}function GJ(Z,$){let J=o(Z);if(J.length<2)return"";let K=A(Z),j=m(Z)==="background"?z.bodyFill:"none";return`<polyline points="${J.map((W)=>{return X($,W.x,W.y),`${W.x},${W.y}`}).join(" ")}" fill="${j}" stroke="${z.body}" stroke-width="${K?.width??0.254}" stroke-linecap="round" stroke-linejoin="round"/>`}function HJ(Z,$){let J=F(Z,"center"),K=S(Z,"radius");if(!J||K===void 0)return"";let U=A(Z),V=m(Z)==="background"?z.bodyFill:"none";return X($,J.x-K,J.y-K),X($,J.x+K,J.y+K),`<circle cx="${J.x}" cy="${J.y}" r="${K}" fill="${V}" stroke="${z.body}" stroke-width="${U?.width??0.254}"/>`}function qJ(Z,$){let J=F(Z,"start"),K=F(Z,"mid"),U=F(Z,"end");if(!J||!K||!U)return"";let j=A(Z);return X($,J.x,J.y),X($,K.x,K.y),X($,U.x,U.y),`<path d="${$J(J,K,U)}" fill="none" stroke="${z.body}" stroke-width="${j?.width??0.254}" stroke-linecap="round"/>`}function $J(Z,$,J){let{x:K,y:U}=Z,j=$.x,V=$.y,W=J.x,Q=J.y,D=2*(K*(V-Q)+j*(Q-U)+W*(U-V));if(Math.abs(D)<0.0001)return`M ${Z.x} ${Z.y} L ${J.x} ${J.y}`;let q=((K*K+U*U)*(V-Q)+(j*j+V*V)*(Q-U)+(W*W+Q*Q)*(U-V))/D,H=((K*K+U*U)*(W-j)+(j*j+V*V)*(K-W)+(W*W+Q*Q)*(j-K))/D,G=Math.sqrt((K-q)**2+(U-H)**2),w=(j-K)*(Q-U)-(V-U)*(W-K)>0?0:1,M=0;return`M ${Z.x} ${Z.y} A ${G} ${G} 0 ${M} ${w} ${J.x} ${J.y}`}function EJ(Z,$){let J=g(Z,"at"),K=S(Z,"length")??2.54;if(!J)return"";let U=N(Z,"name"),j=N(Z,"number"),V=U&&U.length>=2&&E(U[1])?U[1]:"",W=j&&j.length>=2&&E(j[1])?j[1]:"",Q=J.rotation??0,D=Q*Math.PI/180,q=J.x+K*Math.cos(D),H=J.y+K*Math.sin(D);X($,J.x,J.y),X($,q,H);let G=[];if(G.push(`<line x1="${J.x}" y1="${J.y}" x2="${q}" y2="${H}" stroke="${z.pin}" stroke-width="0.254"/>`),G.push(`<circle cx="${J.x}" cy="${J.y}" r="0.3" fill="${z.pin}"/>`),V&&V!=="~"){let w=q,M=H,P="start",B="middle";if(Q===0)w=q+0.5,P="start";else if(Q===180)w=q-0.5,P="end";else if(Q===90)M=H+0.5,B="hanging",P="middle";else if(Q===270)M=H-0.5,B="alphabetic",P="middle";G.push(`<text x="${w}" y="${M}" fill="${z.pinText}" font-size="1" font-family="sans-serif" text-anchor="${P}" dominant-baseline="${B}" transform="scale(1,-1) translate(0,${-2*M})">${C(V)}</text>`)}if(W){let I=(J.x+q)/2,w=(J.y+H)/2,M=Q===0||Q===180?0.8:0;G.push(`<text x="${I}" y="${w+M}" fill="${z.pinText}" font-size="0.8" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle" transform="scale(1,-1) translate(0,${-2*(w+M)})">${C(W)}</text>`)}return G.join(`
461
+ `)}function s(Z){if(!Z||Z.trim()==="")return f("No footprint data");try{let $=p(Z);if(!k($)||R($)!=="footprint")return f("Invalid footprint format");let J=[],K={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0};for(let Q of _($,"pad")){let D=XJ(Q,K);if(D)J.push(D)}for(let Q of _($,"fp_line")){let D=wJ(Q,K);if(D)J.push(D)}for(let Q of _($,"fp_circle")){let D=IJ(Q,K);if(D)J.push(D)}for(let Q of _($,"fp_arc")){let D=MJ(Q,K);if(D)J.push(D)}for(let Q of _($,"fp_text")){let D=NJ(Q,K);if(D)J.push(D)}let U=1;if(!isFinite(K.minX))K.minX=-5,K.maxX=5,K.minY=-5,K.maxY=5;let j=K.maxX-K.minX+U*2,V=K.maxY-K.minY+U*2;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${K.minX-U} ${K.minY-U} ${j} ${V}`}" width="100%" height="100%" style="background-color: ${Y.background}">
462
+ <g>
463
+ ${J.join(`
464
+ `)}
465
+ </g>
466
+ </svg>`}catch($){return f(`Parse error: ${$}`)}}function b(Z){if(Z.includes("F.Cu"))return Y.fCu;if(Z.includes("B.Cu"))return Y.bCu;if(Z.includes("F.SilkS"))return Y.fSilkS;if(Z.includes("B.SilkS"))return Y.bSilkS;if(Z.includes("F.Fab"))return Y.fFab;if(Z.includes("F.CrtYd"))return Y.courtyard;if(Z.includes("Edge.Cuts"))return Y.edgeCuts;return Y.fSilkS}function XJ(Z,$){if(!k(Z)||Z.length<4)return"";let J=E(Z[1])?Z[1]:"",K=E(Z[2])?Z[2]:"",U=E(Z[3])?Z[3]:"",j=g(Z,"at"),V=l(Z);if(!j||!V)return"";X($,j.x-V.width/2,j.y-V.height/2),X($,j.x+V.width/2,j.y+V.height/2);let W=[],D=T(Z).some((G)=>G.includes("B."))?Y.bCu:Y.fCu,q=j.rotation??0,H=q!==0?` transform="rotate(${q}, ${j.x}, ${j.y})"`:"";if(U==="circle"){let G=Math.min(V.width,V.height)/2;W.push(`<circle cx="${j.x}" cy="${j.y}" r="${G}" fill="${D}"${H}/>`)}else if(U==="oval"){let G=V.width/2,I=V.height/2;W.push(`<ellipse cx="${j.x}" cy="${j.y}" rx="${G}" ry="${I}" fill="${D}"${H}/>`)}else if(U==="roundrect"){let G=S(Z,"roundrect_rratio")??0.25,I=Math.min(V.width,V.height)*G/2,w=j.x-V.width/2,M=j.y-V.height/2;W.push(`<rect x="${w}" y="${M}" width="${V.width}" height="${V.height}" rx="${I}" fill="${D}"${H}/>`)}else if(U==="custom"){let G=N(Z,"primitives");if(G)for(let I of _(G,"gr_poly")){let w=o(I);if(w.length>=3){let M=w.map((P)=>{let B=j.x+P.x,JJ=j.y+P.y;return X($,B,JJ),`${B},${JJ}`}).join(" ");W.push(`<polygon points="${M}" fill="${D}"${H}/>`)}}if(W.length===0){let I=j.x-V.width/2,w=j.y-V.height/2;W.push(`<rect x="${I}" y="${w}" width="${V.width}" height="${V.height}" fill="${D}"${H}/>`)}}else{let G=j.x-V.width/2,I=j.y-V.height/2;W.push(`<rect x="${G}" y="${I}" width="${V.width}" height="${V.height}" fill="${D}"${H}/>`)}if(K==="thru_hole"||K==="np_thru_hole"){let G=N(Z,"drill");if(G&&G.length>=2)if(E(G[1])&&G[1]==="oval"&&G.length>=4){let w=parseFloat(E(G[2])?G[2]:"0"),M=parseFloat(E(G[3])?G[3]:"0");W.push(`<ellipse cx="${j.x}" cy="${j.y}" rx="${w/2}" ry="${M/2}" fill="${Y.drill}"${H}/>`)}else{let w=parseFloat(E(G[1])?G[1]:"0");W.push(`<circle cx="${j.x}" cy="${j.y}" r="${w/2}" fill="${Y.drill}"/>`)}}if(J){let G=Math.min(V.width,V.height)*0.5;W.push(`<text x="${j.x}" y="${j.y}" fill="#FFFFFF" font-size="${G}" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${C(J)}</text>`)}return W.join(`
467
+ `)}function wJ(Z,$){let J=F(Z,"start"),K=F(Z,"end");if(!J||!K)return"";let U=A(Z),V=T(Z)[0]??"F.SilkS";if(V.includes("CrtYd"))return"";let W=b(V);return X($,J.x,J.y),X($,K.x,K.y),`<line x1="${J.x}" y1="${J.y}" x2="${K.x}" y2="${K.y}" stroke="${W}" stroke-width="${U?.width??0.15}" stroke-linecap="round"/>`}function IJ(Z,$){let J=F(Z,"center"),K=F(Z,"end");if(!J||!K)return"";let U=Math.sqrt((K.x-J.x)**2+(K.y-J.y)**2),j=A(Z),W=T(Z)[0]??"F.SilkS";if(W.includes("CrtYd"))return"";let Q=b(W);return X($,J.x-U,J.y-U),X($,J.x+U,J.y+U),`<circle cx="${J.x}" cy="${J.y}" r="${U}" fill="none" stroke="${Q}" stroke-width="${j?.width??0.15}"/>`}function MJ(Z,$){let J=F(Z,"start"),K=F(Z,"mid"),U=F(Z,"end");if(!J||!K||!U)return"";let j=A(Z),W=T(Z)[0]??"F.SilkS";if(W.includes("CrtYd"))return"";let Q=b(W);return X($,J.x,J.y),X($,K.x,K.y),X($,U.x,U.y),`<path d="${$J(J,K,U)}" fill="none" stroke="${Q}" stroke-width="${j?.width??0.15}" stroke-linecap="round"/>`}function NJ(Z,$){if(!k(Z)||Z.length<3)return"";let J=E(Z[1])?Z[1]:"",K=E(Z[2])?Z[2]:"";if(J==="reference"||J==="value")return"";let U=g(Z,"at");if(!U)return"";let V=T(Z)[0]??"F.SilkS",W=b(V),Q=N(Z,"effects"),D=1;if(Q){let G=N(Q,"font");if(G){let I=l(G);if(I)D=I.height}}X($,U.x-2,U.y-1),X($,U.x+2,U.y+1);let q=U.rotation??0,H=q!==0?` transform="rotate(${q}, ${U.x}, ${U.y})"`:"";return`<text x="${U.x}" y="${U.y}" fill="${W}" font-size="${D}" font-family="sans-serif" text-anchor="middle" dominant-baseline="central"${H}>${C(K)}</text>`}function X(Z,$,J){Z.minX=Math.min(Z.minX,$),Z.maxX=Math.max(Z.maxX,$),Z.minY=Math.min(Z.minY,J),Z.maxY=Math.max(Z.maxY,J)}function f(Z){return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" width="100%" height="100%">
468
+ <rect width="200" height="100" fill="#FFF0F0"/>
469
+ <text x="100" y="50" fill="#CC0000" font-size="12" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${C(Z)}</text>
470
+ </svg>`}var e=1,x="",VJ="user",a=!1,i=null,h=document.getElementById("search-input"),KJ=document.getElementById("source-select"),FJ=document.getElementById("search-btn"),t=document.getElementById("results-grid"),n=document.getElementById("pagination"),_J=document.getElementById("loading"),u=document.getElementById("preview-modal"),c=document.getElementById("modal-content"),YJ=document.getElementById("modal-close");function UJ(){let $=new URLSearchParams(window.location.search).get("q");if($)h.value=$,x=$,L();h.addEventListener("input",zJ),h.addEventListener("keydown",(J)=>{if(J.key==="Enter")J.preventDefault(),L()}),FJ.addEventListener("click",L),KJ.addEventListener("change",()=>{if(VJ=KJ.value,x)L()}),YJ.addEventListener("click",r),u.addEventListener("click",(J)=>{if(J.target===u)r()}),document.addEventListener("keydown",(J)=>{if(J.key==="Escape")r()})}function zJ(){if(i)clearTimeout(i);i=window.setTimeout(()=>{let Z=h.value.trim();if(Z&&Z!==x)x=Z,e=1,L()},300)}async function L(){let Z=h.value.trim();if(!Z||a)return;x=Z,a=!0,jJ(!0);try{let $=new URLSearchParams({q:Z,source:VJ,page:String(e),limit:"20"}),J=await fetch(`/api/search?${$}`);if(!J.ok)throw new Error("Search failed");let K=await J.json();kJ(K.results),BJ(K.pagination)}catch($){console.error("Search error:",$),t.innerHTML='<div class="error">Search failed. Please try again.</div>'}finally{a=!1,jJ(!1)}}function kJ(Z){if(Z.length===0){t.innerHTML='<div class="no-results">No components found. Try a different search term.</div>';return}t.innerHTML=Z.map(($)=>`
471
+ <div class="card" data-uuid="${$.uuid}">
472
+ <div class="card-images">
473
+ <div class="image-container symbol-container" data-uuid="${$.uuid}">
474
+ <div class="symbol-placeholder">Loading...</div>
475
+ <div class="image-label">Symbol</div>
476
+ </div>
477
+ <div class="image-container footprint-container" data-uuid="${$.uuid}">
478
+ <div class="footprint-placeholder">Loading...</div>
479
+ <div class="image-label">Footprint</div>
480
+ </div>
481
+ </div>
482
+ <div class="card-info">
483
+ <div class="card-title" title="${O($.title)}">${O($.title)}</div>
484
+ <div class="card-package">Package: ${O($.package||"Unknown")}</div>
485
+ <div class="card-owner">By: ${O($.owner.nickname||$.owner.username)}</div>
486
+ </div>
487
+ <div class="card-uuid">
488
+ <input type="text" value="${$.uuid}" readonly />
489
+ <button class="copy-btn" data-uuid="${$.uuid}" title="Copy UUID">
490
+ <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>
491
+ </button>
492
+ </div>
493
+ </div>
494
+ `).join(""),document.querySelectorAll(".copy-btn").forEach(($)=>{$.addEventListener("click",(J)=>{J.stopPropagation();let K=$.dataset.uuid;if(K)WJ(K,$)})}),document.querySelectorAll(".image-container").forEach(($)=>{$.addEventListener("click",async()=>{let J=$.dataset.uuid;if(J)AJ(J)})}),Z.forEach(($)=>PJ($.uuid))}async function PJ(Z){let $=document.querySelector(`.symbol-container[data-uuid="${Z}"]`),J=document.querySelector(`.footprint-container[data-uuid="${Z}"]`);if(!$&&!J)return;try{let K=await fetch(`/api/component/${Z}`);if(!K.ok){if($)$.innerHTML='<div class="no-preview">Error</div><div class="image-label">Symbol</div>';if(J)J.innerHTML='<div class="no-preview">Error</div><div class="image-label">Footprint</div>';return}let U=await K.json();if($)if(U.symbolSexpr){let j=d(U.symbolSexpr);$.innerHTML=j+'<div class="image-label">Symbol</div>'}else $.innerHTML='<div class="no-preview">No preview</div><div class="image-label">Symbol</div>';if(J)if(U.footprintSexpr){let j=s(U.footprintSexpr);J.innerHTML=j+'<div class="image-label">Footprint</div>'}else J.innerHTML='<div class="no-preview">No preview</div><div class="image-label">Footprint</div>'}catch{if($)$.innerHTML='<div class="no-preview">Error</div><div class="image-label">Symbol</div>';if(J)J.innerHTML='<div class="no-preview">Error</div><div class="image-label">Footprint</div>'}}async function AJ(Z){u.classList.remove("hidden"),c.innerHTML='<div class="modal-loading">Loading...</div>';try{let $=await fetch(`/api/component/${Z}`);if(!$.ok)throw new Error("Failed to fetch component");let J=await $.json(),K=J.symbolSexpr?d(J.symbolSexpr):"",U=J.footprintSexpr?s(J.footprintSexpr):"";c.innerHTML=`
495
+ <div class="modal-header">
496
+ <h2>${O(J.title)}</h2>
497
+ <div class="modal-uuid">
498
+ <code>${J.uuid}</code>
499
+ <button class="copy-btn modal-copy" data-uuid="${J.uuid}">Copy UUID</button>
500
+ </div>
501
+ </div>
502
+ <div class="modal-previews">
503
+ <div class="modal-preview">
504
+ <h3>Symbol (KiCad)</h3>
505
+ <div class="preview-svg">${K||'<div class="no-preview">No symbol preview</div>'}</div>
506
+ </div>
507
+ <div class="modal-preview">
508
+ <h3>Footprint (KiCad)</h3>
509
+ <div class="preview-svg">${U||'<div class="no-preview">No footprint preview</div>'}</div>
510
+ </div>
511
+ </div>
512
+ <div class="modal-info">
513
+ <p><strong>Description:</strong> ${O(J.description||"N/A")}</p>
514
+ ${J.model3d?"<p><strong>3D Model:</strong> Available</p>":""}
515
+ </div>
516
+ `;let j=c.querySelector(".modal-copy");if(j)j.addEventListener("click",()=>{WJ(J.uuid,j)})}catch($){console.error("Modal error:",$),c.innerHTML='<div class="modal-error">Failed to load component details</div>'}}function r(){u.classList.add("hidden")}function BJ(Z){if(Z.totalPages<=1){n.innerHTML="";return}n.innerHTML=`
517
+ <button class="page-btn" ${Z.hasPrev?"":"disabled"} data-page="${Z.page-1}">
518
+ ← Prev
519
+ </button>
520
+ <span class="page-info">Page ${Z.page} of ${Z.totalPages}</span>
521
+ <button class="page-btn" ${Z.hasNext?"":"disabled"} data-page="${Z.page+1}">
522
+ Next →
523
+ </button>
524
+ `,n.querySelectorAll(".page-btn").forEach(($)=>{$.addEventListener("click",()=>{let J=parseInt($.dataset.page||"1",10);if(J>0)e=J,L(),window.scrollTo(0,0)})})}async function WJ(Z,$){try{await navigator.clipboard.writeText(Z),$.classList.add("copied"),setTimeout(()=>$.classList.remove("copied"),1500)}catch{let J=document.createElement("input");J.value=Z,document.body.appendChild(J),J.select(),document.execCommand("copy"),document.body.removeChild(J),$.classList.add("copied"),setTimeout(()=>$.classList.remove("copied"),1500)}}function jJ(Z){_J.classList.toggle("hidden",!Z)}function O(Z){let $=document.createElement("div");return $.textContent=Z,$.innerHTML}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",UJ);else UJ();
525
+
526
+ </script>
527
+ </body>
528
+ </html>