diff --git a/astro.config.mjs b/astro.config.mjs
index 2528d402..028fbc54 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,8 +1,31 @@
import { defineConfig } from "astro/config";
+// Prevent duplicate customElements.define() errors in dev mode.
+// The myst-awesome library imports Web Awesome components from many
+// separate
+
+
+
+
+
+
+
diff --git a/src/pages/browse/publication/[id].astro b/src/pages/browse/publication/[id].astro
index 36c8a4b4..f61233f2 100644
--- a/src/pages/browse/publication/[id].astro
+++ b/src/pages/browse/publication/[id].astro
@@ -27,8 +27,8 @@ export async function getStaticPaths() {
const paths = pages.map((page, index) => {
const id = page.id;
- console.log(`Processing page ${index + 1}: "${page.id}"`);
- console.log('Page contents:', page);
+ // console.log(`Processing page ${index + 1}: "${page.id}"`);
+ // console.log('Page contents:', page);
return {
params: { id },
@@ -36,10 +36,10 @@ export async function getStaticPaths() {
};
});
- console.log(`Generated ${paths.length} paths for [id].astro`);
- paths.forEach((path, i) => {
- console.log(` Path ${i + 1}: id="${path.params.id}"`);
- });
+ // console.log(`Generated ${paths.length} paths for [id].astro`);
+ // paths.forEach((path, i) => {
+ // console.log(` Path ${i + 1}: id="${path.params.id}"`);
+ // });
return paths;
} catch (error) {
@@ -101,74 +101,6 @@ interface TreeNode {
children?: TreeNode[];
}
-function buildFileTree(files: any[]): TreeNode[] {
- const root: TreeNode[] = [];
- const folderMap = new Map();
-
- files.forEach((file: any) => {
- // Remove 'root/code/' prefix
- const relativePath = file.title.replace('root/code/', '');
- const parts = relativePath.split('/');
- const fileName = parts[parts.length - 1];
-
- // Build folder structure
- let currentPath = '';
- for (let i = 0; i < parts.length - 1; i++) {
- const folderName = parts[i];
- const parentPath = currentPath;
- currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
-
- if (!folderMap.has(currentPath)) {
- const folderNode: TreeNode = {
- name: folderName,
- path: currentPath,
- type: 'folder',
- children: []
- };
- folderMap.set(currentPath, folderNode);
-
- // Add to parent or root
- if (parentPath) {
- const parent = folderMap.get(parentPath);
- parent?.children?.push(folderNode);
- } else {
- root.push(folderNode);
- }
- }
- }
-
- // Add file
- const fileNode: TreeNode = {
- name: fileName,
- path: relativePath,
- type: 'file',
- url: file.url,
- size: file.extra?.size_bytes ? formatBytes(file.extra.size_bytes) : undefined,
- id: file.id // CID from download object
- };
-
- if (parts.length === 1) {
- root.push(fileNode);
- } else {
- const parentPath = parts.slice(0, -1).join('/');
- const parent = folderMap.get(parentPath);
- parent?.children?.push(fileNode);
- }
- });
-
- return root;
-}
-
-function formatBytes(bytes: number): string {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return `${Math.round(bytes / Math.pow(k, i) * 10) / 10} ${sizes[i]}`;
-}
-
-const fileTree = buildFileTree(codeFiles);
-
// Determine medical imaging file extensions
const meshExtensions = ['.stl', '.ply', '.mz3', '.off', '.obj', '.fsa', '.fsb', '.byu', '.x3d', '.gii', '.vtp'];
const volumeExtensions = ['.nii', '.nii.gz', '.dcm', '.nrrd', '.mha', '.mhd', '.vtk', '.vti', '.aim', '.isq', '.gipl', '.gipl.gz', '.hdf5', '.iwi', '.iwi.cbor', '.iwi.cbor.zst', '.lsm', '.mnc', '.mnc.gz', '.mgh', '.mgz', '.mgh.gz', '.mrc', '.pic', '.fdf', '.swc', '.fz', '.gqi', '.qsdr', '.mih', '.mif', '.v', '.v16', '.vmr', '.head', '.tract', '.tt', '.trx', '.trk', '.tck', '.annot', '.hdr', '.img'];
@@ -262,7 +194,8 @@ function findDefaultFile(nodes: TreeNode[]): TreeNode | null {
return null;
}
-defaultFile = findDefaultFile(fileTree);
+// defaultFile is now determined client-side
+// defaultFile = findDefaultFile(fileTree);
// Function to replace email addresses with icon links
function replaceEmailsWithIcons(html: string): string {
@@ -334,14 +267,14 @@ function getFileIcon(filename: string): string {
}
---
-{/* Store tree rendering function in template section */}
-
-
-{fileTree.length > 0 && (
-
)}
@@ -538,53 +471,13 @@ function getFileIcon(filename: string): string {
- {fileTree.map((node) => {
- // Recursive rendering function defined inline
- const renderNode = (n: TreeNode): any => {
- if (n.type === 'folder') {
- return (
-
-
- {n.name}
- {n.children?.map((child) => renderNode(child))}
-
- );
- } else {
- return (
-
-
- {n.name}
- {n.size && (
-
- {n.size}
-
-
-
- )}
-
- );
- }
- };
- return renderNode(node);
- })}
+
+
+
+
+
Loading file tree...
+
@@ -706,6 +599,9 @@ function getFileIcon(filename: string): string {
import { dicomLoader } from '@niivue/dicom-loader';
import { downloadZip } from 'client-zip';
+ // File tree builder (lazy-loading)
+ import { FileTreeManager } from '../../../lib/fileTreeBuilder';
+
// Import highlight.js core and individual languages
import hljs from 'highlight.js/lib/core';
import python from 'highlight.js/lib/languages/python';
@@ -840,7 +736,7 @@ function getFileIcon(filename: string): string {
};
// Helper function to find companion file for MHD/RAW pairs
- const findCompanionFile = (filename: string, fileTreeData: any[]): string | null => {
+ const findCompanionFile = (filename: string): string | null => {
const baseNameMatch = filename.match(/^(.+)\.(mhd|raw)$/i);
if (!baseNameMatch) return null;
@@ -849,25 +745,20 @@ function getFileIcon(filename: string): string {
const companionExt = currentExt === 'mhd' ? 'raw' : 'mhd';
const companionFilename = `${baseName}.${companionExt}`;
- // Search recursively in the file tree
- const searchTree = (nodes: any[]): string | null => {
- for (const node of nodes) {
- if (node.type === 'file' && node.name.toLowerCase() === companionFilename.toLowerCase()) {
- return node.url;
- }
- if (node.children) {
- const found = searchTree(node.children);
- if (found) return found;
- }
+ // Search in flat codeFiles array
+ const codeFiles = (window as any).codeFilesData || [];
+ for (const file of codeFiles) {
+ const fileTitle = file.title || '';
+ const fileName = fileTitle.split('/').pop() || '';
+ if (fileName.toLowerCase() === companionFilename.toLowerCase()) {
+ return file.url;
}
- return null;
- };
-
- return searchTree(fileTreeData);
+ }
+ return null;
};
// Helper function to find companion file for HDR/IMG pairs
- const findHdrImgCompanion = (filename: string, fileTreeData: any[]): string | null => {
+ const findHdrImgCompanion = (filename: string): string | null => {
const baseNameMatch = filename.match(/^(.+)\.(hdr|img)$/i);
if (!baseNameMatch) return null;
@@ -876,21 +767,16 @@ function getFileIcon(filename: string): string {
const companionExt = currentExt === 'hdr' ? 'img' : 'hdr';
const companionFilename = `${baseName}.${companionExt}`;
- // Search recursively in the file tree
- const searchTree = (nodes: any[]): string | null => {
- for (const node of nodes) {
- if (node.type === 'file' && node.name.toLowerCase() === companionFilename.toLowerCase()) {
- return node.url;
- }
- if (node.children) {
- const found = searchTree(node.children);
- if (found) return found;
- }
+ // Search in flat codeFiles array
+ const codeFiles = (window as any).codeFilesData || [];
+ for (const file of codeFiles) {
+ const fileTitle = file.title || '';
+ const fileName = fileTitle.split('/').pop() || '';
+ if (fileName.toLowerCase() === companionFilename.toLowerCase()) {
+ return file.url;
}
- return null;
- };
-
- return searchTree(fileTreeData);
+ }
+ return null;
};
// Helper function to combine MHD and RAW files into MHA format
@@ -1006,7 +892,7 @@ function getFileIcon(filename: string): string {
await nv.loadDicoms([{ url, name: filename }]);
} else if (ext.endsWith('.mhd') || ext.endsWith('.raw')) {
// Check for MHD/RAW companion files and combine them
- const companionUrl = findCompanionFile(filename, (window as any).fileTreeData);
+ const companionUrl = findCompanionFile(filename);
if (companionUrl) {
// Determine which is MHD and which is RAW
const isMhd = ext.endsWith('.mhd');
@@ -1032,7 +918,7 @@ function getFileIcon(filename: string): string {
}
} else if (ext.endsWith('.hdr') || ext.endsWith('.img')) {
// Check for HDR/IMG companion files
- const companionUrl = findHdrImgCompanion(filename, (window as any).fileTreeData);
+ const companionUrl = findHdrImgCompanion(filename);
if (companionUrl) {
// Determine which is HDR and which is IMG
const isHdr = ext.endsWith('.hdr');
@@ -1201,6 +1087,15 @@ function getFileIcon(filename: string): string {
});
}
+ // File tree manager instance (lazy-loading tree builder)
+ const fileTreeManager = new FileTreeManager();
+
+ // Build file tree dynamically (only top-level items, children loaded on expand)
+ const buildFileTree = () => {
+ const codeFiles = (window as any).codeFilesData;
+ fileTreeManager.initialize(codeFiles);
+ };
+
// Handle download all files as ZIP
const downloadAllButton = document.getElementById('download-all-button') as HTMLButtonElement;
if (downloadAllButton) {
@@ -1295,14 +1190,30 @@ function getFileIcon(filename: string): string {
}
if (tabName === 'files') {
- // Load default file if available
- const defaultFileEl = document.querySelector('.selected-file') as HTMLElement;
- if (defaultFileEl && currentViewerType === 'none') {
- const url = defaultFileEl.dataset.url;
- const path = defaultFileEl.dataset.path;
- if (url && path) {
- const filename = path.split('/').pop() || 'file';
- await loadFile(url, filename);
+ // Build file tree on first visit
+ buildFileTree();
+
+ // Load first available file if nothing loaded yet
+ // Priority order matches original findDefaultFile: volume -> mesh -> image -> any
+ if (currentViewerType === 'none') {
+ const codeFiles = (window as any).codeFilesData || [];
+ if (codeFiles.length > 0) {
+ const volumeExts = (window as any).volumeExtensions || [];
+ const meshExts = (window as any).meshExtensions || [];
+ const imageExts = (window as any).imageExtensions || [];
+
+ const findByExtensions = (exts: string[]) =>
+ codeFiles.find((f: any) => exts.some((ext: string) => f.title?.toLowerCase().endsWith(ext)));
+
+ let defaultFile = findByExtensions(volumeExts);
+ if (!defaultFile) defaultFile = findByExtensions(meshExts);
+ if (!defaultFile) defaultFile = findByExtensions(imageExts);
+ if (!defaultFile) defaultFile = codeFiles[0];
+
+ if (defaultFile?.url && defaultFile?.title) {
+ const filename = defaultFile.title.split('/').pop() || 'file';
+ await loadFile(defaultFile.url, filename);
+ }
}
}
}
@@ -1721,6 +1632,31 @@ function getFileIcon(filename: string): string {
--indent-guide-width: 1px;
}
+ /* File tree loading placeholder */
+ .file-tree-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: var(--wa-space-2xl);
+ color: var(--wa-color-text-quiet);
+ }
+
+ .file-tree-loading p {
+ margin-top: var(--wa-space-m);
+ font-size: var(--wa-font-size-m);
+ }
+
+ .loading-spinner {
+ font-size: 2rem;
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+
.file-size {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-text-quiet);
diff --git a/src/pages/index.astro b/src/pages/index.astro
index a863d915..f809d34d 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -61,10 +61,10 @@ const featuredArticle = {
Learn more about our vision →