kishare 1.0.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/LICENSE +674 -0
- package/README.md +91 -0
- package/bin/kishare.js +27 -0
- package/index.html +15 -0
- package/package.json +53 -0
- package/src/components/project-list.ts +326 -0
- package/src/components/viewer-panel.ts +546 -0
- package/src/components/workspace-app.ts +270 -0
- package/src/indexer/copy-public-files.ts +65 -0
- package/src/indexer/indexer-utils.ts +111 -0
- package/src/indexer/scan-projects.ts +399 -0
- package/src/lib/constants.ts +44 -0
- package/src/lib/html-utils.ts +20 -0
- package/src/lib/project-index.ts +60 -0
- package/src/lib/router.ts +208 -0
- package/src/main.ts +13 -0
- package/src/node/cli.ts +161 -0
- package/src/styles/main.css +698 -0
- package/tsconfig.json +26 -0
- package/vendor/kicanvas/tsconfig.json +48 -0
- package/vite.config.ts +69 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer panel component - wraps KiCanvas embed element
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProjectMetadata, GitInfo } from '../lib/project-index.js';
|
|
6
|
+
import { router, type ViewPosition, type MarkerBounds } from '../lib/router.js';
|
|
7
|
+
import { MARKER, TOAST } from '../lib/constants.js';
|
|
8
|
+
import { githubIcon } from '../lib/html-utils.js';
|
|
9
|
+
|
|
10
|
+
interface MousePosition {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ViewerPanel extends HTMLElement {
|
|
16
|
+
private currentEmbed: HTMLElement | null = null;
|
|
17
|
+
private mousePosition: MousePosition = { x: 0, y: 0 };
|
|
18
|
+
private gitInfo: GitInfo | null = null;
|
|
19
|
+
private markerId: number | null = null;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connectedCallback() {
|
|
26
|
+
this.render();
|
|
27
|
+
this.setupEventListeners();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set git info for GitHub issue links
|
|
32
|
+
*/
|
|
33
|
+
setGitInfo(git: GitInfo) {
|
|
34
|
+
this.gitInfo = git;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load a project in the viewer
|
|
39
|
+
*/
|
|
40
|
+
loadProject(project: ProjectMetadata, position?: ViewPosition, marker?: import('../lib/router.js').MarkerBounds) {
|
|
41
|
+
// Cleanup previous marker
|
|
42
|
+
this.clearMarker();
|
|
43
|
+
|
|
44
|
+
// Clear existing content
|
|
45
|
+
this.innerHTML = '';
|
|
46
|
+
|
|
47
|
+
// Create container for viewer and toolbar
|
|
48
|
+
const container = document.createElement('div');
|
|
49
|
+
container.className = 'viewer-container';
|
|
50
|
+
|
|
51
|
+
// Create kicanvas-embed element
|
|
52
|
+
const embed = document.createElement('kicanvas-embed');
|
|
53
|
+
embed.setAttribute('controls', 'full');
|
|
54
|
+
|
|
55
|
+
// Add project file as a source
|
|
56
|
+
if (project.projectFile) {
|
|
57
|
+
this.addSource(embed, project.projectFile);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add all schematic files
|
|
61
|
+
for (const schematic of project.schematics) {
|
|
62
|
+
this.addSource(embed, schematic.path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add PCB if exists
|
|
66
|
+
if (project.pcb) {
|
|
67
|
+
this.addSource(embed, project.pcb);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
container.appendChild(embed);
|
|
71
|
+
|
|
72
|
+
this.appendChild(container);
|
|
73
|
+
this.currentEmbed = embed;
|
|
74
|
+
|
|
75
|
+
// Recreate context menu (it was cleared with innerHTML)
|
|
76
|
+
this.createContextMenu();
|
|
77
|
+
|
|
78
|
+
// Setup event listeners
|
|
79
|
+
this.setupViewerEvents(embed, position, marker);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Setup event listeners
|
|
84
|
+
*/
|
|
85
|
+
private setupViewerEvents(embed: HTMLElement, position?: ViewPosition, marker?: import('../lib/router.js').MarkerBounds) {
|
|
86
|
+
// Track mouse position
|
|
87
|
+
embed.addEventListener('kicanvas:mousemove', ((e: CustomEvent) => {
|
|
88
|
+
this.mousePosition = {
|
|
89
|
+
x: e.detail.x,
|
|
90
|
+
y: e.detail.y,
|
|
91
|
+
};
|
|
92
|
+
}) as EventListener);
|
|
93
|
+
|
|
94
|
+
// Pan to position after load
|
|
95
|
+
if (position) {
|
|
96
|
+
embed.addEventListener('kicanvas:load', () => {
|
|
97
|
+
this.panToPosition(position, marker);
|
|
98
|
+
}, { once: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pan viewer to a specific position and optionally show marker
|
|
104
|
+
*/
|
|
105
|
+
private panToPosition(position: ViewPosition, marker?: import('../lib/router.js').MarkerBounds) {
|
|
106
|
+
try {
|
|
107
|
+
// Switch to the specified file if needed
|
|
108
|
+
if (position.file && position.file !== 'pcb') {
|
|
109
|
+
// Schematic sheet - need to switch to it first
|
|
110
|
+
const shadowRoot = this.currentEmbed?.shadowRoot;
|
|
111
|
+
if (shadowRoot) {
|
|
112
|
+
const schematicApp = shadowRoot.querySelector('kc-schematic-app');
|
|
113
|
+
if (schematicApp && (schematicApp as any).project) {
|
|
114
|
+
const project = (schematicApp as any).project;
|
|
115
|
+
console.log('Switching to sheet:', position.file, 'current:', project.active_page?.project_path);
|
|
116
|
+
|
|
117
|
+
// Always set the active page to ensure we're on the right sheet
|
|
118
|
+
project.set_active_page(position.file);
|
|
119
|
+
|
|
120
|
+
// Wait for the sheet to load and viewer to be ready
|
|
121
|
+
setTimeout(() => this.doPanAndMarker(position, marker, 'schematic'), 300);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// PCB or no file specified - use currently active viewer
|
|
128
|
+
this.doPanAndMarker(position, marker, position.file === 'pcb' ? 'pcb' : null);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.warn('Failed to pan to position:', e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Actually perform the pan and add marker
|
|
136
|
+
*/
|
|
137
|
+
private doPanAndMarker(position: ViewPosition, marker?: import('../lib/router.js').MarkerBounds, preferredType?: 'schematic' | 'pcb' | null) {
|
|
138
|
+
const viewer = this.getViewer(preferredType);
|
|
139
|
+
if (!viewer) {
|
|
140
|
+
console.warn('No viewer found for type:', preferredType);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
console.log('Adding marker to viewer at', position.x, position.y);
|
|
146
|
+
viewer.viewport.camera.center.set(position.x, position.y);
|
|
147
|
+
viewer.viewport.camera.zoom = position.zoom;
|
|
148
|
+
viewer.draw();
|
|
149
|
+
|
|
150
|
+
// Add marker if specified
|
|
151
|
+
if (marker) {
|
|
152
|
+
// Draw bounding box with arrow pointing to center
|
|
153
|
+
const centerX = marker.x + marker.width / 2;
|
|
154
|
+
const centerY = marker.y + marker.height / 2;
|
|
155
|
+
|
|
156
|
+
console.log('Adding marker at center:', centerX, centerY);
|
|
157
|
+
// Add arrow marker at center of bounding box
|
|
158
|
+
this.markerId = viewer.addMarker(centerX, centerY, MARKER.STYLE);
|
|
159
|
+
} else {
|
|
160
|
+
// No marker specified, just add a simple arrow at the center
|
|
161
|
+
this.markerId = viewer.addMarker(position.x, position.y, MARKER.STYLE);
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.warn('Failed to do pan and marker:', e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear any active marker
|
|
170
|
+
*/
|
|
171
|
+
private clearMarker() {
|
|
172
|
+
if (this.markerId !== null) {
|
|
173
|
+
const viewer = this.getViewer();
|
|
174
|
+
if (viewer) {
|
|
175
|
+
viewer.removeMarker(this.markerId);
|
|
176
|
+
}
|
|
177
|
+
this.markerId = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Toggle visibility of all markers
|
|
183
|
+
*/
|
|
184
|
+
private toggleMarkersVisible() {
|
|
185
|
+
const viewer = this.getViewer();
|
|
186
|
+
if (!viewer) return;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const visible = viewer.toggleMarkersVisible();
|
|
190
|
+
this.showToast(visible ? 'Markers shown' : 'Markers hidden');
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.warn('Failed to toggle markers:', e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the current viewer instance
|
|
198
|
+
*/
|
|
199
|
+
private getViewer(preferredType?: 'schematic' | 'pcb' | null): any {
|
|
200
|
+
if (!this.currentEmbed) return null;
|
|
201
|
+
|
|
202
|
+
const shadowRoot = this.currentEmbed.shadowRoot;
|
|
203
|
+
if (!shadowRoot) return null;
|
|
204
|
+
|
|
205
|
+
// If a specific type is requested, return that viewer
|
|
206
|
+
if (preferredType === 'pcb') {
|
|
207
|
+
const boardApp = shadowRoot.querySelector('kc-board-app');
|
|
208
|
+
if (boardApp && (boardApp as any).viewer) {
|
|
209
|
+
return (boardApp as any).viewer;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (preferredType === 'schematic') {
|
|
215
|
+
const schematicApp = shadowRoot.querySelector('kc-schematic-app');
|
|
216
|
+
if (schematicApp && (schematicApp as any).viewer) {
|
|
217
|
+
return (schematicApp as any).viewer;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// No preference - check which is visible
|
|
223
|
+
// Try board viewer first, then schematic
|
|
224
|
+
const boardApp = shadowRoot.querySelector('kc-board-app');
|
|
225
|
+
if (boardApp) {
|
|
226
|
+
const style = window.getComputedStyle(boardApp);
|
|
227
|
+
if (style.display !== 'none' && (boardApp as any).viewer) {
|
|
228
|
+
return (boardApp as any).viewer;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const schematicApp = shadowRoot.querySelector('kc-schematic-app');
|
|
233
|
+
if (schematicApp) {
|
|
234
|
+
const style = window.getComputedStyle(schematicApp);
|
|
235
|
+
if (style.display !== 'none' && (schematicApp as any).viewer) {
|
|
236
|
+
return (schematicApp as any).viewer;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the current file identifier (for URL tracking)
|
|
245
|
+
*/
|
|
246
|
+
private getCurrentFile(): string | null {
|
|
247
|
+
if (!this.currentEmbed) return null;
|
|
248
|
+
|
|
249
|
+
const shadowRoot = this.currentEmbed.shadowRoot;
|
|
250
|
+
if (!shadowRoot) return null;
|
|
251
|
+
|
|
252
|
+
// Check if board app is visible and has a viewer
|
|
253
|
+
const boardApp = shadowRoot.querySelector('kc-board-app');
|
|
254
|
+
if (boardApp) {
|
|
255
|
+
const style = window.getComputedStyle(boardApp);
|
|
256
|
+
if (style.display !== 'none' && (boardApp as any).viewer) {
|
|
257
|
+
return 'pcb';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if schematic app is visible and has active page
|
|
262
|
+
const schematicApp = shadowRoot.querySelector('kc-schematic-app');
|
|
263
|
+
if (schematicApp) {
|
|
264
|
+
const style = window.getComputedStyle(schematicApp);
|
|
265
|
+
if (style.display !== 'none' && (schematicApp as any).project?.active_page) {
|
|
266
|
+
const activePage = (schematicApp as any).project.active_page;
|
|
267
|
+
console.log('Current active page:', activePage.project_path);
|
|
268
|
+
// Return the project_path which uniquely identifies the sheet
|
|
269
|
+
return activePage.project_path || null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get current view position
|
|
278
|
+
*/
|
|
279
|
+
private getCurrentPosition(): ViewPosition | null {
|
|
280
|
+
const viewer = this.getViewer();
|
|
281
|
+
if (!viewer) return null;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const file = this.getCurrentFile();
|
|
285
|
+
return {
|
|
286
|
+
x: viewer.viewport.camera.center.x,
|
|
287
|
+
y: viewer.viewport.camera.center.y,
|
|
288
|
+
zoom: viewer.viewport.camera.zoom,
|
|
289
|
+
file: file || undefined,
|
|
290
|
+
};
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Setup event listeners for keyboard shortcuts
|
|
298
|
+
*/
|
|
299
|
+
private setupEventListeners() {
|
|
300
|
+
this.createContextMenu();
|
|
301
|
+
|
|
302
|
+
// Use keyboard shortcut instead: 'C' key to open context menu
|
|
303
|
+
let lastMouseX = 0;
|
|
304
|
+
let lastMouseY = 0;
|
|
305
|
+
|
|
306
|
+
// Track screen mouse position
|
|
307
|
+
document.addEventListener('mousemove', (e: MouseEvent) => {
|
|
308
|
+
lastMouseX = e.clientX;
|
|
309
|
+
lastMouseY = e.clientY;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
313
|
+
const key = e.key.toLowerCase();
|
|
314
|
+
|
|
315
|
+
// Press 'C' to open context menu at mouse position
|
|
316
|
+
if (key === 'c') {
|
|
317
|
+
if (!this.currentEmbed) return;
|
|
318
|
+
this.showContextMenu(lastMouseX, lastMouseY);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Press 'T' to toggle marker visibility
|
|
322
|
+
if (key === 't') {
|
|
323
|
+
if (!this.currentEmbed) return;
|
|
324
|
+
this.toggleMarkersVisible();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
document.addEventListener('click', () => {
|
|
329
|
+
this.closeContextMenu();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create context menu element
|
|
335
|
+
*/
|
|
336
|
+
private createContextMenu() {
|
|
337
|
+
// Remove old menu if exists
|
|
338
|
+
const oldMenu = this.querySelector('.context-menu');
|
|
339
|
+
if (oldMenu) oldMenu.remove();
|
|
340
|
+
|
|
341
|
+
// Create context menu element
|
|
342
|
+
const menu = document.createElement('div');
|
|
343
|
+
menu.className = 'context-menu hidden';
|
|
344
|
+
menu.innerHTML = `
|
|
345
|
+
<button class="context-menu-item" data-action="copy">
|
|
346
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
347
|
+
<path 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"/>
|
|
348
|
+
</svg>
|
|
349
|
+
Copy location link
|
|
350
|
+
</button>
|
|
351
|
+
<button class="context-menu-item" data-action="github">
|
|
352
|
+
${githubIcon(16)}
|
|
353
|
+
Open GitHub Issue
|
|
354
|
+
</button>
|
|
355
|
+
`;
|
|
356
|
+
this.appendChild(menu);
|
|
357
|
+
|
|
358
|
+
// Setup menu item click handlers
|
|
359
|
+
menu.querySelector('[data-action="copy"]')!.addEventListener('click', (e) => {
|
|
360
|
+
e.stopPropagation();
|
|
361
|
+
this.copyLocationLink();
|
|
362
|
+
this.closeContextMenu();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
menu.querySelector('[data-action="github"]')!.addEventListener('click', (e) => {
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
this.openGitHubIssue();
|
|
368
|
+
this.closeContextMenu();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Show context menu at position
|
|
374
|
+
*/
|
|
375
|
+
private showContextMenu(x: number, y: number) {
|
|
376
|
+
const menu = this.querySelector('.context-menu') as HTMLElement;
|
|
377
|
+
if (!menu) return;
|
|
378
|
+
|
|
379
|
+
// Position menu
|
|
380
|
+
menu.style.left = `${x}px`;
|
|
381
|
+
menu.style.top = `${y}px`;
|
|
382
|
+
menu.classList.remove('hidden');
|
|
383
|
+
|
|
384
|
+
// Adjust if menu goes off screen
|
|
385
|
+
const rect = menu.getBoundingClientRect();
|
|
386
|
+
if (rect.right > window.innerWidth) {
|
|
387
|
+
menu.style.left = `${x - rect.width}px`;
|
|
388
|
+
}
|
|
389
|
+
if (rect.bottom > window.innerHeight) {
|
|
390
|
+
menu.style.top = `${y - rect.height}px`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Close context menu
|
|
396
|
+
*/
|
|
397
|
+
private closeContextMenu() {
|
|
398
|
+
const menu = this.querySelector('.context-menu');
|
|
399
|
+
menu?.classList.add('hidden');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Creates marker at current mouse position and returns URL.
|
|
404
|
+
*/
|
|
405
|
+
private createLocationMarker(): string {
|
|
406
|
+
const position = this.getCurrentPosition();
|
|
407
|
+
if (!position) {
|
|
408
|
+
console.warn('No viewer position available');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Get mouse position directly from viewer instead of relying on events
|
|
413
|
+
const viewer = this.getViewer();
|
|
414
|
+
if (!viewer || !viewer.mouse_position) {
|
|
415
|
+
console.warn('No viewer mouse position available');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const mousePos = {
|
|
420
|
+
x: viewer.mouse_position.x,
|
|
421
|
+
y: viewer.mouse_position.y,
|
|
422
|
+
};
|
|
423
|
+
console.log('Mouse position for marker:', mousePos);
|
|
424
|
+
|
|
425
|
+
// Create a marker bounds around current mouse position
|
|
426
|
+
const marker: MarkerBounds = {
|
|
427
|
+
x: mousePos.x - MARKER.SIZE / 2,
|
|
428
|
+
y: mousePos.y - MARKER.SIZE / 2,
|
|
429
|
+
width: MARKER.SIZE,
|
|
430
|
+
height: MARKER.SIZE,
|
|
431
|
+
};
|
|
432
|
+
console.log('Marker bounds:', marker);
|
|
433
|
+
|
|
434
|
+
// Add temporary marker to show what will be shared
|
|
435
|
+
const centerX = marker.x + marker.width / 2;
|
|
436
|
+
const centerY = marker.y + marker.height / 2;
|
|
437
|
+
|
|
438
|
+
const tempMarkerId = viewer.addMarker(centerX, centerY, MARKER.STYLE);
|
|
439
|
+
|
|
440
|
+
// Remove temporary marker after configured duration
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
viewer.removeMarker(tempMarkerId);
|
|
443
|
+
}, MARKER.TEMP_DURATION);
|
|
444
|
+
|
|
445
|
+
return router.buildPositionUrl(position, marker);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Copy location link to clipboard
|
|
450
|
+
*/
|
|
451
|
+
private async copyLocationLink() {
|
|
452
|
+
const url = this.createLocationMarker();
|
|
453
|
+
try {
|
|
454
|
+
await navigator.clipboard.writeText(url);
|
|
455
|
+
this.showToast('Link copied to clipboard');
|
|
456
|
+
} catch (e) {
|
|
457
|
+
console.error('Failed to copy:', e);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Open GitHub issue with location link
|
|
463
|
+
*/
|
|
464
|
+
private openGitHubIssue() {
|
|
465
|
+
const url = this.createLocationMarker();
|
|
466
|
+
const route = router.getCurrentRoute();
|
|
467
|
+
|
|
468
|
+
// Build GitHub issue URL
|
|
469
|
+
const title = encodeURIComponent(`Review: ${route.projectId || 'Project'}`);
|
|
470
|
+
const body = encodeURIComponent(`## Location\n\n[View in workspace](${url})\n\n## Comment\n\n`);
|
|
471
|
+
|
|
472
|
+
if (this.gitInfo?.repoUrl) {
|
|
473
|
+
const issueUrl = `${this.gitInfo.repoUrl}/issues/new?title=${title}&body=${body}`;
|
|
474
|
+
window.open(issueUrl, '_blank');
|
|
475
|
+
} else {
|
|
476
|
+
// Fallback: just copy the link
|
|
477
|
+
this.copyLocationLink();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Show a toast notification
|
|
483
|
+
*/
|
|
484
|
+
private showToast(message: string) {
|
|
485
|
+
const existing = this.querySelector('.toast');
|
|
486
|
+
if (existing) existing.remove();
|
|
487
|
+
|
|
488
|
+
const toast = document.createElement('div');
|
|
489
|
+
toast.className = 'toast';
|
|
490
|
+
toast.textContent = message;
|
|
491
|
+
this.appendChild(toast);
|
|
492
|
+
|
|
493
|
+
setTimeout(() => toast.classList.add('show'), 10);
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
toast.classList.remove('show');
|
|
496
|
+
setTimeout(() => toast.remove(), TOAST.FADE_DURATION);
|
|
497
|
+
}, TOAST.DURATION);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Add a file source to the kicanvas embed
|
|
502
|
+
*/
|
|
503
|
+
private addSource(embed: HTMLElement, src: string) {
|
|
504
|
+
const source = document.createElement('kicanvas-source');
|
|
505
|
+
source.setAttribute('src', src);
|
|
506
|
+
embed.appendChild(source);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Show welcome message when no project is selected
|
|
511
|
+
*/
|
|
512
|
+
showWelcome() {
|
|
513
|
+
this.currentEmbed = null;
|
|
514
|
+
this.clearMarker();
|
|
515
|
+
this.innerHTML = `
|
|
516
|
+
<div class="welcome">
|
|
517
|
+
<h2>KiShare Workspace</h2>
|
|
518
|
+
<p>Select a project from the sidebar to view schematics and PCBs</p>
|
|
519
|
+
</div>
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Show error message
|
|
525
|
+
*/
|
|
526
|
+
showError(message: string) {
|
|
527
|
+
this.currentEmbed = null;
|
|
528
|
+
this.clearMarker();
|
|
529
|
+
this.innerHTML = `
|
|
530
|
+
<div class="error">
|
|
531
|
+
<h3>Error</h3>
|
|
532
|
+
<p>${message}</p>
|
|
533
|
+
</div>
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Initial render
|
|
539
|
+
*/
|
|
540
|
+
private render() {
|
|
541
|
+
this.showWelcome();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Register custom element
|
|
546
|
+
customElements.define('viewer-panel', ViewerPanel);
|