@jlcpcb/cli 0.1.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # @jlcpcb/cli
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0.3.0
8
+
9
+ Minor Changes
10
+
11
+ CLI:
12
+ - Add component delete functionality with D shortcut in component view
13
+ - Add O shortcut to open datasheet (replaces D)
14
+ - Add library regenerate command to regenerate all installed components
15
+ - Add validation suite for footprint/symbol comparison (jlc validate)
16
+ - Enable 3D models by default during installation
17
+ - Remove non-functional S/F/M shortcuts from component view
18
+
19
+ Core:
20
+ - Add symbol text parsing and rendering for connector components
21
+ - Add arc interpolation for SOLIDREGION polygon paths (improves complex footprint accuracy)
22
+ - Add remove() method to library service for deleting components
23
+
24
+ Patch Changes
25
+
26
+ Core:
27
+ - Strip temperature prefix from component descriptions
28
+ - Improve hole detection and polygon pad handling in footprint converter
29
+
30
+ CLI:
31
+ - Fix async handling in renderApp exit
32
+ - Simplify loading indicators
33
+
34
+ MCP:
35
+ - Fix typecheck errors in batch and library tools
36
+ - Correct validationData property paths
37
+ - Fix InstalledComponent property access
38
+
39
+ Validator:
40
+ - Align SVG reference extraction with KiCad converter
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies []:
45
+ - @jlcpcb/core@0.3.0
46
+
47
+ ## 0.2.0
48
+
49
+ ### Minor Changes
50
+
51
+ - cd6df88: Packages to select: @jlcpcb/cli (minor), @jlcpcb/core (minor)
52
+
53
+ Summary to paste:
54
+
55
+ Add `jlc easyeda install [uuid]` command for EasyEDA community components
56
+
57
+ CLI changes:
58
+ - New `jlc easyeda install [uuid]` subcommand with -p/--project, --with-3d, -f/--force options
59
+ - Dedicated EasyEDAInfoScreen and EasyEDADetailView showing community-specific fields
60
+ - Installation status detection: shows "R Regenerate" if installed, "Enter Install" if not
61
+ - "✓ Installed" indicator next to component title when already in library
62
+ - `jlc install` now rejects non-LCSC IDs with helpful redirect to `jlc easyeda install`
63
+
64
+ Core changes:
65
+ - Add `isEasyEDAInstalled()` method to check if component exists in EasyEDA library
66
+ - Register EasyEDA library in global KiCad tables with portable ${KICAD9_3RD_PARTY} paths
67
+ - Support global installation for community components (no projectPath required)
68
+ - Separate library names (EasyEDA vs EasyEDA-local) to avoid global/local collision
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies [cd6df88]
73
+ - @jlcpcb/core@0.2.0
74
+
3
75
  ## 0.1.1
4
76
 
5
77
  ### Patch Changes
@@ -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 EJ(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 V="";J++;while(J<Z.length)if(Z[J]==="\\"&&J+1<Z.length)V+=Z[J+1],J+=2;else if(Z[J]==='"'){J++;break}else V+=Z[J],J++;$.push(`"${V}"`);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:V,remaining:j}=ZJ(U);K.push(V),U=j}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 d(Z){let $=EJ(Z),{expr:J}=ZJ($);return J}function B(Z){return Array.isArray(Z)}function X(Z){return typeof Z==="string"}function v(Z){if(B(Z)&&Z.length>0&&X(Z[0]))return Z[0];return}function z(Z,$){if(!B(Z))return;for(let J of Z)if(B(J)&&v(J)===$)return J;return}function I(Z,$){if(!B(Z))return[];let J=[];for(let K of Z)if(B(K)&&v(K)===$)J.push(K);return J}function y(Z,$){let J=z(Z,$);if(J&&J.length>=2&&X(J[1]))return J[1];return}function b(Z,$){let J=y(Z,$);if(J!==void 0){let K=parseFloat(J);if(!isNaN(K))return K}return}function _(Z,$){let J=z(Z,$);if(J&&J.length>=3){let K=parseFloat(X(J[1])?J[1]:""),U=parseFloat(X(J[2])?J[2]:"");if(!isNaN(K)&&!isNaN(U))return{x:K,y:U}}return}function c(Z,$){let J=z(Z,$);if(J&&J.length>=3){let K=parseFloat(X(J[1])?J[1]:""),U=parseFloat(X(J[2])?J[2]:"");if(!isNaN(K)&&!isNaN(U)){let V={x:K,y:U};if(J.length>=4&&X(J[3])){let j=parseFloat(J[3]);if(!isNaN(j))V.rotation=j}return V}}return}function o(Z){let $=z(Z,"size");if($&&$.length>=3){let J=parseFloat(X($[1])?$[1]:""),K=parseFloat(X($[2])?$[2]:"");if(!isNaN(J)&&!isNaN(K))return{width:J,height:K}}return}function P(Z){let $=z(Z,"stroke");if(!$)return;let J=b($,"width")??0.254,K=y($,"type")??"default";return{width:J,type:K}}function f(Z){let $=z(Z,"fill");if(!$)return;let J=y($,"type");if(J)return J;if($.length>=2&&X($[1]))return $[1];return}function u(Z){let $=z(Z,"pts");if(!$)return[];let J=[];for(let K of $)if(B(K)&&v(K)==="xy"&&K.length>=3){let U=parseFloat(X(K[1])?K[1]:""),V=parseFloat(X(K[2])?K[2]:"");if(!isNaN(U)&&!isNaN(V))J.push({x:U,y:V})}return J}function R(Z){let $=y(Z,"layer");if($)return[$];let J=z(Z,"layers");if(!J)return[];let K=[];for(let U=1;U<J.length;U++){let V=J[U];if(X(V))K.push(V)}return K}var A={background:"#FFFFF8",body:"#840000",bodyFill:"#FFFFC4",pin:"#840000",pinText:"#008484",text:"#000000"},w={background:"#000000",fCu:"#840000",bCu:"#008400",in1Cu:"#C2C200",in2Cu:"#C200C2",fSilkS:"#008484",bSilkS:"#840084",fMask:"#840084",bMask:"#848400",fPaste:"#840000",bPaste:"#00C2C2",fFab:"#848484",bFab:"#000084",fCrtYd:"#C2C2C2",bCrtYd:"#848484",edgeCuts:"#C2C200",dwgsUser:"#C2C2C2",cmtsUser:"#848484",margin:"#C200C2",padFront:"#840000",padBack:"#008400",padThruHole:"#C2C200",drill:"#848484"},S=10;function g(Z){return Z.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function a(Z){if(!Z||Z.trim()==="")return T("No symbol data");try{let $=d(Z);if(B($)&&v($)==="kicad_symbol_lib"){let D=I($,"symbol");if(D.length===0)return T("No symbol found in library");$=D[0]}if(!B($)||v($)!=="symbol")return T("Invalid symbol format");let J=[],K={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0},U=I($,"symbol");for(let D of U){for(let H of I(D,"rectangle")){let W=XJ(H,K);if(W)J.push(W)}for(let H of I(D,"polyline")){let W=wJ(H,K);if(W)J.push(W)}for(let H of I(D,"circle")){let W=MJ(H,K);if(W)J.push(W)}for(let H of I(D,"arc")){let W=IJ(H,K);if(W)J.push(W)}for(let H of I(D,"pin")){let W=YJ(H,K);if(W)J.push(W)}}let V=5;if(!isFinite(K.minX))K.minX=-20,K.maxX=20,K.minY=-20,K.maxY=20;let j=(K.maxX-K.minX+V*2)*S,Q=(K.maxY-K.minY+V*2)*S;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${(K.minX-V)*S} ${(-K.maxY-V)*S} ${j} ${Q}`}" width="100%" height="100%" style="background-color: ${A.background}">
456
+ <g transform="scale(${S}, ${-S})">
457
+ ${J.join(`
458
+ `)}
459
+ </g>
460
+ </svg>`}catch($){return T(`Parse error: ${$}`)}}function XJ(Z,$){let J=_(Z,"start"),K=_(Z,"end");if(!J||!K)return"";let U=P(Z),j=f(Z)==="background"?A.bodyFill:"none";E($,J.x,J.y),E($,K.x,K.y);let Q=Math.min(J.x,K.x),G=Math.min(J.y,K.y),D=Math.abs(K.x-J.x),H=Math.abs(K.y-J.y);return`<rect x="${Q}" y="${G}" width="${D}" height="${H}" fill="${j}" stroke="${A.body}" stroke-width="${U?.width??0.254}"/>`}function wJ(Z,$){let J=u(Z);if(J.length<2)return"";let K=P(Z),V=f(Z)==="background"?A.bodyFill:"none";return`<polyline points="${J.map((Q)=>{return E($,Q.x,Q.y),`${Q.x},${Q.y}`}).join(" ")}" fill="${V}" stroke="${A.body}" stroke-width="${K?.width??0.254}" stroke-linecap="round" stroke-linejoin="round"/>`}function MJ(Z,$){let J=_(Z,"center"),K=b(Z,"radius");if(!J||K===void 0)return"";let U=P(Z),j=f(Z)==="background"?A.bodyFill:"none";return E($,J.x-K,J.y-K),E($,J.x+K,J.y+K),`<circle cx="${J.x}" cy="${J.y}" r="${K}" fill="${j}" stroke="${A.body}" stroke-width="${U?.width??0.254}"/>`}function IJ(Z,$){let J=_(Z,"start"),K=_(Z,"mid"),U=_(Z,"end");if(!J||!K||!U)return"";let V=P(Z);return E($,J.x,J.y),E($,K.x,K.y),E($,U.x,U.y),`<path d="${jJ(J,K,U)}" fill="none" stroke="${A.body}" stroke-width="${V?.width??0.254}" stroke-linecap="round"/>`}function jJ(Z,$,J){let{x:K,y:U}=Z,V=$.x,j=$.y,Q=J.x,G=J.y,D=2*(K*(j-G)+V*(G-U)+Q*(U-j));if(Math.abs(D)<0.0001)return`M ${Z.x} ${Z.y} L ${J.x} ${J.y}`;let H=((K*K+U*U)*(j-G)+(V*V+j*j)*(G-U)+(Q*Q+G*G)*(U-j))/D,W=((K*K+U*U)*(Q-V)+(V*V+j*j)*(K-Q)+(Q*Q+G*G)*(V-K))/D,q=Math.sqrt((K-H)**2+(U-W)**2),M=(V-K)*(G-U)-(j-U)*(Q-K)>0?0:1,N=0;return`M ${Z.x} ${Z.y} A ${q} ${q} 0 ${N} ${M} ${J.x} ${J.y}`}function YJ(Z,$){let J=c(Z,"at"),K=b(Z,"length")??2.54;if(!J)return"";let U=z(Z,"name"),V=z(Z,"number"),j=U&&U.length>=2&&X(U[1])?U[1]:"",Q=V&&V.length>=2&&X(V[1])?V[1]:"",G=J.rotation??0,D=G*Math.PI/180,H=J.x+K*Math.cos(D),W=J.y+K*Math.sin(D);E($,J.x,J.y),E($,H,W);let q=[];if(q.push(`<line x1="${J.x}" y1="${J.y}" x2="${H}" y2="${W}" stroke="${A.pin}" stroke-width="0.254"/>`),q.push(`<circle cx="${J.x}" cy="${J.y}" r="0.3" fill="${A.pin}"/>`),j&&j!=="~"){let M=H,N=W,k="start",F="middle";if(G===0)M=H+0.5,k="start";else if(G===180)M=H-0.5,k="end";else if(G===90)N=W+0.5,F="hanging",k="middle";else if(G===270)N=W-0.5,F="alphabetic",k="middle";q.push(`<text x="${M}" y="${N}" fill="${A.pinText}" font-size="1" font-family="sans-serif" text-anchor="${k}" dominant-baseline="${F}" transform="scale(1,-1) translate(0,${-2*N})">${g(j)}</text>`)}if(Q){let Y=(J.x+H)/2,M=(J.y+W)/2,N=G===0||G===180?0.8:0;q.push(`<text x="${Y}" y="${M+N}" fill="${A.pinText}" font-size="0.8" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle" transform="scale(1,-1) translate(0,${-2*(M+N)})">${g(Q)}</text>`)}return q.join(`
461
+ `)}function s(Z){if(!Z||Z.trim()==="")return T("No footprint data");try{let $=d(Z);if(!B($)||v($)!=="footprint")return T("Invalid footprint format");let J=[],K={minX:1/0,maxX:-1/0,minY:1/0,maxY:-1/0};for(let G of I($,"pad")){let D=NJ(G,K);if(D)J.push(D)}for(let G of I($,"fp_line")){let D=$J(G,K);if(D)J.push(D)}for(let G of I($,"fp_circle")){let D=KJ(G,K);if(D)J.push(D)}for(let G of I($,"fp_arc")){let D=UJ(G,K);if(D)J.push(D)}for(let G of I($,"fp_rect")){let D=DJ(G,K);if(D)J.push(D)}for(let G of I($,"fp_poly")){let D=VJ(G,K);if(D)J.push(D)}for(let G of I($,"gr_line")){let D=$J(G,K);if(D)J.push(D)}for(let G of I($,"gr_circle")){let D=KJ(G,K);if(D)J.push(D)}for(let G of I($,"gr_arc")){let D=UJ(G,K);if(D)J.push(D)}for(let G of I($,"gr_rect")){let D=DJ(G,K);if(D)J.push(D)}for(let G of I($,"gr_poly")){let D=VJ(G,K);if(D)J.push(D)}for(let G of I($,"fp_text")){let D=zJ(G,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 V=K.maxX-K.minX+U*2,j=K.maxY-K.minY+U*2;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${`${K.minX-U} ${K.minY-U} ${V} ${j}`}" width="100%" height="100%" style="background-color: ${w.background}">
462
+ <g>
463
+ ${J.join(`
464
+ `)}
465
+ </g>
466
+ </svg>`}catch($){return T(`Parse error: ${$}`)}}function C(Z){if(Z.includes("F.Cu"))return w.fCu;if(Z.includes("B.Cu"))return w.bCu;if(/In\d+\.Cu/.test(Z))return w.in1Cu;if(Z.includes("F.SilkS"))return w.fSilkS;if(Z.includes("B.SilkS"))return w.bSilkS;if(Z.includes("F.Mask"))return w.fMask;if(Z.includes("B.Mask"))return w.bMask;if(Z.includes("F.Paste"))return w.fPaste;if(Z.includes("B.Paste"))return w.bPaste;if(Z.includes("F.Fab"))return w.fFab;if(Z.includes("B.Fab"))return w.bFab;if(Z.includes("F.CrtYd"))return w.fCrtYd;if(Z.includes("B.CrtYd"))return w.bCrtYd;if(Z.includes("Edge.Cuts"))return w.edgeCuts;if(Z.includes("Dwgs.User"))return w.dwgsUser;if(Z.includes("Cmts.User"))return w.cmtsUser;if(Z.includes("Margin"))return w.margin;return w.fSilkS}function NJ(Z,$){if(!B(Z)||Z.length<4)return"";let J=X(Z[1])?Z[1]:"",K=X(Z[2])?Z[2]:"",U=X(Z[3])?Z[3]:"",V=c(Z,"at"),j=o(Z);if(!V||!j)return"";E($,V.x-j.width/2,V.y-j.height/2),E($,V.x+j.width/2,V.y+j.height/2);let Q=[],G=R(Z),D;if(K==="thru_hole"||K==="np_thru_hole")D=w.padThruHole;else if(G.some((q)=>q.includes("B.")))D=w.padBack;else D=w.padFront;let H=V.rotation??0,W=H!==0?` transform="rotate(${H}, ${V.x}, ${V.y})"`:"";if(U==="circle"){let q=Math.min(j.width,j.height)/2;Q.push(`<circle cx="${V.x}" cy="${V.y}" r="${q}" fill="${D}"${W}/>`)}else if(U==="oval"){let q=j.width/2,Y=j.height/2;Q.push(`<ellipse cx="${V.x}" cy="${V.y}" rx="${q}" ry="${Y}" fill="${D}"${W}/>`)}else if(U==="roundrect"){let q=b(Z,"roundrect_rratio")??0.25,Y=Math.min(j.width,j.height)*q/2,M=V.x-j.width/2,N=V.y-j.height/2;Q.push(`<rect x="${M}" y="${N}" width="${j.width}" height="${j.height}" rx="${Y}" fill="${D}"${W}/>`)}else if(U==="custom"){let q=z(Z,"primitives");if(q)for(let Y of I(q,"gr_poly")){let M=u(Y);if(M.length>=3){let N=M.map((k)=>{let F=V.x+k.x,m=V.y+k.y;return E($,F,m),`${F},${m}`}).join(" ");Q.push(`<polygon points="${N}" fill="${D}"${W}/>`)}}if(Q.length===0){let Y=V.x-j.width/2,M=V.y-j.height/2;Q.push(`<rect x="${Y}" y="${M}" width="${j.width}" height="${j.height}" fill="${D}"${W}/>`)}}else{let q=V.x-j.width/2,Y=V.y-j.height/2;Q.push(`<rect x="${q}" y="${Y}" width="${j.width}" height="${j.height}" fill="${D}"${W}/>`)}if(K==="thru_hole"||K==="np_thru_hole"){let q=z(Z,"drill");if(q&&q.length>=2)if(X(q[1])&&q[1]==="oval"&&q.length>=4){let M=parseFloat(X(q[2])?q[2]:"0"),N=parseFloat(X(q[3])?q[3]:"0");Q.push(`<ellipse cx="${V.x}" cy="${V.y}" rx="${M/2}" ry="${N/2}" fill="${w.drill}"${W}/>`)}else{let M=parseFloat(X(q[1])?q[1]:"0");Q.push(`<circle cx="${V.x}" cy="${V.y}" r="${M/2}" fill="${w.drill}"/>`)}}if(J){let q=Math.min(j.width,j.height)*0.5;Q.push(`<text x="${V.x}" y="${V.y}" fill="#FFFFFF" font-size="${q}" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${g(J)}</text>`)}return Q.join(`
467
+ `)}function $J(Z,$){let J=_(Z,"start"),K=_(Z,"end");if(!J||!K)return"";let U=P(Z),j=R(Z)[0]??"F.SilkS";if(j.includes("CrtYd"))return"";let Q=C(j);return E($,J.x,J.y),E($,K.x,K.y),`<line x1="${J.x}" y1="${J.y}" x2="${K.x}" y2="${K.y}" stroke="${Q}" stroke-width="${U?.width??0.15}" stroke-linecap="round"/>`}function KJ(Z,$){let J=_(Z,"center"),K=_(Z,"end");if(!J||!K)return"";let U=Math.sqrt((K.x-J.x)**2+(K.y-J.y)**2),V=P(Z),Q=R(Z)[0]??"F.SilkS";if(Q.includes("CrtYd"))return"";let G=C(Q);return E($,J.x-U,J.y-U),E($,J.x+U,J.y+U),`<circle cx="${J.x}" cy="${J.y}" r="${U}" fill="none" stroke="${G}" stroke-width="${V?.width??0.15}"/>`}function UJ(Z,$){let J=_(Z,"start"),K=_(Z,"mid"),U=_(Z,"end");if(!J||!K||!U)return"";let V=P(Z),Q=R(Z)[0]??"F.SilkS";if(Q.includes("CrtYd"))return"";let G=C(Q);return E($,J.x,J.y),E($,K.x,K.y),E($,U.x,U.y),`<path d="${jJ(J,K,U)}" fill="none" stroke="${G}" stroke-width="${V?.width??0.15}" stroke-linecap="round"/>`}function VJ(Z,$){let J=u(Z);if(J.length<3)return"";let U=R(Z)[0]??"F.Cu";if(U.includes("CrtYd"))return"";let V=C(U),j=P(Z),Q=f(Z),G=Q==="solid"||Q==="yes"?V:"none",D=J.map((q)=>{return E($,q.x,q.y),`${q.x},${q.y}`}).join(" "),H=j?.width??0,W=H>0?` stroke="${V}" stroke-width="${H}"`:"";return`<polygon points="${D}" fill="${G}"${W}/>`}function DJ(Z,$){let J=_(Z,"start"),K=_(Z,"end");if(!J||!K)return"";let V=R(Z)[0]??"F.SilkS";if(V.includes("CrtYd"))return"";let j=C(V),Q=P(Z),D=f(Z)==="solid"?j:"none";E($,J.x,J.y),E($,K.x,K.y);let H=Math.min(J.x,K.x),W=Math.min(J.y,K.y),q=Math.abs(K.x-J.x),Y=Math.abs(K.y-J.y);return`<rect x="${H}" y="${W}" width="${q}" height="${Y}" fill="${D}" stroke="${j}" stroke-width="${Q?.width??0.15}"/>`}function zJ(Z,$){if(!B(Z)||Z.length<3)return"";let J=X(Z[1])?Z[1]:"",K=X(Z[2])?Z[2]:"";if(J==="reference"||J==="value")return"";let U=c(Z,"at");if(!U)return"";let j=R(Z)[0]??"F.SilkS",Q=C(j),G=z(Z,"effects"),D=1,H="middle";if(G){let N=z(G,"font");if(N){let F=o(N);if(F)D=F.height}let k=z(G,"justify");if(k)for(let F=1;F<k.length;F++){let m=X(k[F])?k[F]:"";if(m==="left")H="start";else if(m==="right")H="end"}}let W=K.length*D*0.6,q=D;if(H==="start")E($,U.x,U.y-q/2),E($,U.x+W,U.y+q/2);else if(H==="end")E($,U.x-W,U.y-q/2),E($,U.x,U.y+q/2);else E($,U.x-W/2,U.y-q/2),E($,U.x+W/2,U.y+q/2);let Y=U.rotation??0,M=Y!==0?` transform="rotate(${Y}, ${U.x}, ${U.y})"`:"";return`<text x="${U.x}" y="${U.y}" dy="0.35em" fill="${Q}" font-size="${D}" font-family="sans-serif" text-anchor="${H}"${M}>${g(K)}</text>`}function E(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 T(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">${g(Z)}</text>
470
+ </svg>`}var JJ=1,h="",WJ="user",i=!1,n=null,O=document.getElementById("search-input"),GJ=document.getElementById("source-select"),_J=document.getElementById("search-btn"),e=document.getElementById("results-grid"),r=document.getElementById("pagination"),FJ=document.getElementById("loading"),l=document.getElementById("preview-modal"),p=document.getElementById("modal-content"),kJ=document.getElementById("modal-close");function qJ(){let Z=window.__INITIAL_QUERY__;if(Z)O.value=Z,h=Z,L();else{let J=new URLSearchParams(window.location.search).get("q");if(J)O.value=J,h=J,L()}O.addEventListener("input",BJ),O.addEventListener("keydown",($)=>{if($.key==="Enter")$.preventDefault(),L()}),_J.addEventListener("click",L),GJ.addEventListener("change",()=>{if(WJ=GJ.value,h)L()}),kJ.addEventListener("click",t),l.addEventListener("click",($)=>{if($.target===l)t()}),document.addEventListener("keydown",($)=>{if($.key==="Escape")t()})}function BJ(){if(n)clearTimeout(n);n=window.setTimeout(()=>{let Z=O.value.trim();if(Z&&Z!==h)h=Z,JJ=1,L()},300)}async function L(){let Z=O.value.trim();if(!Z||i)return;h=Z,i=!0,QJ(!0);try{let $=new URLSearchParams({q:Z,source:WJ,page:String(JJ),limit:"20"}),J=await fetch(`/api/search?${$}`);if(!J.ok)throw Error("Search failed");let K=await J.json();AJ(K.results),vJ(K.pagination)}catch($){console.error("Search error:",$),e.innerHTML='<div class="error">Search failed. Please try again.</div>'}finally{i=!1,QJ(!1)}}function AJ(Z){if(Z.length===0){e.innerHTML='<div class="no-results">No components found. Try a different search term.</div>';return}e.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="${x($.title)}">${x($.title)}</div>
484
+ <div class="card-package">Package: ${x($.package||"Unknown")}</div>
485
+ <div class="card-owner">By: ${x($.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)HJ(K,$)})}),document.querySelectorAll(".image-container").forEach(($)=>{$.addEventListener("click",async()=>{let J=$.dataset.uuid;if(J)RJ(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 V=a(U.symbolSexpr);$.innerHTML=V+'<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 V=s(U.footprintSexpr);J.innerHTML=V+'<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 RJ(Z){l.classList.remove("hidden"),p.innerHTML='<div class="modal-loading">Loading...</div>';try{let $=await fetch(`/api/component/${Z}`);if(!$.ok)throw Error("Failed to fetch component");let J=await $.json(),K=J.symbolSexpr?a(J.symbolSexpr):"",U=J.footprintSexpr?s(J.footprintSexpr):"";p.innerHTML=`
495
+ <div class="modal-header">
496
+ <h2>${x(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> ${x(J.description||"N/A")}</p>
514
+ ${J.model3d?"<p><strong>3D Model:</strong> Available</p>":""}
515
+ </div>
516
+ `;let V=p.querySelector(".modal-copy");if(V)V.addEventListener("click",()=>{HJ(J.uuid,V)})}catch($){console.error("Modal error:",$),p.innerHTML='<div class="modal-error">Failed to load component details</div>'}}function t(){l.classList.add("hidden")}function vJ(Z){if(Z.totalPages<=1){r.innerHTML="";return}r.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
+ `,r.querySelectorAll(".page-btn").forEach(($)=>{$.addEventListener("click",()=>{let J=parseInt($.dataset.page||"1",10);if(J>0)JJ=J,L(),window.scrollTo(0,0)})})}async function HJ(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 QJ(Z){FJ.classList.toggle("hidden",!Z)}function x(Z){let $=document.createElement("div");return $.textContent=Z,$.innerHTML}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",qJ);else qJ();
525
+
526
+ </script>
527
+ </body>
528
+ </html>