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.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Main application component
3
+ */
4
+
5
+ import DOMPurify from 'dompurify';
6
+ import { loadProjectIndex, type GitInfo, type ProjectMetadata } from '../lib/project-index.js';
7
+ import { router } from '../lib/router.js';
8
+ import { isSafeUrl, githubIcon } from '../lib/html-utils.js';
9
+ import { SIDEBAR } from '../lib/constants.js';
10
+ import { ProjectList } from './project-list.js';
11
+ import { ViewerPanel } from './viewer-panel.js';
12
+
13
+ export class WorkspaceApp extends HTMLElement {
14
+ private projectList!: ProjectList;
15
+ private viewerPanel!: ViewerPanel;
16
+ private projects: ProjectMetadata[] = [];
17
+ private title: string = 'KiShare';
18
+ private sidebarCollapsed: boolean = false;
19
+ private sidebarWidth: number = 300; // Track uncollapsed width
20
+ private gitInfo: GitInfo | null = null;
21
+
22
+ constructor() {
23
+ super();
24
+ }
25
+
26
+ async connectedCallback() {
27
+ this.render();
28
+ await this.loadProjects();
29
+ this.setupRouting();
30
+ this.setupSidebarToggle();
31
+ this.setupSidebarResize();
32
+ }
33
+
34
+ /**
35
+ * Load project index from server
36
+ */
37
+ private async loadProjects() {
38
+ try {
39
+ const index = await loadProjectIndex();
40
+ this.projects = index.projects;
41
+ this.title = DOMPurify.sanitize(index.title);
42
+
43
+ // Update title in UI
44
+ const titleEl = this.querySelector('.sidebar-title');
45
+ if (titleEl) {
46
+ titleEl.textContent = this.title;
47
+ }
48
+ document.title = this.title;
49
+
50
+ // Update git info in footer and viewer
51
+ this.gitInfo = index.git;
52
+ this.updateGitInfo(index.git);
53
+ this.viewerPanel.setGitInfo(index.git);
54
+
55
+ // Update project list
56
+ this.projectList.setGitInfo(index.git);
57
+ this.projectList.setProjects(this.projects);
58
+
59
+ console.log(`Loaded ${this.projects.length} projects`);
60
+ } catch (error) {
61
+ console.error('Failed to load projects:', error);
62
+ this.viewerPanel.showError('Failed to load project index');
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Update git info in sidebar footer
68
+ */
69
+ private updateGitInfo(git: GitInfo) {
70
+ const footer = this.querySelector('.sidebar-footer');
71
+ if (!footer) return;
72
+
73
+ // Extract user/repo from repoUrl (e.g., "https://github.com/user/repo" -> "user/repo")
74
+ let repoPath = '';
75
+ if (git.repoUrl) {
76
+ const match = git.repoUrl.match(/https?:\/\/[^/]+\/(.+)/);
77
+ if (match) {
78
+ repoPath = match[1];
79
+ }
80
+ }
81
+
82
+ if (git.repoUrl && repoPath) {
83
+ const commitUrl = git.commitUrl || git.repoUrl;
84
+ if (!isSafeUrl(commitUrl)) {
85
+ console.warn('Invalid URL protocol in git info:', commitUrl);
86
+ return;
87
+ }
88
+
89
+ const html = `
90
+ <a href="${commitUrl}" target="_blank" rel="noopener" class="commit-link">
91
+ ${githubIcon(14)}
92
+ <span class="repo-path">${repoPath}</span>
93
+ ${git.commitHashShort ? `<span class="commit-separator">-</span><span class="commit-hash">${git.commitHashShort}</span>` : ''}
94
+ </a>
95
+ `;
96
+ footer.innerHTML = DOMPurify.sanitize(html);
97
+ } else if (git.commitHashShort) {
98
+ footer.innerHTML = DOMPurify.sanitize(`<span class="commit-hash">${git.commitHashShort}</span>`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Setup sidebar toggle functionality
104
+ */
105
+ private setupSidebarToggle() {
106
+ const toggleBtn = this.querySelector('.sidebar-toggle') as HTMLElement;
107
+ const sidebar = this.querySelector('.sidebar') as HTMLElement;
108
+
109
+ toggleBtn?.addEventListener('click', () => {
110
+ this.sidebarCollapsed = !this.sidebarCollapsed;
111
+ sidebar?.classList.toggle('collapsed', this.sidebarCollapsed);
112
+ toggleBtn?.classList.toggle('collapsed', this.sidebarCollapsed);
113
+
114
+ // Update button position
115
+ if (this.sidebarCollapsed) {
116
+ toggleBtn.style.left = '8px';
117
+ } else {
118
+ // Restore position based on tracked width
119
+ toggleBtn.style.left = `${this.sidebarWidth + 8}px`;
120
+ }
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Setup sidebar resize functionality
126
+ */
127
+ private setupSidebarResize() {
128
+ const resizeHandle = this.querySelector('.sidebar-resize-handle') as HTMLElement;
129
+ const sidebar = this.querySelector('.sidebar') as HTMLElement;
130
+ const toggleBtn = this.querySelector('.sidebar-toggle') as HTMLElement;
131
+
132
+ if (!resizeHandle || !sidebar) return;
133
+
134
+ // Load saved width from localStorage
135
+ const savedWidth = localStorage.getItem(SIDEBAR.STORAGE_KEY);
136
+ if (savedWidth) {
137
+ const width = parseInt(savedWidth, 10);
138
+ this.sidebarWidth = width;
139
+ sidebar.style.width = `${width}px`;
140
+ sidebar.style.minWidth = `${width}px`;
141
+ if (toggleBtn) {
142
+ toggleBtn.style.left = `${width + 8}px`;
143
+ }
144
+ } else {
145
+ // Initialize with default width
146
+ this.sidebarWidth = sidebar.offsetWidth || 300;
147
+ }
148
+
149
+ let isResizing = false;
150
+ let startX = 0;
151
+ let startWidth = 0;
152
+
153
+ const onMouseDown = (e: MouseEvent) => {
154
+ isResizing = true;
155
+ startX = e.clientX;
156
+ startWidth = sidebar.offsetWidth;
157
+ document.body.style.cursor = 'col-resize';
158
+ document.body.style.userSelect = 'none';
159
+ e.preventDefault();
160
+ };
161
+
162
+ const onMouseMove = (e: MouseEvent) => {
163
+ if (!isResizing) return;
164
+
165
+ const delta = e.clientX - startX;
166
+ const newWidth = Math.max(SIDEBAR.MIN_WIDTH, Math.min(SIDEBAR.MAX_WIDTH, startWidth + delta));
167
+
168
+ this.sidebarWidth = newWidth;
169
+ sidebar.style.width = `${newWidth}px`;
170
+ sidebar.style.minWidth = `${newWidth}px`;
171
+ if (toggleBtn) {
172
+ toggleBtn.style.left = `${newWidth + 8}px`;
173
+ }
174
+ };
175
+
176
+ const onMouseUp = () => {
177
+ if (isResizing) {
178
+ isResizing = false;
179
+ document.body.style.cursor = '';
180
+ document.body.style.userSelect = '';
181
+
182
+ // Save width to localStorage
183
+ this.sidebarWidth = sidebar.offsetWidth;
184
+ localStorage.setItem(SIDEBAR.STORAGE_KEY, this.sidebarWidth.toString());
185
+ }
186
+ };
187
+
188
+ resizeHandle.addEventListener('mousedown', onMouseDown);
189
+ document.addEventListener('mousemove', onMouseMove);
190
+ document.addEventListener('mouseup', onMouseUp);
191
+ }
192
+
193
+ /**
194
+ * Setup routing and handle route changes
195
+ */
196
+ private setupRouting() {
197
+ router.onRouteChange((route) => {
198
+ if (route.path === '/project' && route.projectId) {
199
+ this.loadProject(route.projectId, route.position, route.marker);
200
+ } else {
201
+ // Default route - show welcome
202
+ this.viewerPanel.showWelcome();
203
+ this.projectList.setSelectedProject(null);
204
+ }
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Load and display a specific project
210
+ */
211
+ private loadProject(
212
+ projectId: string,
213
+ position?: import('../lib/router.js').ViewPosition,
214
+ marker?: import('../lib/router.js').MarkerBounds
215
+ ) {
216
+ const project = this.projects.find(p => p.id === projectId);
217
+
218
+ if (!project) {
219
+ console.error(`Project not found: ${projectId}`);
220
+ this.viewerPanel.showError(`Project "${projectId}" not found`);
221
+ return;
222
+ }
223
+
224
+ console.log(`Loading project: ${project.name}`);
225
+
226
+ // Update UI
227
+ this.projectList.setSelectedProject(projectId);
228
+ this.viewerPanel.loadProject(project, position, marker);
229
+ }
230
+
231
+ /**
232
+ * Initial render of the workspace structure
233
+ */
234
+ private render() {
235
+ this.innerHTML = `
236
+ <div class="workspace">
237
+ <aside class="sidebar">
238
+ <div class="sidebar-header">
239
+ <h1 class="sidebar-title">${this.title}</h1>
240
+ </div>
241
+ <div class="sidebar-content">
242
+ <project-list></project-list>
243
+ </div>
244
+ <div class="sidebar-hints">
245
+ <div class="hint">Right-click to pan</div>
246
+ <div class="hint">Press 'C' to open GitHub issue</div>
247
+ <div class="hint">Press 'T' to toggle markers</div>
248
+ </div>
249
+ <div class="sidebar-footer"></div>
250
+ <div class="sidebar-resize-handle"></div>
251
+ </aside>
252
+ <button class="sidebar-toggle" title="Toggle sidebar">
253
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
254
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
255
+ </svg>
256
+ </button>
257
+ <main class="viewer-area">
258
+ <viewer-panel></viewer-panel>
259
+ </main>
260
+ </div>
261
+ `;
262
+
263
+ // Get references to child components
264
+ this.projectList = this.querySelector('project-list') as ProjectList;
265
+ this.viewerPanel = this.querySelector('viewer-panel') as ViewerPanel;
266
+ }
267
+ }
268
+
269
+ // Register custom element
270
+ customElements.define('workspace-app', WorkspaceApp);
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Copy relevant project files to output directory
4
+ * Respects .gitignore
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import type { WorkspaceConfig } from '../lib/project-index.js';
10
+ import { initializePaths, loadConfig, getProjectDirs, getRootDir, getOutputDir, getTrackedFiles } from './indexer-utils.js';
11
+
12
+ export function copyPublicFiles() {
13
+ initializePaths();
14
+ const config = loadConfig();
15
+ const projectDirs = getProjectDirs(config);
16
+
17
+ console.log(`Copying from directories: ${projectDirs.map(d => path.relative(getRootDir(), d)).join(', ')}`);
18
+
19
+ // Clean public directories for each project directory
20
+ for (const projectDir of projectDirs) {
21
+ const relativeDir = path.relative(getRootDir(), projectDir);
22
+ const publicDir = path.join(getOutputDir(), relativeDir);
23
+ if (fs.existsSync(publicDir)) {
24
+ fs.rmSync(publicDir, { recursive: true });
25
+ }
26
+ }
27
+
28
+ // Get tracked files from all project directories
29
+ const trackedFiles = getTrackedFiles(projectDirs);
30
+ if (trackedFiles.size === 0) {
31
+ console.log('No tracked files found in configured directories');
32
+ return;
33
+ }
34
+
35
+ console.log(`Found ${trackedFiles.size} tracked files`);
36
+
37
+ // Copy each tracked file, preserving the source directory structure
38
+ let copiedCount = 0;
39
+ for (const relativePath of trackedFiles) {
40
+ const srcPath = path.join(getRootDir(), relativePath);
41
+ const destPath = path.join(getOutputDir(), relativePath);
42
+
43
+ // Check if source is a directory (submodule)
44
+ const stat = fs.statSync(srcPath);
45
+ if (stat.isDirectory()) {
46
+ // Recursively copy submodule directory
47
+ fs.cpSync(srcPath, destPath, { recursive: true });
48
+ console.log(` Copied submodule: ${relativePath}`);
49
+ copiedCount++;
50
+ } else {
51
+ // Create destination directory and copy file
52
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
53
+ fs.copyFileSync(srcPath, destPath);
54
+ copiedCount++;
55
+ }
56
+ }
57
+
58
+ console.log(`Copied ${copiedCount} entries to ${getOutputDir()}`);
59
+ }
60
+
61
+ // Run if this is the main module
62
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
63
+ if (isMainModule) {
64
+ copyPublicFiles();
65
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Shared utilities for indexer scripts
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { execSync } from 'child_process';
8
+ import { fileURLToPath } from 'url';
9
+ import type { WorkspaceConfig } from '../lib/project-index.js';
10
+
11
+ // Module-level variables set at runtime
12
+ let ROOT_DIR: string;
13
+ let OUTPUT_DIR: string;
14
+ let KISHARE_ROOT: string;
15
+
16
+ // Get kishare installation root from environment variable or resolve from this file's location
17
+ function getKishareRoot(): string {
18
+ if (process.env.KISHARE_ROOT) {
19
+ return path.resolve(process.env.KISHARE_ROOT);
20
+ }
21
+ // Fallback: resolve from this file's location (two levels up from src/indexer/)
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ return path.resolve(path.dirname(__filename), '../..');
24
+ }
25
+
26
+ // Get user's project root (where kishare-config.json and KiCad files live)
27
+ function getProjectRoot(): string {
28
+ // KISHARE_PROJECT_ROOT is set by CLI, otherwise use cwd
29
+ return process.env.KISHARE_PROJECT_ROOT || process.cwd();
30
+ }
31
+
32
+ // Check if one path is a parent of another
33
+ export function isParentOf(parent: string, child: string): boolean {
34
+ const relative = path.relative(parent, child);
35
+ return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
36
+ }
37
+
38
+ // Read configuration from user's project directory
39
+ export function loadConfig(): WorkspaceConfig {
40
+ const configPath = path.join(getProjectRoot(), 'kishare-config.json');
41
+ if (!fs.existsSync(configPath)) {
42
+ console.warn(`No config file found at ${configPath}, using defaults`);
43
+ return {};
44
+ }
45
+ const configData = fs.readFileSync(configPath, 'utf-8');
46
+ return JSON.parse(configData) as WorkspaceConfig;
47
+ }
48
+
49
+ // Initialize paths
50
+ // ROOT_DIR = user's project (where KiCad files live)
51
+ // KISHARE_ROOT = kishare package installation
52
+ // OUTPUT_DIR = kishare's public directory (for Vite to serve)
53
+ export function initializePaths() {
54
+ ROOT_DIR = getProjectRoot();
55
+ KISHARE_ROOT = getKishareRoot();
56
+ OUTPUT_DIR = path.join(KISHARE_ROOT, 'public');
57
+
58
+ console.log(`Config file: ${path.join(ROOT_DIR, 'kishare-config.json')}`);
59
+ console.log(`Root directory (project files): ${ROOT_DIR}`);
60
+ console.log(`KiShare root: ${KISHARE_ROOT}`);
61
+ console.log(`Output directory: ${OUTPUT_DIR}`);
62
+ }
63
+
64
+ // Get project directories from config (defaults to ['projects'])
65
+ export function getProjectDirs(config: WorkspaceConfig): string[] {
66
+ const dirs = config.projectDirs || ['projects'];
67
+ return dirs.map(dir => path.join(ROOT_DIR, dir));
68
+ }
69
+
70
+ // Getters for initialized paths
71
+ export function getRootDir(): string {
72
+ if (!ROOT_DIR) {
73
+ throw new Error('Paths not initialized. Call initializePaths() first.');
74
+ }
75
+ return ROOT_DIR;
76
+ }
77
+
78
+ export function getOutputDir(): string {
79
+ if (!OUTPUT_DIR) {
80
+ throw new Error('Paths not initialized. Call initializePaths() first.');
81
+ }
82
+ return OUTPUT_DIR;
83
+ }
84
+
85
+ export function getKishareRootDir(): string {
86
+ if (!KISHARE_ROOT) {
87
+ throw new Error('Paths not initialized. Call initializePaths() first.');
88
+ }
89
+ return KISHARE_ROOT;
90
+ }
91
+
92
+ // Get list of git-tracked files in the specified project directories
93
+ export function getTrackedFiles(projectDirs: string[]): Set<string> {
94
+ const allFiles = new Set<string>();
95
+ for (const projectDir of projectDirs) {
96
+ try {
97
+ const relativeDir = path.relative(getRootDir(), projectDir);
98
+ console.log(`Getting git tracked files for ${relativeDir}...`);
99
+ const output = execSync(`git ls-files --recurse-submodules "${relativeDir}/"`, {
100
+ cwd: getRootDir(),
101
+ encoding: 'utf-8',
102
+ });
103
+
104
+ output.trim().split('\n').filter(f => f).forEach(f => allFiles.add(f));
105
+ } catch (error) {
106
+ console.warn(`Could not get git tracked files for ${projectDir}`);
107
+ }
108
+ }
109
+
110
+ return allFiles;
111
+ }