mirror of
https://github.com/prodigist/ReactiveNotes.git
synced 2026-07-22 12:30:26 +00:00
Partial clean up of code for public release, Improved modular scope import, allowing easier user addition to module imports, added README
This commit is contained in:
parent
0be88d7908
commit
0cb1867ac7
5 changed files with 383 additions and 84 deletions
308
README.md
308
README.md
|
|
@ -1,94 +1,248 @@
|
|||
# Obsidian Sample Plugin
|
||||
# ReactiveNotes - Dynamic React Components in Obsidian
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
ReactiveNotes transforms your Obsidian notes into interactive documents by enabling full React component capabilities directly within your markdown files.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
## Core Capabilities
|
||||
|
||||
## First time developing plugins?
|
||||
### 1. Interactive Components
|
||||
- Write and use React components directly in markdown
|
||||
- Full React 18 feature support including Hooks
|
||||
- Real-time component updates
|
||||
- TypeScript support
|
||||
- Hot reloading during development
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
### 2. Data Visualization
|
||||
Built-in libraries:
|
||||
- **Recharts**: Full suite of chart components
|
||||
- Line, Bar, Area, Pie charts
|
||||
- Custom tooltips and legends
|
||||
- Responsive containers
|
||||
- **Lightweight Charts**: Professional-grade financial charts
|
||||
- Candlestick charts
|
||||
- Technical indicators
|
||||
- Custom overlays
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
### 3. Canvas Manipulation
|
||||
- Direct canvas access for custom drawing
|
||||
- Real-time canvas updates
|
||||
- Image processing capabilities
|
||||
- Custom overlays and annotations
|
||||
- Interactive drawing tools
|
||||
|
||||
## Releasing new releases
|
||||
### 4. Data Processing
|
||||
- Real-time data manipulation
|
||||
- CSV/Excel file processing
|
||||
- Data transformation utilities
|
||||
- Pattern recognition
|
||||
- Statistical analysis
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
### 5. State Management
|
||||
- Persistent storage between sessions
|
||||
- State synchronization across components
|
||||
- Frontmatter integration
|
||||
- Cross-note state management
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
### 6. UI Components
|
||||
Ready-to-use components from shadcn/ui:
|
||||
- Cards
|
||||
- Tabs
|
||||
- Dialogs
|
||||
- Forms
|
||||
- Buttons
|
||||
- Alerts
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
## Integration Features
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
|
||||
## How to use
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint .\src\`
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
### 1. File System Integration
|
||||
```javascript
|
||||
// Access and manipulate files
|
||||
const fileContent = await window.fs.readFile('example.csv');
|
||||
const jsonData = await window.fs.readFile('data.json', { encoding: 'utf8' });
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
### 2. Theme Integration
|
||||
```javascript
|
||||
// Automatic theme detection
|
||||
const theme = getTheme(); // 'dark' or 'light'
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
// Theme-aware styling
|
||||
const styles = {
|
||||
background: theme === 'dark' ? '#1a1b1e' : '#ffffff',
|
||||
color: theme === 'dark' ? '#ffffff' : '#000000'
|
||||
};
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
### 3. Data Persistence
|
||||
```javascript
|
||||
// Persistent storage hook
|
||||
const [value, setValue] = useStorage('key', defaultValue);
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
// Automatic state persistence
|
||||
const [count, setCount] = useStorage('counter', 0);
|
||||
```
|
||||
|
||||
## Component Types
|
||||
|
||||
### 1. Visualization Components
|
||||
```react
|
||||
// Interactive data visualization
|
||||
const DataVisualization = () => {
|
||||
return (
|
||||
<ResponsiveContainer>
|
||||
<LineChart data={data}>
|
||||
<Line type="monotone" dataKey="value" />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Canvas Components
|
||||
```react
|
||||
// Interactive canvas usage
|
||||
const CanvasComponent = () => {
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
// Custom drawing and animations
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Network Visualizations
|
||||
```react
|
||||
// Force-directed graphs
|
||||
const NetworkGraph = () => {
|
||||
return (
|
||||
<ForceGraph
|
||||
nodes={nodes}
|
||||
links={links}
|
||||
onNodeClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Technical Features
|
||||
|
||||
### 1. React Integration
|
||||
- Full React 18 support
|
||||
- Hooks support
|
||||
- Error Boundaries
|
||||
- Suspense support
|
||||
- TypeScript enabled
|
||||
|
||||
### 2. Performance
|
||||
- Efficient re-rendering
|
||||
- Automatic cleanup
|
||||
- Memory leak prevention
|
||||
- Resource management
|
||||
- Lazy loading support
|
||||
|
||||
### 3. Development Tools
|
||||
- Hot reloading
|
||||
- Error reporting
|
||||
- Debug mode
|
||||
- Performance monitoring
|
||||
|
||||
## API Access
|
||||
|
||||
### 1. Available Libraries
|
||||
- React + Hooks
|
||||
- Recharts
|
||||
- Lightweight Charts
|
||||
- d3-force
|
||||
- Lucide icons
|
||||
- shadcn/ui components
|
||||
|
||||
### 2. Utility Functions
|
||||
- File system operations
|
||||
- Data processing
|
||||
- Theme detection
|
||||
- State persistence
|
||||
- Math operations
|
||||
|
||||
### 3. Component Registry
|
||||
- Custom component registration
|
||||
- Component sharing
|
||||
- Dynamic loading
|
||||
- Type safety
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Component Design
|
||||
```react
|
||||
const ResponsiveComponent = () => {
|
||||
// Use relative units
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
{/* Component content */}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
```react
|
||||
const StatefulComponent = () => {
|
||||
// Use persistent storage for important state
|
||||
const [data, setData] = useStorage('data-key', initialData);
|
||||
|
||||
// Use local state for UI-only state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
```react
|
||||
const SafeComponent = () => {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorDisplay />}>
|
||||
{/* Component content */}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- No external package imports (use provided libraries)
|
||||
- Limited to frontend functionality
|
||||
- Storage is note-specific through frontmatter
|
||||
- Component code must be self-contained
|
||||
|
||||
## Future Plans
|
||||
|
||||
- Additional visualization libraries
|
||||
- Enhanced state management
|
||||
- Component sharing mechanism
|
||||
- Template system
|
||||
- Enhanced debugging tools
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
||||
|
||||
## Support
|
||||
|
||||
- Report bugs through [GitHub Issues](https://github.com/YourUsername/ReactiveNotes/issues)
|
||||
- Request features in [Discussions](https://github.com/YourUsername/ReactiveNotes/discussions)
|
||||
- Join our [Discord community](your-discord-link)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details
|
||||
|
||||
---
|
||||
|
||||
<p align="center">Built with ❤️ for the Obsidian community</p>
|
||||
21
main.tsx
21
main.tsx
|
|
@ -130,6 +130,7 @@ class ReactComponentChild extends MarkdownRenderChild {
|
|||
code = code.replace(/export\s+const\s+/, 'const ');
|
||||
code = code.replace(/export\s+function\s+/, 'function ');
|
||||
code = code.replace(/export\s+class\s+/, 'class ');
|
||||
|
||||
|
||||
// Remove type annotations
|
||||
//code = code.replace(/:\s*[A-Za-z<>[\]]+/g, '');
|
||||
|
|
@ -162,19 +163,25 @@ class ReactComponentChild extends MarkdownRenderChild {
|
|||
throw new Error('No React component found');
|
||||
}
|
||||
|
||||
// Find component name and rename if it's 'Component'
|
||||
const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
|
||||
|
||||
if (componentName === 'WrappedComponent') {
|
||||
code = code.replace(/\bComponent\b/, safeName);
|
||||
}
|
||||
// Combine everything
|
||||
// added Component assignment
|
||||
code = `
|
||||
${chartWrapper}
|
||||
${code}
|
||||
const Component = (() => {
|
||||
const WrappedComponent = (() => {
|
||||
// Add error handling for component existence
|
||||
if (typeof ${componentName} === 'undefined') {
|
||||
throw new Error(\`Component "${componentName}" was matched but is undefined. Code context: ${code.slice(0, 100)}...\`);
|
||||
if (typeof ${safeName} === 'undefined') {
|
||||
throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100)}...\`);
|
||||
}
|
||||
const isChartComponent = ${componentName}.toString().includes('ResponsiveContainer') ||
|
||||
${componentName}.toString().includes('svg');
|
||||
return isChartComponent ? withChartContainer(${componentName}) : ${componentName};
|
||||
const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
|
||||
${safeName}.toString().includes('svg');
|
||||
return isChartComponent ? withChartContainer(${safeName}) : ${safeName};
|
||||
})();
|
||||
`;
|
||||
|
||||
|
|
@ -182,7 +189,7 @@ class ReactComponentChild extends MarkdownRenderChild {
|
|||
return `
|
||||
(async () => {
|
||||
${code}
|
||||
return Component;
|
||||
return WrappedComponent;
|
||||
})()
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
133
src/components/componentRegistry.tsx
Normal file
133
src/components/componentRegistry.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// src/components/ComponentRenderer.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { transform } from '@babel/standalone';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { App } from 'obsidian';
|
||||
import { useStorage } from '../hooks/useStorage';
|
||||
import { ComponentRegistry } from '../core/registry';
|
||||
import { StorageManager } from '../core/storage';
|
||||
|
||||
interface ComponentRendererProps {
|
||||
code: string;
|
||||
scopeId: string;
|
||||
inline?: boolean;
|
||||
storage: StorageManager; // Add storage prop
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TransformError {
|
||||
message: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}
|
||||
|
||||
const ErrorDisplay: React.FC<{ error: TransformError }> = ({ error }) => {
|
||||
return (
|
||||
<div className="react-component-error">
|
||||
<p>Error in component:</p>
|
||||
<pre className="error-message">
|
||||
{error.message}
|
||||
{error.line && error.column &&
|
||||
`\nAt line ${error.line}, column ${error.column}`
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComponentRenderer: React.FC<ComponentRendererProps> = ({
|
||||
code,
|
||||
scopeId,
|
||||
storage, // Get storage from props
|
||||
inline = false
|
||||
}) => {
|
||||
const [Component, setComponent] = useState<React.ComponentType | null>(null);
|
||||
const [error, setError] = useState<TransformError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const createComponent = async () => {
|
||||
try {
|
||||
const transformedCode = transform(code, {
|
||||
presets: ['env','react'],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-jsx', // Add JSX syntax plugin explicitly
|
||||
'@babel/plugin-transform-react-jsx', // Transforms JSX into JS
|
||||
'@babel/plugin-proposal-class-properties', // Handles class properties
|
||||
'@babel/plugin-proposal-object-rest-spread', // Handles object spread syntax
|
||||
],
|
||||
filename: `component-${scopeId}`,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
babelrc: false
|
||||
}).code;
|
||||
console.log('Transformed Code:', transformedCode);
|
||||
// Create storage hook bound to this storage instance
|
||||
const boundUseStorage = <T,>(key: string, defaultValue: T) =>
|
||||
useStorage(key, defaultValue, storage);
|
||||
|
||||
// Enhanced scope with registry and storage
|
||||
const scope = {
|
||||
React,
|
||||
useState: React.useState,
|
||||
useEffect: React.useEffect,
|
||||
useRef: React.useRef,
|
||||
useStorage: boundUseStorage, // Add bound storage hook
|
||||
...ComponentRegistry,
|
||||
};
|
||||
|
||||
const componentFn = new Function(
|
||||
...Object.keys(scope),
|
||||
`try {
|
||||
${transformedCode}
|
||||
return Component;
|
||||
} catch (err) {
|
||||
console.error('Error in component execution:', err);
|
||||
throw err;
|
||||
}`
|
||||
);
|
||||
|
||||
const ComponentType = componentFn(...Object.values(scope));
|
||||
setComponent(() => ComponentType);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Component transformation failed:', err);
|
||||
setError({
|
||||
message: err instanceof Error ? err.message : 'Unknown error',
|
||||
line: (err as any).loc?.line,
|
||||
column: (err as any).loc?.column
|
||||
});
|
||||
setComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
createComponent();
|
||||
}, [code, scopeId, storage]); // Add storage to dependencies
|
||||
|
||||
const Wrapper: React.FC<{ children: React.ReactNode }> =
|
||||
inline ? ({ children }) => (
|
||||
<span className="inline-component">{children}</span>
|
||||
) : ({ children }) => (
|
||||
<div className="block-component">{children}</div>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay error={error} />;
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div
|
||||
className="component-sandbox"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Component />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
5
src/components/ui/index.tsx
Normal file
5
src/components/ui/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// src/components/ui/index.ts
|
||||
export { Card, CardHeader, CardTitle, CardContent } from './card';
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
|
||||
export { Switch } from './switch';
|
||||
// Add other component exports as needed
|
||||
Loading…
Reference in a new issue