diff --git a/README.md b/README.md
index 4688d98..ebda8ec 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ export default Greeting;
**Component Requirements:**
- Must include a default export
- Must be self-contained in one code block
-- Component name should match default export
+- Component ro Render must be the first component within codeblock
### Component Structure
@@ -120,12 +120,30 @@ import * as XLSX from 'xlsx'; // Excel files
import * as mathjs from 'mathjs'; // Mathematics
import { format, parseISO } from 'date-fns'; // Date handling
-// Interface components
-import { Card, Tabs, Switch } from '@/components/ui';
-import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
+ // Interface components
+ import { Card, Tabs, Switch } from '@/components/ui';
+ import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
```
-See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
-Even if needed libraries are not provided within this list CDN imports are always an option.
+ See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
+ Even if needed libraries are not provided within this list CDN imports are always an option.
+
+ ### Available in Component Scope
+
+ All components have access to these objects and functions:
+
+ ```javascript
+ // Note context
+ noteContext.frontmatter // All frontmatter properties (not just react_data)
+ noteContext.path // Full path to current note
+ noteContext.basename // Note filename without extension
+
+ // Utilities
+ getTheme() // Returns 'dark' or 'light'
+ readFile(path, exts) // File reading utility
+ getFrontmatter(key?,defaultValue?,notePath?) // If properties are null returns entire frontmatter or current note if path not provided
+ vaultImport(path, index) // Preprocessor directive to include code from other notes (not a runtime function)
+ ```
+
## 💻 Core Features
@@ -299,6 +317,47 @@ const styles = {
// Or use 'dark:' within your components
```
+### 8. Vault Code Imports
+
+Import React code from other notes in your vault:
+
+```javascript
+// Import code from another note
+vaultImport('Components/SharedComponents.md', 0); // 0 = first code block
+vaultImport('Hooks/CustomHooks.md', 1); // 1 = second code block
+
+const MyComponent = () => {
+ // Use imported components and hooks from these files
+ const data = useImportedHook();
+ return ;
+};
+```
+
+
+### 9. Definition Storage Blocks
+
+Not all react code blocks need to be valid React you can use React code blocks to store and organize utility functions, hooks, and other definitions:
+
+```javascript
+// This displays as a definition storage block instead of rendering
+const utilities = () => ({
+ formatDate: (date) => date.toLocaleDateString(),
+ calculateSum: (arr) => arr.reduce((a, b) => a + b, 0),
+ parseData: (raw) => JSON.parse(raw)
+});
+
+export default utilities;
+```
+
+When a code block returns an object instead of JSX, it automatically displays as a storage block showing all exported definitions. If you have multiple objects containing definitions only the first object will be previewed.
+Perfect for:
+
+Utility function collections
+Custom hook libraries
+Configuration objects
+Shared constants
+
+Import these definitions into other components using vaultImport().
## 🎨 Component Examples
@@ -426,6 +485,9 @@ Knowledge progression dashboard using persistant storage and ability to make new

+**Games utilising event listeners for key action mapping**
+
+
## 🛠️Development Guide
### Component Structure
```react
diff --git a/assets/codeStorageBox.gif b/assets/codeStorageBox.gif
new file mode 100644
index 0000000..dfb2d5b
Binary files /dev/null and b/assets/codeStorageBox.gif differ
diff --git a/main.tsx b/main.tsx
index 20fea6b..2730664 100644
--- a/main.tsx
+++ b/main.tsx
@@ -19,7 +19,7 @@ class ReactComponentChild extends MarkdownRenderChild {
private noteFile: TFile;
private ctx: MarkdownPostProcessorContext;
private app: App;
-
+ private processedPaths = new Set();
constructor(containerEl: HTMLElement, plugin: Plugin, ctx: MarkdownPostProcessorContext) {
super(containerEl);
@@ -50,13 +50,41 @@ class ReactComponentChild extends MarkdownRenderChild {
}
- private async getFrontmatterData(key: string, defaultValue: T): Promise {
- if (!this.noteFile) return defaultValue;
-
- const cache = this.app.metadataCache.getFileCache(this.noteFile);
- const frontmatter = cache?.frontmatter;
- return frontmatter?.react_data?.[key] ?? defaultValue;
- }
+ private async getFrontmatter(
+ key: string | null = null,
+ defaultValue: T | null = null,
+ notePath: string | null = null
+ ): Promise | null> {
+ try {
+ // Get the file - either current note or specified path
+ const file = notePath
+ ? this.app.vault.getAbstractFileByPath(notePath)
+ : this.noteFile;
+
+ if (!file || !(file instanceof TFile)) {
+ console.error(`Invalid file: ${notePath}`);
+ return defaultValue;
+ }
+
+ const cache = this.app.metadataCache.getFileCache(file);
+ const frontmatter = cache?.frontmatter;
+
+ if (!frontmatter) {
+ return defaultValue;
+ }
+
+ // Return specific key if requested
+ if (key) {
+ return frontmatter[key] ?? defaultValue;
+ }
+
+ // Otherwise return entire frontmatter
+ return frontmatter;
+ } catch (error) {
+ console.error('Error getting frontmatter:', error);
+ return defaultValue;
+ }
+ }
private async updateFrontmatterData(key: string, value: T): Promise {
if (!this.noteFile) return;
@@ -77,24 +105,63 @@ class ReactComponentChild extends MarkdownRenderChild {
throw error;
}
}
-
- // Add wrapper component for error boundary
private RenderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ React.useEffect(() => {
+ // Capture the original console.error when the component mounts
+ const originalConsoleError = console.error;
+
+ // Override console.error
+ console.error = (...args: any[]) => {
+ const errorMessage = args[0]?.toString() || '';
+ // Check if the error message is the one we want to suppress
+ if (errorMessage.includes('Objects are not valid as a React child')) {
+ // Suppress this specific error by not calling the original console.error
+ return;
+ }
+ // For all other errors, call the original console.error
+ originalConsoleError.apply(console, args);
+ };
+
+ // Cleanup function: Restore the original console.error when the component unmounts
+ return () => {
+ console.error = originalConsoleError;
+ };
+ }, []); // Empty dependency array ensures this effect runs only once on mount and cleanup on unmount.
+
return (
(
-
-
Error in component:
-
- {error.message}
-
-
- )}
+ fallback={({ error }) => {
+ // The ErrorBoundary will catch the error and display a fallback UI.
+ // The console log for "Objects are not valid as a React child" should have been suppressed.
+ if (error.message?.includes('Objects are not valid as a React child')) {
+ const objectMatch = error.message.match(/found: object with keys {([^}]+)}/);
+ const keys = objectMatch ? objectMatch[1].split(',').map(k => k.trim()) : ['unknown'];
+ return (
+
+
+ 🧩Component Rendered an Object
+
+
A React component attempted to render a plain JavaScript object. Objects need to be mapped to renderable elements.
+
Object keys found:
+
{keys.map(key =>
{key}
)}
+
+ );
+ }
+ // Default error display for other types of errors
+ return (
+
+
Error in component:
+
{error.message}
+ {error.stack && Stack Trace
{error.stack}
}
+
+ );
+ }}
>
{children}
);
};
+
private needsThree(code: string): boolean {
// Check if the code contains any Three.js specific imports or usage
// More comprehensive detection of THREE usage
@@ -123,7 +190,7 @@ class ReactComponentChild extends MarkdownRenderChild {
return hasThreeImports || hasThreeUsage || commonThreeClasses;
}
- private preprocessCode(code: string): string {
+ private async preprocessCode(code: string): Promise {
// Replace CDN imports with script loading
code = code.replace(
@@ -138,49 +205,58 @@ class ReactComponentChild extends MarkdownRenderChild {
code = code.replace(/import\s*\([^)]*\);?\s*$/gm, '');
// Handle both JS/TS exports
- code = code.replace(/export\s+default\s+/, '');
- code = code.replace(/export\s+const\s+/, 'const ');
- code = code.replace(/export\s+function\s+/, 'function ');
- code = code.replace(/export\s+class\s+/, 'class ');
+ code = code.replace(/export\s+default\s+/gm, '');
+ code = code.replace(/export\s+const\s+/gm, 'const ');
+ code = code.replace(/export\s+function\s+/gm, 'function ');
+ code = code.replace(/export\s+class\s+/gm, 'class ');
// Remove type annotations
//code = code.replace(/:\s*[A-Za-z<>[\]]+/g, '');
//code = code.replace(/<[A-Za-z,\s]+>/g, '');
- // Create a HOC wrapper for chart components
- const chartWrapper = `
- const withChartContainer = (WrappedComponent) => {
- return function ChartContainer(props) {
- const [mounted, setMounted] = React.useState(false);
-
- React.useEffect(() => {
- const timer = setTimeout(() => setMounted(true), 100);
- return () => clearTimeout(timer);
- }, []);
-
- return React.createElement(
- 'div',
- { style: { width: '100%', minHeight: '400px' } },
- mounted ? React.createElement(WrappedComponent, props) : null
- );
- };
- };
- `;
+
// Find the component name
- const componentMatch = code.match(/(?:function\s+(\w+)|(?:const|class)\s+(\w+)\s*=\s*(?:(?:\([^)]*\)|)\s*=>|function\s*\(|React\.memo\(|React\.forwardRef\(|class\s+extends\s+React\.Component))/);
- const componentName = componentMatch ? (componentMatch[1] || componentMatch[2]) : 'EmptyComponent';
- console.log('Found component:', componentName); // Debug info
- if (!componentMatch) {
- throw new Error('No React component found');
- }
+ const componentMatch = code.match(/^(?![\s]*\/\/).*?(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:(?:\([^)]*\)|)\s*=>|function\s*\(|React\.memo\(|React\.forwardRef\(|class\s+extends\s+React\.Component))/m);
+ const storeMatch = !componentMatch ? code.match(/(?:const\s+(\w+)\s*=\s*{|class\s+(\w+)\s*{)/) : null;
- // Find component name and rename if it's 'Component'
- const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
+ if (!componentMatch) {
+ // If no component found, look for object literals or other exports
+ if (!storeMatch) {
+ throw new Error('No React component found');
+ }
+ }
+ // Set safeName using either match
+ const componentName = componentMatch
+ ? (componentMatch[1] || componentMatch[2])
+ : (storeMatch ? (storeMatch[1]|| storeMatch[2]) : 'EmptyComponent');
+ console.log('Found component:', componentName);
+
+ // Find component name and rename if it's 'WrappedComponent'
+ const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
- if (componentName === 'WrappedComponent') {
- code = code.replace(/\bComponent\b/, safeName);
- }
+ if (componentName === 'WrappedComponent') {
+ code = code.replace(/\bWrappedComponent\b/, safeName);
+ }
+ // Create a HOC wrapper for chart components
+ const chartWrapper = `
+ const withChartContainer = (WrappedComponent) => {
+ return function ChartContainer(props) {
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ const timer = setTimeout(() => setMounted(true), 100);
+ return () => clearTimeout(timer);
+ }, []);
+
+ return React.createElement(
+ 'div',
+ { style: { width: '100%', minHeight: '400px' } },
+ mounted ? React.createElement(WrappedComponent, props) : null
+ );
+ };
+ };
+ `;
// Combine everything
// added Component assignment
code = `
@@ -189,14 +265,122 @@ class ReactComponentChild extends MarkdownRenderChild {
const WrappedComponent = (() => {
// Add error handling for component existence
if (typeof ${safeName} === 'undefined') {
- throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100)}...\`);
+ throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100).replace(/`/g, '\\`')}...\`);
}
- const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
+ // Simple check - is it a valid React component?
+ const isValidComponent = (() => {
+ // Class component
+ if (${safeName}.prototype?.isReactComponent) return true;
+
+ // Function that returns React elements
+ if (typeof ${safeName} === 'function') {
+ try {
+ const result = ${safeName}({});
+ return React.isValidElement(result);
+ } catch {
+ // If it throws, check source code
+ return ${safeName}.toString().includes('createElement') ||
+ ${safeName}.toString().includes('<');
+ }
+ }
+
+ // Direct objects are never valid components
+ return false;
+ })();
+
+ // If not a valid component, treat as a storage block
+ if (!isValidComponent) {
+ return function StorageBlockWrapper() {
+ // Get the definitions - handle all cases
+ let definitions;
+
+ if (typeof ${safeName} === 'function') {
+ try {
+ // Try as function
+ definitions = ${safeName}({});
+ } catch (e) {
+ try {
+ // Try as class
+ definitions = new ${safeName}();
+ } catch (e2) {
+ // Last resort
+ definitions = ${safeName};
+ }
+ }
+ } else {
+ // Already an object (like IIFE or object literal)
+ definitions = ${safeName};
+ }
+
+ // Ensure it's an object
+ if (!definitions || typeof definitions !== 'object') {
+ definitions = { error: "Couldn't extract definitions" };
+ }
+
+ const keys = Object.keys(definitions);
+ return React.createElement('div', {
+ style: {
+ padding: '12px',
+ backgroundColor: 'var(--background-modifier-form-field)',
+ borderRadius: '6px',
+ border: '1px solid var(--background-modifier-border)',
+ fontFamily: 'var(--font-monospace)'
+ }
+ }, [
+ React.createElement('div', {
+ style: {
+ fontWeight: 'bold',
+ marginBottom: '8px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px'
+ }
+ }, [
+ React.createElement('span', null, '📦'),
+ React.createElement('span', null, 'Definitions Storage Container:')
+ ]),
+ React.createElement('div', {
+ style: {
+ padding: '8px',
+ marginBottom: '12px',
+ color: 'var(--text-on-accent)',
+ borderRadius: '4px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px'
+ }
+ }, [
+ React.createElement('code', {
+ style: {
+ padding: '2px 6px',
+ backgroundColor: 'rgba(255,255,255,0.2)',
+ borderRadius: '3px',
+ fontWeight: 'bold'
+ }
+ }, '${safeName}')
+ ]),
+ React.createElement('div', {
+ style: { fontSize: '0.9em', opacity: 0.8 }
+ }, 'Exported definitions:'),
+ React.createElement('ul', {
+ style: {
+ margin: '8px 0',
+ paddingLeft: '20px'
+ }
+ }, keys.map(key =>
+ React.createElement('li', { key },
+ \`\${key}: \${typeof definitions[key]}\`
+ )
+ ))
+ ]);
+ };
+ }
+ const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
${safeName}.toString().includes('svg');
- return isChartComponent ? withChartContainer(${safeName}) : ${safeName};
+ return isChartComponent ? withChartContainer(${safeName}) : ${safeName};
})();
`;
-
+ code = await this.processVaultImports(code);
// Wrap code in async IIFE to allow for await
return `
(async () => {
@@ -222,6 +406,80 @@ private getThreeJs() {
}
}
+private async processVaultImports(code: string, depth = 0): Promise {
+ if (depth > 10) {
+ console.warn('Maximum import depth reached (10)');
+ return code;
+ }
+ const vaultImportRegex = /vaultImport\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(\d+))?\s*\)/g;
+
+ // Create a map to track processed imports (avoid duplicates)
+ const processedImports = new Map();
+
+ // Find all vaultImport calls
+ const matches = [...code.matchAll(vaultImportRegex)];
+
+ for (const match of matches) {
+ const [fullMatch, path, index = '0'] = match;
+ const importKey = `${path}#${index}`;
+
+ // Skip if already processed
+ if (processedImports.has(importKey)) continue;
+
+ try {
+ const file = this.app.vault.getAbstractFileByPath(path);
+ if (!file || !(file instanceof TFile)) continue;
+
+ const content = await this.app.vault.read(file);
+ const blocks = content.match(/```react\n([\s\S]*?)```/g) || [];
+
+ if (blocks[parseInt(index)]) {
+ let blockCode = blocks[parseInt(index)]
+ .replace(/```react\n/, '')
+ .replace(/```$/, '');
+
+ // Check for nested imports
+ const importKey = `${path}#${index}`;
+ if (!this.processedPaths.has(importKey)) {
+ this.processedPaths.add(importKey);
+ // Recursively process nested imports
+ blockCode = await this.processVaultImports(blockCode, depth + 1);
+ }
+ blockCode = blockCode.replace(
+ /import\s+(\w+)\s+from\s+['"]https:\/\/cdnjs\.cloudflare\.com\/([^'"]+)['"]/g,
+ (match, importName, cdnPath) => `
+ const script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/${cdnPath}';
+ document.body.appendChild(script);`);
+ blockCode = blockCode.replace(/import\s+.*?['"]\s*;?\s*$/gm, '')
+ .replace(/import\s*{[^}]*}\s*from\s*['"][^'"]*['"];?\s*$/gm, '')
+ .replace(/import\s*\([^)]*\);?\s*$/gm, '');
+
+ // Strip exports
+ blockCode = blockCode
+ .replace(/export\s+default\s+/gm, '')
+ .replace(/export\s+const\s+/gm, 'const ')
+ .replace(/export\s+function\s+/gm, 'function ')
+ .replace(/export\s+class\s+/gm, 'class ')
+ .replace(/export\s+{[^}]*}\s*;?/gm, '')
+
+
+ const importedCode = `\n// Imported from ${path}#${index}\n${blockCode}\n`;
+ processedImports.set(importKey, importedCode);
+ }
+ } catch (error) {
+ console.error(`Error importing from ${path}:`, error);
+ }
+ }
+
+ // Append all unique imports
+ const appendedCode = Array.from(processedImports.values()).join('\n');
+
+ // Remove ALL vaultImport calls in one pass
+ const cleanedCode = code.replace(vaultImportRegex, '');
+ return appendedCode+cleanedCode;
+}
+
async render(code: string) {
console.time('component-render');
try {
@@ -241,10 +499,10 @@ private getThreeJs() {
console.time('preprocess');
// Preprocess code
const needsThree=this.needsThree(code);
- const processedCode = this.preprocessCode(code);
- //console.log('Processed code:', processedCode);
+ const processedCode = await this.preprocessCode(code);
console.timeEnd('preprocess');
+
console.time('tailwind');
// Process Tailwind
//Could potentially generate tailwind css based on specific user code using build:css script defined in config
@@ -268,14 +526,12 @@ private getThreeJs() {
}).code;
console.timeEnd('babel');
//console.log('Transformed code:', transformedCode);
-
- // Create bound storage hook
// Create bound storage hook with frontmatter support
const boundUseStorage = (key: string, defaultValue: T) => {
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
- this.getFrontmatterData(key, defaultValue).then(setValue);
+ this.getFrontmatter(key, defaultValue).then(setValue);
}, [key]);
const updateValue = React.useCallback(
@@ -312,99 +568,13 @@ private getThreeJs() {
}),
//lazy load THREE if needed
...needsThree ? this.getThreeJs() : undefined,
-readFile: async (path=null, extensions = ['txt', 'md', 'json','csv']) => {
- return new Promise((resolve) => {
- let isResolved = false;
- let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation
- // Create a SuggestModal subclass that lists vault files
-
- // If a specific path is provided, read that file directly without showing modal
- if (path) {
- const file = this.app.vault.getAbstractFileByPath(path);
- if (file && file instanceof TFile) {
- this.app.vault.cachedRead(file).then(content => {
- isResolved = true;
- resolve({
- path: file.path,
- name: file.name,
- content: content
- });
- }).catch(err => {
- console.error("Failed to read file:", err);
- isResolved = true;
- resolve(null);
- });
- } else {
- console.error("File not found:", path);
- isResolved = true;
- resolve(null);
- }
- return;
- }
- class FileSuggestModal extends SuggestModal {
- getSuggestions(query:string) {
- // Get all files with the right extension
- const files = this.app.vault.getFiles()
- .filter(file => extensions.includes(file.extension));
-
- // Filter by query if provided
- if (query) {
- return files.filter(file =>
- file.path.toLowerCase().includes(query.toLowerCase())
- );
- }
- return files;
- }
-
- renderSuggestion(file:TFile, el: HTMLElement) {
- el.createEl("div", { text: file.name });
- el.createEl("small", { text: file.path });
- }
-
- onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
- console.log("File content:");
- if (cancelTimeoutHandle !== undefined) {
- clearTimeout(cancelTimeoutHandle);
- cancelTimeoutHandle = undefined;
- }
- this.app.vault.cachedRead(file).then(content => {isResolved = true;
- resolve({
- path: file.path,
- name: file.basename,
- extension: file.extension,
- content: content
- });
- }).catch(err => {
- console.error("Failed to read file:", err);
- resolve(null);});}
- }
-
- // Show the modal
- const modal = new FileSuggestModal(this.app);
- modal.setPlaceholder("Select a file");
- modal.onClose = () => {
- // If onClose is called, and a suggestion hasn't ALREADY been chosen and processed,
- // this implies a cancellation or an edge case where onClose fires very quickly.
- // We use a setTimeout to yield to the event loop, giving onChooseSuggestion
- // a chance to run if it was triggered by the same user action that closed the modal.
- if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close)
- clearTimeout(cancelTimeoutHandle);
- }
- cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type
- // After yielding, if the promise hasn't been resolved by onChooseSuggestion
- // and a suggestion wasn't flagged as chosen, then resolve as null (cancellation).
- if (!isResolved) {
- resolve(null);
- }
- // If suggestionChosen IS true here, it means onChooseSuggestion ran,
- // set the flag, and it's responsible for resolving (or has already).
- // If promiseResolved IS true, it's already handled.
- }, 0); // Yield to the event loop.
- };
- modal.open();
-
- });
- }
+ readFile: async (path = null, extensions = ['txt', 'md', 'json', 'csv']) => {
+ return await this.readFile(this.app, path, extensions);
+ },
+ // Frontmatter access utility
+ getFrontmatter: async (notePath = null, key = null, defaultValue = null) => {
+ return await this.getFrontmatter(notePath, key, defaultValue);
+ },
};
// Execute the code and get the component
@@ -426,6 +596,102 @@ readFile: async (path=null, extensions = ['txt', 'md', 'json','csv']) => {
console.timeEnd('component-render');
}
+ async readFile(
+ app: App,
+ path: string | null = null,
+ extensions: string[] = ['txt', 'md', 'json', 'csv']
+ ): Promise<{ path: string; name: string; extension?: string; content: string } | null> {
+ return new Promise((resolve) => {
+ let isResolved = false;
+ let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation
+ // Create a SuggestModal subclass that lists vault files
+
+ // If a specific path is provided, read that file directly without showing modal
+ if (path) {
+ const file = this.app.vault.getAbstractFileByPath(path);
+ if (file && file instanceof TFile) {
+ this.app.vault.cachedRead(file).then(content => {
+ isResolved = true;
+ resolve({
+ path: file.path,
+ name: file.name,
+ content: content
+ });
+ }).catch(err => {
+ console.error("Failed to read file:", err);
+ isResolved = true;
+ resolve(null);
+ });
+ } else {
+ console.error("File not found:", path);
+ isResolved = true;
+ resolve(null);
+ }
+ return;
+ }
+ class FileSuggestModal extends SuggestModal {
+ getSuggestions(query:string) {
+ // Get all files with the right extension
+ const files = this.app.vault.getFiles()
+ .filter(file => extensions.includes(file.extension));
+
+ // Filter by query if provided
+ if (query) {
+ return files.filter(file =>
+ file.path.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+ return files;
+ }
+
+ renderSuggestion(file:TFile, el: HTMLElement) {
+ el.createEl("div", { text: file.name });
+ el.createEl("small", { text: file.path });
+ }
+
+ onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
+ console.log("File content:");
+ if (cancelTimeoutHandle !== undefined) {
+ clearTimeout(cancelTimeoutHandle);
+ cancelTimeoutHandle = undefined;
+ }
+ this.app.vault.cachedRead(file).then(content => {isResolved = true;
+ resolve({
+ path: file.path,
+ name: file.basename,
+ extension: file.extension,
+ content: content
+ });
+ }).catch(err => {
+ console.error("Failed to read file:", err);
+ resolve(null);});}
+ }
+
+ // Show the modal
+ const modal = new FileSuggestModal(this.app);
+ modal.setPlaceholder("Select a file");
+ modal.onClose = () => {
+ // We use a setTimeout to yield to the event loop, giving onChooseSuggestion
+ // a chance to run if it was triggered by the same user action that closed the modal.
+ if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close)
+ clearTimeout(cancelTimeoutHandle);
+ }
+ cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type
+ // After yielding, if the promise hasn't been resolved by onChooseSuggestion
+ // and a suggestion wasn't flagged as chosen, then resolve as null (cancellation).
+ if (!isResolved) {
+ resolve(null);
+ }
+ // If suggestionChosen IS true here, it means onChooseSuggestion ran,
+ // set the flag, and it's responsible for resolving (or has already).
+ // If promiseResolved IS true, it's already handled.
+ }, 0); // Yield to the event loop.
+ };
+ modal.open();
+
+ });
+
+ }
onunload() {
this.root.unmount();
diff --git a/manifest.json b/manifest.json
index f7032fd..bdaf821 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "reactive-notes",
"name": "Reactive Notes",
- "version": "1.0.2",
+ "version": "1.0.3",
"minAppVersion": "1.4.0",
"description": "Transform your vault into a reactive computational environment. Create dynamic React components directly in your notes.",
"author": "Elias Noesis",
@@ -10,4 +10,4 @@
"fundingUrl": {
"Buy Me A Coffee": "https://www.buymeacoffee.com/prodigist"
}
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 24b7c0f..67d69f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reactive-notes",
- "version": "1.0.2",
+ "version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reactive-notes",
- "version": "1.0.1",
+ "version": "1.0.3",
"dependencies": {
"@codemirror/language": "^6.10.6",
"@emotion/css": "^11.13.5",
diff --git a/package.json b/package.json
index 05fcb7b..16d4777 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "reactive-notes",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "Transform your vault into a reactive computational environment with React components.",
"main": "main.js",
"scripts": {
diff --git a/src/components/componentRegistry.tsx b/src/components/componentRegistry.tsx
index a41d54e..feac808 100644
--- a/src/components/componentRegistry.tsx
+++ b/src/components/componentRegistry.tsx
@@ -15,7 +15,7 @@ import {
Upload, Activity, AlertCircle, TrendingUp, TrendingDown, Home, BarChart2, BarChart3, Code, Settings, Moon, Sun, ChevronLeft, ChevronRight, Heart, ExternalLink,
Play, Image as ImageIcon, Check, X, Plus, Minus, ArrowRightCircle, ArrowLeftCircle, ArrowUpCircle, ArrowDownCircle,
ChevronUp, ChevronDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, PlusCircle, MinusCircle, XCircle, CheckCircle,
- Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle,
+ Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle, Clock, Calendar,
} from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import {
@@ -99,7 +99,8 @@ RadialBarChart, RadialBar,
ExternalLink, BarChart2,
Play, ImageIcon, Check, X, Plus, Minus, ArrowRightCircle, ArrowLeftCircle, ArrowUpCircle, ArrowDownCircle,
ChevronUp, ChevronDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, PlusCircle, MinusCircle, XCircle, CheckCircle,
- Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle,
+ Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle, Clock, Calendar,
+ LucideIcons,
//data processing utilities
dateFns: { format, addDays, differenceInDays, parseISO },
diff --git a/versions.json b/versions.json
index 8a45d43..0e53289 100644
--- a/versions.json
+++ b/versions.json
@@ -1,3 +1,4 @@
{
- "1.0.2": "1.4.0"
-}
+ "1.0.2": "1.4.0",
+ "1.0.3": "1.4.0"
+}
\ No newline at end of file