mirror of
https://github.com/banisterious/obsidian-charted-roots.git
synced 2026-07-22 06:40:24 +00:00
Set up Canvas Roots plugin foundation
Establish complete project structure for Obsidian plugin development: - TypeScript build system with esbuild and ESLint configuration - Component-based CSS architecture with automated build pipeline - Data models for Person and Canvas structures - Plugin settings interface and command structure - Deployment scripts for development workflow Documentation: - Comprehensive contributing guide and security policy - Bases integration guide with example template - Development workflow and CSS system documentation - ESLint configuration details Project follows Obsidian plugin standards with proper manifest, versioning, and module configuration.
This commit is contained in:
parent
f4b8685ae6
commit
6997c8f80e
40 changed files with 11841 additions and 3 deletions
42
.eslintrc.json
Normal file
42
.eslintrc.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2022
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"args": "none",
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"no-console": "off",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"main.js",
|
||||
"*.config.mjs",
|
||||
"node_modules/**"
|
||||
]
|
||||
}
|
||||
62
.gitignore
vendored
Normal file
62
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Build output
|
||||
main.js
|
||||
main.js.map
|
||||
*.js.map
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*:Zone.Identifier
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# External documentation and reference libraries
|
||||
docs/developer/obsidian-developer-docs/
|
||||
docs/developer/obsidian-help-docs/
|
||||
docs/developer/family-chart/
|
||||
|
||||
# Claude Code working files
|
||||
CLAUDE.md
|
||||
78
.stylelintrc.json
Normal file
78
.stylelintrc.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-prettier"
|
||||
],
|
||||
"rules": {
|
||||
"custom-property-pattern": "^(md|cr)-[a-z0-9-]+$",
|
||||
"selector-class-pattern": "^(cr|canvas-roots)-[a-z0-9-]+(__[a-z0-9-]+)?(--[a-z0-9-]+)?$",
|
||||
"declaration-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["after-declaration", "first-nested"],
|
||||
"ignore": ["after-comment", "inside-single-line-block"]
|
||||
}
|
||||
],
|
||||
"comment-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["stylelint-commands"]
|
||||
}
|
||||
],
|
||||
"rule-empty-line-before": [
|
||||
"always-multi-line",
|
||||
{
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"at-rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["blockless-after-same-name-blockless", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"max-nesting-depth": 3,
|
||||
"no-descending-specificity": null,
|
||||
"font-family-no-missing-generic-family-keyword": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"selector-no-vendor-prefix": null,
|
||||
"media-feature-name-no-vendor-prefix": null,
|
||||
"at-rule-no-vendor-prefix": null,
|
||||
"length-zero-no-unit": true,
|
||||
"color-hex-length": "short",
|
||||
"color-hex-case": "lower",
|
||||
"string-quotes": "double",
|
||||
"unit-case": "lower",
|
||||
"value-keyword-case": "lower",
|
||||
"function-name-case": "lower",
|
||||
"property-case": "lower",
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-type-case": "lower",
|
||||
"at-rule-name-case": "lower",
|
||||
"media-feature-name-case": "lower"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["styles/variables.css"],
|
||||
"rules": {
|
||||
"custom-property-pattern": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["styles/theme.css"],
|
||||
"rules": {
|
||||
"selector-class-pattern": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"styles.css",
|
||||
"**/*.min.css",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
394
CONTRIBUTING.md
Normal file
394
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
# Contributing to Canvas Roots
|
||||
|
||||
Thank you for your interest in contributing to Canvas Roots! This document provides guidelines and information for contributors.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Testing](#testing)
|
||||
- [Documentation](#documentation)
|
||||
- [Submitting Changes](#submitting-changes)
|
||||
- [Security](#security)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows a code of conduct to ensure a welcoming and inclusive environment:
|
||||
|
||||
- Be respectful and considerate
|
||||
- Welcome newcomers and help them get started
|
||||
- Focus on constructive feedback
|
||||
- Respect differing viewpoints and experiences
|
||||
- Accept responsibility for mistakes and learn from them
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v16 or higher)
|
||||
- npm (v7 or higher)
|
||||
- Obsidian (latest version recommended)
|
||||
- Git
|
||||
- A code editor (VS Code recommended)
|
||||
|
||||
### Understanding the Project
|
||||
|
||||
Before contributing, familiarize yourself with:
|
||||
|
||||
1. **Project Goals**: Read [README.md](README.md) and [docs/canvas-roots-initial-spec.md](docs/canvas-roots-initial-spec.md)
|
||||
2. **Documentation Style**: Review [docs/assets/templates/documentation-style-guide.md](docs/assets/templates/documentation-style-guide.md)
|
||||
3. **Security Considerations**: Read [SECURITY.md](SECURITY.md) - this plugin handles sensitive PII
|
||||
4. **Development Guide**: See [docs/development.md](docs/development.md)
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. **Fork and Clone**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/obsidian-canvas-roots.git
|
||||
cd obsidian-canvas-roots
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Build the Plugin**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. **Set Up Test Vault**
|
||||
- Create a test Obsidian vault for development
|
||||
- Update the vault path in `deploy.sh` and `dev-deploy.sh`
|
||||
- Deploy: `npm run deploy`
|
||||
|
||||
5. **Enable in Obsidian**
|
||||
- Open Obsidian
|
||||
- Go to Settings → Community plugins
|
||||
- Enable "Canvas Roots"
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
canvas-roots/
|
||||
├── src/ # Source code
|
||||
│ ├── models/ # Data models
|
||||
│ ├── canvas/ # Canvas manipulation
|
||||
│ ├── layout/ # D3 layout algorithms
|
||||
│ ├── utils/ # Utilities
|
||||
│ ├── gedcom/ # GEDCOM import/export
|
||||
│ └── settings.ts # Plugin settings
|
||||
├── styles/ # CSS components
|
||||
├── docs/ # Documentation
|
||||
│ ├── assets/ # Documentation assets
|
||||
│ └── developer/ # Developer docs
|
||||
├── main.ts # Plugin entry point
|
||||
├── manifest.json # Plugin metadata
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Daily Development
|
||||
|
||||
1. **Start watch mode**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. **Watch CSS changes** (in separate terminal)
|
||||
```bash
|
||||
npm run build:css:watch
|
||||
```
|
||||
|
||||
3. **Make changes** to TypeScript or CSS files
|
||||
|
||||
4. **Deploy to test vault**
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
5. **Reload Obsidian** (Ctrl/Cmd + R)
|
||||
|
||||
### Branch Strategy
|
||||
|
||||
- `main` - Stable releases only
|
||||
- Feature branches: `feature/description`
|
||||
- Bug fixes: `fix/description`
|
||||
- Documentation: `docs/description`
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Follow these guidelines:
|
||||
|
||||
- Use present tense: "Add feature" not "Added feature"
|
||||
- Use imperative mood: "Fix bug" not "Fixes bug"
|
||||
- Keep first line under 72 characters
|
||||
- Reference issues: "Fix #123: Description"
|
||||
- **Never mention AI tools or assistants** in commit messages
|
||||
|
||||
**Format:**
|
||||
```
|
||||
<type>: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Code style changes (formatting)
|
||||
- `refactor`: Code refactoring
|
||||
- `test`: Adding or updating tests
|
||||
- `chore`: Build process or tooling changes
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat: Add GEDCOM import functionality
|
||||
|
||||
Implements basic GEDCOM file parsing and person note creation.
|
||||
Supports GEDCOM 5.5.1 format.
|
||||
|
||||
Closes #42
|
||||
```
|
||||
|
||||
```
|
||||
fix: Correct node positioning on canvas
|
||||
|
||||
Fixes an issue where nodes were positioned incorrectly when
|
||||
the root person had no parents.
|
||||
|
||||
Fixes #57
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Follow the ESLint configuration ([.eslintrc.json](.eslintrc.json))
|
||||
- Use TypeScript strict mode
|
||||
- Prefer `const` over `let`
|
||||
- Use meaningful variable names
|
||||
- Add JSDoc comments for public APIs
|
||||
- Avoid `any` types when possible
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
/**
|
||||
* Generates a unique identifier for a person node.
|
||||
* @returns A UUID v4 string
|
||||
*/
|
||||
function generatePersonId(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
- Follow the Stylelint configuration ([.stylelintrc.json](.stylelintrc.json))
|
||||
- Use BEM naming with `cr-` prefix
|
||||
- Prefer CSS variables over hard-coded values
|
||||
- Use Obsidian's CSS variables for theme compatibility
|
||||
- See [docs/css-system.md](docs/css-system.md) for details
|
||||
|
||||
**Example:**
|
||||
```css
|
||||
.cr-person-node {
|
||||
width: var(--cr-node-width);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.cr-person-node__name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cr-person-node--highlighted {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
Run linting before committing:
|
||||
|
||||
```bash
|
||||
# TypeScript linting
|
||||
npm run lint
|
||||
|
||||
# CSS linting
|
||||
npm run lint:css
|
||||
|
||||
# Auto-fix issues
|
||||
npm run lint:fix
|
||||
npm run lint:css:fix
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Create test data**
|
||||
- Create person notes with various relationships
|
||||
- Test with different family structures
|
||||
- Include edge cases (orphans, adoptions, multiple spouses)
|
||||
|
||||
2. **Test scenarios**
|
||||
- Generate tree from person note
|
||||
- Re-layout existing canvas
|
||||
- GEDCOM import/export (when implemented)
|
||||
- Settings changes
|
||||
- Theme switching (light/dark)
|
||||
- Mobile compatibility
|
||||
|
||||
3. **Performance testing**
|
||||
- Test with large family trees (100+ people)
|
||||
- Monitor memory usage
|
||||
- Check layout calculation speed
|
||||
|
||||
### Automated Testing
|
||||
|
||||
(To be implemented)
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### When to Update Documentation
|
||||
|
||||
Update documentation when:
|
||||
|
||||
- Adding new features
|
||||
- Changing existing functionality
|
||||
- Fixing bugs that affect user workflow
|
||||
- Adding new configuration options
|
||||
- Changing plugin architecture
|
||||
|
||||
### Documentation Standards
|
||||
|
||||
Follow the [Documentation Style Guide](docs/assets/templates/documentation-style-guide.md):
|
||||
|
||||
- Use kebab-case for file names
|
||||
- Include table of contents for long documents
|
||||
- Use descriptive headings
|
||||
- Add code examples for complex features
|
||||
- Include screenshots for UI features
|
||||
- Use second person ("you") when addressing users
|
||||
- Avoid using "we" or "our"
|
||||
|
||||
### Documentation Types
|
||||
|
||||
- **User Guides**: How to use features
|
||||
- **Developer Docs**: Technical implementation details
|
||||
- **API Reference**: Public API documentation
|
||||
- **Tutorials**: Step-by-step walkthroughs
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
### Before Submitting
|
||||
|
||||
- [ ] Code follows style guidelines
|
||||
- [ ] All linters pass (`npm run lint`, `npm run lint:css`)
|
||||
- [ ] Build succeeds (`npm run build`)
|
||||
- [ ] Changes tested in Obsidian
|
||||
- [ ] Documentation updated if needed
|
||||
- [ ] Commit messages follow guidelines
|
||||
- [ ] No sensitive data in commits
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. **Create a Pull Request**
|
||||
- Use a clear, descriptive title
|
||||
- Reference related issues
|
||||
- Describe what changed and why
|
||||
- Include screenshots for UI changes
|
||||
- List breaking changes if any
|
||||
|
||||
2. **PR Template** (fill this in):
|
||||
```markdown
|
||||
## Description
|
||||
Brief description of changes
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation update
|
||||
|
||||
## Related Issues
|
||||
Fixes #(issue number)
|
||||
|
||||
## Testing
|
||||
How was this tested?
|
||||
|
||||
## Screenshots (if applicable)
|
||||
Add screenshots here
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows style guidelines
|
||||
- [ ] Linters pass
|
||||
- [ ] Build succeeds
|
||||
- [ ] Documentation updated
|
||||
- [ ] Tested in Obsidian
|
||||
```
|
||||
|
||||
3. **Review Process**
|
||||
- Address review comments
|
||||
- Keep PR focused and small when possible
|
||||
- Be responsive to feedback
|
||||
- Update PR description if scope changes
|
||||
|
||||
4. **After Merge**
|
||||
- Delete your branch
|
||||
- Update your fork's main branch
|
||||
|
||||
## Security
|
||||
|
||||
### Reporting Security Issues
|
||||
|
||||
**Do not open public issues for security vulnerabilities.**
|
||||
|
||||
See [SECURITY.md](SECURITY.md) for reporting procedures.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
This plugin handles sensitive PII (personally identifiable information):
|
||||
|
||||
- **Never log personal data** to console
|
||||
- **Be cautious with error messages** - don't expose PII
|
||||
- **Test privacy implications** of new features
|
||||
- **Document data handling** in security policy
|
||||
- **Consider GDPR/CCPA** compliance implications
|
||||
|
||||
### Data Privacy Guidelines
|
||||
|
||||
- Store data locally only (no external connections)
|
||||
- Use Obsidian's native file system APIs
|
||||
- Don't cache sensitive data unnecessarily
|
||||
- Respect user's vault privacy settings
|
||||
- Document all data storage locations
|
||||
|
||||
## Questions?
|
||||
|
||||
- **General questions**: Open a [GitHub Discussion](https://github.com/banisterious/obsidian-canvas-roots/discussions)
|
||||
- **Bug reports**: Open a [GitHub Issue](https://github.com/banisterious/obsidian-canvas-roots/issues)
|
||||
- **Feature requests**: Open a [GitHub Issue](https://github.com/banisterious/obsidian-canvas-roots/issues) with "enhancement" label
|
||||
- **Security issues**: See [SECURITY.md](SECURITY.md)
|
||||
|
||||
## License
|
||||
|
||||
By contributing to Canvas Roots, you agree that your contributions will be licensed under the MIT License.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Thank you for contributing to Canvas Roots and helping make family tree visualization in Obsidian better for everyone!
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 John Banister
|
||||
Copyright (c) 2025 banisterious
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
26
README.md
26
README.md
|
|
@ -81,6 +81,28 @@ The plugin will offer flexibility when importing family data:
|
|||
- **Deep Queries:** By populating note properties with rich data (dates, locations, sources), you'll be able to run complex genealogical queries against your vault.
|
||||
|
||||
|
||||
## 🧑💻 Development Notes (Pre-Development Phase)
|
||||
## 📚 Documentation
|
||||
|
||||
This project is currently in the specification and architecture phase. The future plugin relies on reading the vault's file structure, applying the layout logic of D3.js internally, and safely writing the positional data back into the Canvas JSON file. Careful handling of the Canvas Node ID and the person's `cr_id` is required for persistence.
|
||||
### For Contributors
|
||||
|
||||
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to Canvas Roots, including development setup, coding standards, and pull request process
|
||||
- **[Development Guide](docs/development.md)** - Complete development workflow, build commands, and testing procedures
|
||||
- **[CSS System](docs/css-system.md)** - CSS component architecture, build pipeline, and styling conventions
|
||||
- **[ESLint Setup](docs/eslint-setup.md)** - ESLint configuration details and compatibility notes
|
||||
|
||||
### For Users
|
||||
|
||||
- **[Bases Integration Guide](docs/bases-integration.md)** - How to use Obsidian Bases for efficient family tree data management
|
||||
- **[Security Policy](SECURITY.md)** - Important information about PII handling, data privacy, and security best practices
|
||||
|
||||
## 🧑💻 Development Status
|
||||
|
||||
Canvas Roots is in active development. The core plugin structure, build system, and documentation are in place. The plugin currently includes:
|
||||
|
||||
- TypeScript-based plugin foundation with Obsidian API integration
|
||||
- Build system with esbuild and automated CSS compilation
|
||||
- Data models for Person and Canvas structures
|
||||
- Settings interface for layout customization
|
||||
- Command structure for tree generation and re-layout
|
||||
|
||||
The plugin relies on reading the vault's file structure, applying the layout logic of D3.js internally, and safely writing the positional data back into the Canvas JSON file. Careful handling of the Canvas Node ID and the person's `cr_id` is required for persistence.
|
||||
|
|
|
|||
215
SECURITY.md
Normal file
215
SECURITY.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Currently, security updates are provided for the latest release version only.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.1.x | :white_check_mark: |
|
||||
|
||||
## Data Privacy and Personally Identifiable Information (PII)
|
||||
|
||||
### Nature of Data Stored
|
||||
|
||||
Canvas Roots handles **highly sensitive personally identifiable information (PII)** by design, including:
|
||||
|
||||
- **Full names** of individuals and family members
|
||||
- **Birth and death dates**
|
||||
- **Family relationships** (parents, children, spouses)
|
||||
- **GEDCOM files** containing extensive genealogical data
|
||||
- **Custom notes** about individuals
|
||||
- **Potential additional PII** in linked Markdown files
|
||||
|
||||
**Important**: All data is stored **locally** in your Obsidian vault. The plugin does **not**:
|
||||
- Transmit data over the network
|
||||
- Connect to external services
|
||||
- Upload information to cloud servers
|
||||
- Share data with third parties
|
||||
|
||||
### Data Storage Locations
|
||||
|
||||
1. **Person Notes** (`.md` files)
|
||||
- Location: Anywhere in your vault (user-controlled)
|
||||
- Format: Markdown with YAML frontmatter
|
||||
- Contains: Names, dates, relationships, cr_id values
|
||||
|
||||
2. **Canvas Files** (`.canvas` files)
|
||||
- Location: Anywhere in your vault (user-controlled)
|
||||
- Format: JSON
|
||||
- Contains: Visual layout data, references to person notes
|
||||
|
||||
3. **Plugin Settings** (`data.json`)
|
||||
- Location: `.obsidian/plugins/canvas-roots/data.json`
|
||||
- Format: Plain text JSON (unencrypted)
|
||||
- Contains: Plugin configuration (node sizes, spacing, preferences)
|
||||
- Does NOT contain: Personal genealogical data
|
||||
|
||||
### Why Plain Text Storage?
|
||||
|
||||
1. **Obsidian Standard**: Follows Obsidian's markdown-first philosophy
|
||||
2. **User Control**: Users maintain full ownership and control of their data
|
||||
3. **Transparency**: No hidden or obfuscated data storage
|
||||
4. **Portability**: Data can be easily backed up, migrated, or processed by other tools
|
||||
5. **Mobile Compatibility**: Works across all Obsidian platforms
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
### For Individual Users
|
||||
|
||||
1. **Vault Protection**
|
||||
- Use strong passwords/encryption for your vault
|
||||
- Enable Obsidian's vault encryption if available
|
||||
- Store your vault on encrypted storage devices
|
||||
- Limit physical access to devices containing your vault
|
||||
|
||||
2. **Sharing Considerations**
|
||||
- **Never** share your vault publicly without sanitizing PII first
|
||||
- Be extremely cautious about who has access to your vault
|
||||
- Consider using separate vaults for sensitive genealogical data vs. other notes
|
||||
- Remove or anonymize data before sharing Canvas screenshots
|
||||
|
||||
3. **Cloud Sync and Backup**
|
||||
- Understand that cloud sync services (Obsidian Sync, Dropbox, etc.) will sync all PII
|
||||
- Ensure your cloud storage is properly secured with 2FA
|
||||
- Consider local-only vaults for highly sensitive family data
|
||||
- Encrypted cloud storage is strongly recommended
|
||||
|
||||
4. **Version Control (Git)**
|
||||
- **NEVER** commit genealogical vaults to public repositories
|
||||
- Use private repositories only if necessary
|
||||
- Consider adding person notes directory to `.gitignore`
|
||||
- Be aware that git history contains all previous versions of data
|
||||
|
||||
5. **GEDCOM Files**
|
||||
- GEDCOM files contain extensive PII about living and deceased individuals
|
||||
- Treat GEDCOM files with the same security as financial documents
|
||||
- Be cautious when importing GEDCOM files from untrusted sources
|
||||
- Consider redacting information about living individuals before export
|
||||
|
||||
### For Professional Genealogists
|
||||
|
||||
1. **Client Data**
|
||||
- Maintain separate vaults for each client
|
||||
- Never mix client data in shared vaults
|
||||
- Follow applicable data protection regulations (GDPR, CCPA, etc.)
|
||||
- Obtain explicit consent before storing client family data
|
||||
|
||||
2. **Compliance**
|
||||
- This plugin does not provide GDPR/CCPA compliance features
|
||||
- Users are responsible for compliance with applicable regulations
|
||||
- Consider data retention policies for deceased individuals
|
||||
- Document your data handling procedures
|
||||
|
||||
3. **Data Anonymization**
|
||||
- Remove cr_id values before sharing example data
|
||||
- Replace real names with pseudonyms for demonstrations
|
||||
- Use fictional data for screenshots and examples
|
||||
- Implement your own data anonymization workflow as needed
|
||||
|
||||
## Security Best Practices for Users
|
||||
|
||||
### Data Access Control
|
||||
|
||||
1. **Physical Security**: Secure devices containing your vault
|
||||
2. **Account Security**: Use strong passwords for OS accounts
|
||||
3. **Application Security**: Keep Obsidian and plugins updated
|
||||
4. **Network Security**: Be cautious on public WiFi when accessing vaults
|
||||
|
||||
### Data Backup
|
||||
|
||||
1. **Regular Backups**: Maintain encrypted backups of your vault
|
||||
2. **Backup Testing**: Periodically verify backup integrity
|
||||
3. **Offsite Storage**: Consider encrypted offsite backup storage
|
||||
4. **Backup Security**: Protect backups with same security as primary vault
|
||||
|
||||
### Data Lifecycle
|
||||
|
||||
1. **Data Collection**: Only include necessary PII
|
||||
2. **Data Retention**: Consider retention policies for old data
|
||||
3. **Data Deletion**: Securely delete data when no longer needed
|
||||
4. **Data Migration**: Securely transfer data when changing systems
|
||||
|
||||
## Known Security Limitations
|
||||
|
||||
1. **No Built-in Encryption**: The plugin does not encrypt data (relies on Obsidian/OS)
|
||||
2. **No Access Controls**: Anyone with vault access can view all data
|
||||
3. **No Audit Logging**: The plugin does not log data access
|
||||
4. **No Data Masking**: All data is visible in plain text
|
||||
5. **No Automatic Redaction**: No automatic PII redaction features
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Canvas Roots, please report it by:
|
||||
|
||||
1. **DO NOT** open a public GitHub issue for security vulnerabilities
|
||||
2. Email the maintainer directly at: [Check GitHub profile or package.json]
|
||||
3. Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact (especially regarding PII exposure)
|
||||
- Suggested fix (if available)
|
||||
|
||||
You can expect:
|
||||
- Initial response within 48 hours
|
||||
- Regular updates on the status of your report
|
||||
- Credit in the security advisory (unless you prefer to remain anonymous)
|
||||
- Coordination on disclosure timeline
|
||||
|
||||
## Legal and Ethical Considerations
|
||||
|
||||
### Privacy Laws
|
||||
|
||||
Users should be aware of and comply with applicable privacy laws, including but not limited to:
|
||||
|
||||
- **GDPR** (EU): Right to be forgotten, data portability, consent requirements
|
||||
- **CCPA** (California): Consumer privacy rights, data disclosure requirements
|
||||
- **Other Regional Laws**: Consult local privacy regulations
|
||||
|
||||
### Ethical Genealogy
|
||||
|
||||
1. **Living Individuals**: Exercise caution when recording data about living persons
|
||||
2. **Consent**: Consider obtaining consent before recording others' information
|
||||
3. **Sensitive Information**: Handle adoptions, paternity, and medical data with care
|
||||
4. **Cultural Sensitivity**: Respect cultural norms around family information
|
||||
5. **Historical Context**: Be mindful of historical injustices in genealogical records
|
||||
|
||||
## Data Breach Response
|
||||
|
||||
If you suspect your vault containing family data has been compromised:
|
||||
|
||||
1. **Immediate Actions**:
|
||||
- Disconnect the device from network
|
||||
- Change passwords for cloud sync services
|
||||
- Review access logs if available
|
||||
- Identify what data may have been exposed
|
||||
|
||||
2. **Assessment**:
|
||||
- Determine scope of exposure
|
||||
- Identify affected individuals
|
||||
- Consider legal notification requirements
|
||||
|
||||
3. **Mitigation**:
|
||||
- Create new vault with fresh data
|
||||
- Review and update security practices
|
||||
- Consider informing affected family members
|
||||
- Document the incident
|
||||
|
||||
## Future Security Enhancements
|
||||
|
||||
Planned improvements:
|
||||
|
||||
- Optional encryption for cr_id values
|
||||
- Data sanitization utilities for sharing
|
||||
- Automatic living/deceased person detection
|
||||
- PII redaction tools for exports
|
||||
- Audit logging capabilities
|
||||
- Access control recommendations
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We take the security and privacy of genealogical data seriously and appreciate security researchers and privacy advocates who help keep Canvas Roots secure.
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Your family's privacy is in your hands. This plugin provides tools for organizing genealogical data, but you are responsible for securing that data appropriately.
|
||||
415
build-css.js
Executable file
415
build-css.js
Executable file
|
|
@ -0,0 +1,415 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Canvas Roots CSS Build System
|
||||
*
|
||||
* A Node.js build script that:
|
||||
* - Lints CSS with Stylelint
|
||||
* - Formats CSS with Prettier
|
||||
* - Concatenates component files
|
||||
* - Generates final styles.css for Obsidian
|
||||
* - Provides watch mode for development
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const chalk = require('chalk');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
stylesDir: 'styles',
|
||||
outputFile: 'styles.css',
|
||||
tempFile: 'styles.tmp.css',
|
||||
|
||||
// Component order for concatenation (dependency-aware)
|
||||
componentOrder: [
|
||||
'variables.css', // CSS custom properties and design tokens
|
||||
'base.css', // Base structural elements
|
||||
'layout.css', // Layout utilities
|
||||
'canvas.css', // Canvas-specific styling
|
||||
'nodes.css', // Family tree node styling
|
||||
'edges.css', // Relationship edge styling
|
||||
'settings.css', // Settings interface
|
||||
'modals.css', // Modal dialogs
|
||||
'animations.css', // Keyframes and transitions
|
||||
'responsive.css', // Responsive breakpoints
|
||||
'theme.css' // Theme compatibility (last)
|
||||
],
|
||||
|
||||
// Files to exclude from processing
|
||||
excludedFiles: []
|
||||
};
|
||||
|
||||
// Logging utilities with colors
|
||||
const log = {
|
||||
info: (msg) => console.log(chalk.blue(`[INFO]`), msg),
|
||||
success: (msg) => console.log(chalk.green(`[SUCCESS]`), msg),
|
||||
warn: (msg) => console.log(chalk.yellow(`[WARN]`), msg),
|
||||
error: (msg) => console.log(chalk.red(`[ERROR]`), msg),
|
||||
debug: (msg) => console.log(chalk.gray(`[DEBUG]`), msg)
|
||||
};
|
||||
|
||||
/**
|
||||
* Get file information (lines, size, etc.)
|
||||
*/
|
||||
async function getFileInfo(filePath) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const lines = content.split('\n').length;
|
||||
const sizeKB = (stats.size / 1024).toFixed(2);
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
lines,
|
||||
bytes: stats.size,
|
||||
sizeKB: parseFloat(sizeKB)
|
||||
};
|
||||
} catch (error) {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Stylelint on component files
|
||||
*/
|
||||
async function lintCSS() {
|
||||
log.info('Running Stylelint on component files...');
|
||||
|
||||
try {
|
||||
execSync('npx stylelint "styles/**/*.css"', {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
log.success('CSS linting passed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('CSS linting failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format CSS files with Prettier
|
||||
*/
|
||||
async function formatCSS() {
|
||||
log.info('Formatting CSS with Prettier...');
|
||||
|
||||
try {
|
||||
execSync('npx prettier --write "styles/**/*.css"', {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
log.success('CSS formatting completed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('CSS formatting failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate build header with metadata
|
||||
*/
|
||||
function generateBuildHeader(componentCount) {
|
||||
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
||||
|
||||
return `/* ==========================================================================
|
||||
Canvas Roots Plugin Stylesheet - BUILT VERSION
|
||||
========================================================================== */
|
||||
|
||||
/* Canvas Roots: Family Trees in Obsidian Canvas
|
||||
* An Obsidian plugin for creating and visualizing family trees using D3.js layouts.
|
||||
*
|
||||
* GitHub: https://github.com/banisterious/obsidian-canvas-roots
|
||||
*
|
||||
* BUILD INFORMATION:
|
||||
* - Generated: ${timestamp}
|
||||
* - Components: ${componentCount} files
|
||||
* - Build System: Node.js CSS Build Script v1.0.0
|
||||
*
|
||||
* NOTE: This file is automatically generated from component files in styles/
|
||||
* Do not edit this file directly - edit the component files instead!
|
||||
* Run 'npm run build:css' to regenerate this file.
|
||||
*/
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate component header
|
||||
*/
|
||||
function generateComponentHeader(componentName, fileInfo) {
|
||||
return `
|
||||
/* ==========================================================================
|
||||
COMPONENT: ${componentName.toUpperCase()}
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: ${fileInfo.lines} lines, ${fileInfo.sizeKB} KB */
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate build footer with statistics
|
||||
*/
|
||||
function generateBuildFooter(stats, componentBreakdown) {
|
||||
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
||||
|
||||
let footer = `
|
||||
/* ==========================================================================
|
||||
BUILD STATISTICS
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Build completed: ${timestamp}
|
||||
* Components processed: ${stats.componentCount}
|
||||
* Total lines: ${stats.totalLines}
|
||||
* Total size: ${stats.totalKB} KB
|
||||
* Build duration: ${stats.buildDuration} seconds
|
||||
*
|
||||
* Component breakdown:`;
|
||||
|
||||
componentBreakdown.forEach(component => {
|
||||
footer += `\n * - ${component.name}: ${component.lines} lines, ${component.sizeKB} KB`;
|
||||
});
|
||||
|
||||
footer += `\n */\n\n/* End of generated stylesheet */\n`;
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main CSS build function
|
||||
*/
|
||||
async function buildCSS() {
|
||||
const buildStart = Date.now();
|
||||
log.info('Starting CSS build process...');
|
||||
|
||||
try {
|
||||
// Check if styles directory exists
|
||||
const stylesPath = path.join(process.cwd(), CONFIG.stylesDir);
|
||||
await fs.access(stylesPath);
|
||||
} catch (error) {
|
||||
log.error(`Styles directory '${CONFIG.stylesDir}' not found!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize build metrics
|
||||
let totalLines = 0;
|
||||
let totalBytes = 0;
|
||||
let componentCount = 0;
|
||||
const componentBreakdown = [];
|
||||
|
||||
// Start building
|
||||
let buildContent = generateBuildHeader(CONFIG.componentOrder.length);
|
||||
|
||||
log.info(`Processing ${CONFIG.componentOrder.length} CSS components...`);
|
||||
|
||||
// Process each component in order
|
||||
for (const component of CONFIG.componentOrder) {
|
||||
const componentPath = path.join(CONFIG.stylesDir, component);
|
||||
const fileInfo = await getFileInfo(componentPath);
|
||||
|
||||
if (fileInfo.exists) {
|
||||
log.info(` + ${component} (${fileInfo.lines} lines, ${fileInfo.sizeKB} KB)`);
|
||||
|
||||
// Add component header
|
||||
buildContent += generateComponentHeader(component, fileInfo);
|
||||
|
||||
// Add component content
|
||||
const componentContent = await fs.readFile(componentPath, 'utf8');
|
||||
buildContent += componentContent;
|
||||
|
||||
// Add component footer
|
||||
buildContent += `\n\n/* End of ${component} */\n`;
|
||||
|
||||
// Update metrics
|
||||
totalLines += fileInfo.lines;
|
||||
totalBytes += fileInfo.bytes;
|
||||
componentCount++;
|
||||
|
||||
componentBreakdown.push({
|
||||
name: component,
|
||||
lines: fileInfo.lines,
|
||||
sizeKB: fileInfo.sizeKB
|
||||
});
|
||||
} else {
|
||||
log.warn(` - ${component} (not found, skipping)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned files
|
||||
const allFiles = await fs.readdir(CONFIG.stylesDir);
|
||||
const cssFiles = allFiles.filter(file =>
|
||||
file.endsWith('.css') &&
|
||||
!CONFIG.excludedFiles.includes(file) &&
|
||||
!CONFIG.componentOrder.includes(file)
|
||||
);
|
||||
|
||||
if (cssFiles.length > 0) {
|
||||
log.warn(`Found ${cssFiles.length} orphaned CSS files:`);
|
||||
cssFiles.forEach(file => {
|
||||
log.warn(` ! ${file} (not in build order)`);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate final statistics
|
||||
const buildEnd = Date.now();
|
||||
const buildDuration = ((buildEnd - buildStart) / 1000).toFixed(2);
|
||||
const totalKB = (totalBytes / 1024).toFixed(2);
|
||||
|
||||
const stats = {
|
||||
componentCount,
|
||||
totalLines,
|
||||
totalKB: parseFloat(totalKB),
|
||||
buildDuration: parseFloat(buildDuration)
|
||||
};
|
||||
|
||||
// Add build footer
|
||||
buildContent += generateBuildFooter(stats, componentBreakdown);
|
||||
|
||||
// Write to temporary file first
|
||||
await fs.writeFile(CONFIG.tempFile, buildContent, 'utf8');
|
||||
|
||||
// Check if output file exists
|
||||
const outputExists = await getFileInfo(CONFIG.outputFile);
|
||||
if (outputExists.exists) {
|
||||
log.info(`Replacing existing ${CONFIG.outputFile} (${outputExists.sizeKB} KB)`);
|
||||
}
|
||||
|
||||
// Move temp file to final output
|
||||
await fs.rename(CONFIG.tempFile, CONFIG.outputFile);
|
||||
|
||||
// Final verification
|
||||
const finalInfo = await getFileInfo(CONFIG.outputFile);
|
||||
|
||||
log.success('Build completed successfully!');
|
||||
log.success(`Generated: ${CONFIG.outputFile} (${finalInfo.lines} lines, ${finalInfo.sizeKB} KB)`);
|
||||
log.success(`Build time: ${buildDuration} seconds`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch mode for development
|
||||
*/
|
||||
async function watchCSS() {
|
||||
log.info('Starting CSS watch mode...');
|
||||
log.info(`Watching for changes in ${CONFIG.stylesDir} directory...`);
|
||||
log.info('Press Ctrl+C to stop watching');
|
||||
|
||||
// Initial build
|
||||
await buildCSS();
|
||||
|
||||
// Setup file watcher
|
||||
const watcher = chokidar.watch(path.join(CONFIG.stylesDir, '*.css'), {
|
||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||
persistent: true,
|
||||
ignoreInitial: true
|
||||
});
|
||||
|
||||
watcher.on('change', async (filePath) => {
|
||||
const fileName = path.basename(filePath);
|
||||
log.info(`Detected change in ${fileName}, rebuilding...`);
|
||||
await buildCSS();
|
||||
});
|
||||
|
||||
watcher.on('add', async (filePath) => {
|
||||
const fileName = path.basename(filePath);
|
||||
log.info(`New file added: ${fileName}, rebuilding...`);
|
||||
await buildCSS();
|
||||
});
|
||||
|
||||
watcher.on('unlink', async (filePath) => {
|
||||
const fileName = path.basename(filePath);
|
||||
log.info(`File removed: ${fileName}, rebuilding...`);
|
||||
await buildCSS();
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
log.info('Stopping watch mode...');
|
||||
watcher.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Full build pipeline with linting and formatting
|
||||
*/
|
||||
async function fullBuild() {
|
||||
log.info('Starting full CSS build pipeline...');
|
||||
|
||||
// Step 1: Format CSS files
|
||||
const formatSuccess = await formatCSS();
|
||||
if (!formatSuccess) {
|
||||
log.error('Build failed at formatting stage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Lint CSS files
|
||||
const lintSuccess = await lintCSS();
|
||||
if (!lintSuccess) {
|
||||
if (process.argv.includes('--no-fail-on-lint')) {
|
||||
log.warn('CSS linting failed but continuing build due to --no-fail-on-lint flag...');
|
||||
} else {
|
||||
log.error('Build failed at linting stage');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Build CSS
|
||||
const buildSuccess = await buildCSS();
|
||||
if (!buildSuccess) {
|
||||
log.error('Build failed at build stage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.success('Full CSS build pipeline completed successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isWatch = args.includes('--watch') || args.includes('-w');
|
||||
const isLintOnly = args.includes('--lint') || args.includes('-l');
|
||||
const isFormatOnly = args.includes('--format') || args.includes('-f');
|
||||
const isBuildOnly = args.includes('--build-only') || args.includes('-b');
|
||||
|
||||
console.log(chalk.cyan('\n🎨 Canvas Roots CSS Build System v1.0.0\n'));
|
||||
|
||||
try {
|
||||
if (isLintOnly) {
|
||||
await lintCSS();
|
||||
} else if (isFormatOnly) {
|
||||
await formatCSS();
|
||||
} else if (isBuildOnly) {
|
||||
await buildCSS();
|
||||
} else if (isWatch) {
|
||||
await watchCSS();
|
||||
} else {
|
||||
await fullBuild();
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Build failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the build system
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildCSS,
|
||||
lintCSS,
|
||||
formatCSS,
|
||||
watchCSS
|
||||
};
|
||||
26
deploy.sh
Executable file
26
deploy.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Deploy script for Canvas Roots plugin
|
||||
# Builds the plugin and copies it to the Obsidian vault
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building Canvas Roots plugin..."
|
||||
npm run build
|
||||
|
||||
# Convert Windows path to WSL path
|
||||
VAULT_PATH="/mnt/d/Vaults/Banister/.obsidian/plugins/canvas-roots"
|
||||
|
||||
echo "Deploying to: $VAULT_PATH"
|
||||
|
||||
# Create plugin directory if it doesn't exist
|
||||
mkdir -p "$VAULT_PATH"
|
||||
|
||||
# Copy necessary files
|
||||
echo "Copying files..."
|
||||
cp main.js "$VAULT_PATH/"
|
||||
cp manifest.json "$VAULT_PATH/"
|
||||
cp styles.css "$VAULT_PATH/" 2>/dev/null || echo "No styles.css found (optional)"
|
||||
|
||||
echo "✓ Plugin deployed successfully!"
|
||||
echo "Reload Obsidian to see changes."
|
||||
55
dev-deploy.sh
Executable file
55
dev-deploy.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Development deployment script with file watching
|
||||
# Builds on file changes and auto-deploys to vault
|
||||
|
||||
set -e
|
||||
|
||||
# Convert Windows path to WSL path
|
||||
VAULT_PATH="/mnt/d/Vaults/Banister/.obsidian/plugins/canvas-roots"
|
||||
|
||||
echo "Starting Canvas Roots development mode..."
|
||||
echo "Watching for changes and auto-deploying to: $VAULT_PATH"
|
||||
echo "Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
# Create plugin directory if it doesn't exist
|
||||
mkdir -p "$VAULT_PATH"
|
||||
|
||||
# Initial build and deploy
|
||||
echo "Initial build..."
|
||||
npm run build
|
||||
|
||||
echo "Deploying..."
|
||||
cp main.js "$VAULT_PATH/"
|
||||
cp manifest.json "$VAULT_PATH/"
|
||||
cp styles.css "$VAULT_PATH/" 2>/dev/null || true
|
||||
|
||||
echo "✓ Initial deployment complete!"
|
||||
echo "Now watching for changes..."
|
||||
echo ""
|
||||
|
||||
# Watch for changes and rebuild
|
||||
# We'll use inotifywait if available, otherwise fall back to simple approach
|
||||
if command -v inotifywait &> /dev/null; then
|
||||
while true; do
|
||||
# Wait for any .ts file to change
|
||||
inotifywait -e modify,create,delete -r --exclude 'node_modules|\.git|main\.js' . 2>/dev/null
|
||||
|
||||
echo "Change detected, rebuilding..."
|
||||
if npm run build 2>&1; then
|
||||
cp main.js "$VAULT_PATH/"
|
||||
cp manifest.json "$VAULT_PATH/"
|
||||
cp styles.css "$VAULT_PATH/" 2>/dev/null || true
|
||||
echo "✓ Deployed at $(date +%H:%M:%S)"
|
||||
else
|
||||
echo "✗ Build failed at $(date +%H:%M:%S)"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
else
|
||||
echo "Note: Install inotify-tools for automatic rebuilds on file changes"
|
||||
echo "Run: sudo apt-get install inotify-tools"
|
||||
echo ""
|
||||
echo "For now, run 'npm run deploy' manually after making changes."
|
||||
fi
|
||||
281
docs/assets/templates/documentation-style-guide.md
Normal file
281
docs/assets/templates/documentation-style-guide.md
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# Documentation Style Guide
|
||||
|
||||
## Executive Summary
|
||||
|
||||
- **Consistency**: Maintain consistent formatting, voice, and structure across all documentation
|
||||
- **Visual Aid**: Use annotated screenshots and diagrams to enhance understanding
|
||||
- **Accessibility**: Ensure all documentation is accessible to all users
|
||||
- **Organization**: Follow the established file and section structure for all documents
|
||||
- **User-Focused**: Write from the user's perspective with clear, concise language
|
||||
|
||||
## Voice and Tone
|
||||
|
||||
### Writing Style
|
||||
|
||||
- **Person**: Use second person ("you") when addressing the user directly
|
||||
- **Tense**: Use present tense for actions and descriptions
|
||||
- **Active Voice**: Prefer active voice over passive voice
|
||||
- **Clarity**: Use clear, concise language without unnecessary jargon
|
||||
- **Consistency**: Maintain consistent terminology throughout all documentation
|
||||
- **Personal Pronouns**: Avoid using "we," "our," or "us" in documentation. Instead, refer directly to "the plugin," "the feature," or use other specific subjects.
|
||||
|
||||
### Examples
|
||||
|
||||
**Preferred:**
|
||||
- "Open the person note before running the command."
|
||||
- "You can customize node spacing in the settings."
|
||||
- "The plugin automatically generates unique cr_id values."
|
||||
|
||||
**Avoid:**
|
||||
- "The person note should be opened before the command is run."
|
||||
- "We added a new feature that enables multi-parent graphs."
|
||||
|
||||
### Technical Writing
|
||||
|
||||
- Break complex procedures into numbered steps
|
||||
- Use bullet points for lists of related items
|
||||
- Highlight important notes using blockquotes
|
||||
- Use code blocks for YAML examples, GEDCOM data, or code snippets
|
||||
- Define abbreviations on first use (e.g., "GEDCOM (Genealogical Data Communication)")
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── user/ # End-user documentation
|
||||
│ ├── guides/ # How-to guides
|
||||
│ ├── concepts/ # Conceptual explanations
|
||||
│ └── reference/ # Reference materials
|
||||
├── developer/ # Developer documentation
|
||||
│ ├── architecture/ # System architecture
|
||||
│ ├── implementation/ # Implementation details
|
||||
│ ├── testing/ # Testing guidance
|
||||
│ └── contributing/ # Contribution guides
|
||||
├── planning/ # Active planning documents
|
||||
│ ├── features/ # Feature planning
|
||||
│ └── feature-requirements/ # Detailed requirements
|
||||
├── assets/ # Documentation assets
|
||||
│ ├── images/ # Screenshots and diagrams
|
||||
│ └── templates/ # Document templates
|
||||
├── archive/ # Historical documents
|
||||
└── canvas-roots-initial-spec.md # Initial specification
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Files**: Use kebab-case for all file names: `tree-generation.md`
|
||||
- **Directories**: Use Title Case: `Implementation/`
|
||||
- **Extension**: Use `.md` for all documentation files
|
||||
|
||||
## Document Structure
|
||||
|
||||
### Table of Contents
|
||||
|
||||
Every documentation file must include a Table of Contents after the title:
|
||||
|
||||
```markdown
|
||||
## Table of Contents
|
||||
|
||||
- [1. Introduction](#1-introduction)
|
||||
- [2. Installation](#2-installation)
|
||||
- [2.1. Prerequisites](#21-prerequisites)
|
||||
- [2.2. Installation Steps](#22-installation-steps)
|
||||
```
|
||||
|
||||
### Heading Hierarchy
|
||||
|
||||
- **H1 (#)**: Document title - only one per document
|
||||
- **H2 (##)**: Major sections
|
||||
- **H3 (###)**: Subsections
|
||||
- **H4 (####)**: Minor subsections
|
||||
|
||||
Use Title Case for all headings. Keep headings concise (under 60 characters).
|
||||
|
||||
## Image Guidelines
|
||||
|
||||
- **Organization**: Store all images in `docs/assets/images/`
|
||||
- **Naming**: Use descriptive filenames: `tree-layout-example.png`
|
||||
- **Sizing**:
|
||||
- Banner images: 1200-1600px wide
|
||||
- Inline images: 600-800px wide
|
||||
- Diagrams: 800-1200px wide
|
||||
- **Alt Text**: Always include descriptive alt text
|
||||
|
||||
## Code Examples
|
||||
|
||||
### YAML Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
cr_id: 550e8400-e29b-41d4-a716-446655440000
|
||||
name: John Smith
|
||||
born: 1950-03-15
|
||||
died: 2020-08-22
|
||||
---
|
||||
|
||||
Father:: [[Robert Smith]]
|
||||
Mother:: [[Mary Johnson]]
|
||||
Spouse:: [[Jane Doe]]
|
||||
```
|
||||
|
||||
### GEDCOM Format
|
||||
|
||||
```gedcom
|
||||
0 @I1@ INDI
|
||||
1 NAME John /Smith/
|
||||
1 SEX M
|
||||
1 BIRT
|
||||
2 DATE 15 MAR 1950
|
||||
```
|
||||
|
||||
### DataView Queries
|
||||
|
||||
```dataviewjs
|
||||
// Find all people with cr_id
|
||||
dv.table(
|
||||
["Name", "Born", "Died"],
|
||||
dv.pages().where(p => p.cr_id)
|
||||
.map(p => [p.file.link, p.born, p.died])
|
||||
)
|
||||
```
|
||||
|
||||
## Annotated Screenshots
|
||||
|
||||
### Annotation Color Scheme
|
||||
|
||||
Use colorblind-friendly colors:
|
||||
- Person notes: Blue (#3498db)
|
||||
- Canvas nodes: Green (#2ecc71)
|
||||
- Relationship edges: Purple (#9b59b6)
|
||||
- Settings/UI elements: Orange (#e67e22)
|
||||
|
||||
### Annotation Style
|
||||
|
||||
- Rounded rectangles for highlighting
|
||||
- 2px solid borders
|
||||
- 50% transparent fill
|
||||
- Sans-serif font (Calibri, Arial)
|
||||
- 14-16pt font size
|
||||
|
||||
## Accessibility Guidelines
|
||||
|
||||
- **Alt Text**: All images must include descriptive alt text
|
||||
- **Contrast**: Minimum 4.5:1 ratio between text and background
|
||||
- **Color Independence**: Never use color as the only means of conveying information
|
||||
- **Link Text**: Use descriptive link text, not "click here"
|
||||
- **Plain Language**: Aim for 9th-grade reading level
|
||||
- **Headings**: Use proper heading hierarchy (h1, h2, h3) in sequential order
|
||||
|
||||
## Version Control
|
||||
|
||||
### When to Update Documentation
|
||||
|
||||
- When adding a new feature
|
||||
- When changing existing functionality
|
||||
- When deprecating or removing features
|
||||
- When fixing bugs that affect user workflow
|
||||
- When improving clarity based on user feedback
|
||||
|
||||
### Date Usage
|
||||
|
||||
- **Past Releases**: Use specific dates in YYYY-MM-DD format
|
||||
- **Future Plans**: Avoid specific dates; use "In a future release" or "Planned for implementation"
|
||||
- **Implementation Timelines**: Use dependency relationships, not calendar dates
|
||||
- Example: "Person schema → D3 layout → Canvas generation → GEDCOM integration"
|
||||
|
||||
### Documentation Commits
|
||||
|
||||
- Use the prefix `docs:` for documentation-only commits
|
||||
- Example: `docs: Add GEDCOM import guide`
|
||||
|
||||
## Documentation Templates
|
||||
|
||||
### Feature Documentation
|
||||
|
||||
```markdown
|
||||
# Feature Name
|
||||
|
||||
## Overview
|
||||
Brief description of what the feature does.
|
||||
|
||||
## How to Access
|
||||
How to enable or use the feature.
|
||||
|
||||
## Configuration Options
|
||||
All configuration options with examples.
|
||||
|
||||
## Usage Examples
|
||||
2-3 practical examples.
|
||||
|
||||
## Tips and Best Practices
|
||||
Advice on getting the most out of the feature.
|
||||
|
||||
## Troubleshooting
|
||||
Common issues and solutions.
|
||||
|
||||
## Related Features
|
||||
Links to related documentation.
|
||||
```
|
||||
|
||||
### Tutorial
|
||||
|
||||
```markdown
|
||||
# Tutorial: [Task Name]
|
||||
|
||||
## What You'll Learn
|
||||
What the user will accomplish.
|
||||
|
||||
## Prerequisites
|
||||
What's needed before starting.
|
||||
|
||||
## Step 1: [First Task]
|
||||
Detailed instructions with screenshots.
|
||||
|
||||
## Step 2: [Second Task]
|
||||
Detailed instructions with screenshots.
|
||||
|
||||
## Next Steps
|
||||
Suggestions for what to try next.
|
||||
|
||||
## Troubleshooting
|
||||
Common issues specific to this tutorial.
|
||||
```
|
||||
|
||||
### Technical Documentation
|
||||
|
||||
```markdown
|
||||
# Component Name
|
||||
|
||||
## Overview
|
||||
Technical description of the component.
|
||||
|
||||
## Architecture
|
||||
Component's architecture and design decisions.
|
||||
|
||||
## API Reference
|
||||
Public APIs, interfaces, and types.
|
||||
|
||||
## Implementation Details
|
||||
How the component works internally.
|
||||
|
||||
## Testing
|
||||
Testing strategy and examples.
|
||||
```
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Before submitting documentation:
|
||||
|
||||
- [ ] Follows voice and tone guidelines
|
||||
- [ ] Proper formatting and structure
|
||||
- [ ] Images include alt text
|
||||
- [ ] Appropriate examples for complex concepts
|
||||
- [ ] Links to related documentation
|
||||
- [ ] Code examples are complete and tested
|
||||
- [ ] Spell-checked and grammar-checked
|
||||
- [ ] Mobile-friendly
|
||||
- [ ] All cross-references are valid
|
||||
|
||||
---
|
||||
|
||||
For the complete template source, see: [Sonigraph Documentation Style Guide](https://github.com/banisterious/sonigraph/blob/main/docs/assets/templates/documentation-style-guide.md)
|
||||
128
docs/assets/templates/family-members.base
Normal file
128
docs/assets/templates/family-members.base
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Canvas Roots - Family Members Base Template
|
||||
#
|
||||
# This base provides table views for managing family tree data.
|
||||
# Copy this file to your vault and customize as needed.
|
||||
#
|
||||
# Learn more: https://github.com/banisterious/obsidian-canvas-roots
|
||||
|
||||
filters:
|
||||
or:
|
||||
- file.hasTag("person")
|
||||
- file.hasProperty("cr_id")
|
||||
|
||||
formulas:
|
||||
# Display the person's full name or fallback to filename
|
||||
display_name: 'name || file.name'
|
||||
|
||||
# Calculate lifespan if both dates are present
|
||||
full_lifespan: 'if(born && died, died.year() - born.year() + " years", "")'
|
||||
|
||||
# Calculate current age if born but not died
|
||||
age_now: 'if(born && !died, now().year() - born.year(), "")'
|
||||
|
||||
# Format birth date for display
|
||||
birth_display: 'if(born, born.format("YYYY-MM-DD"), "")'
|
||||
|
||||
# Format death date for display
|
||||
death_display: 'if(died, died.format("YYYY-MM-DD"), "")'
|
||||
|
||||
properties:
|
||||
cr_id:
|
||||
displayName: "ID"
|
||||
formula.display_name:
|
||||
displayName: "Name"
|
||||
note.father:
|
||||
displayName: "Father"
|
||||
note.mother:
|
||||
displayName: "Mother"
|
||||
note.spouse:
|
||||
displayName: "Spouse(s)"
|
||||
note.child:
|
||||
displayName: "Children"
|
||||
formula.birth_display:
|
||||
displayName: "Born"
|
||||
formula.death_display:
|
||||
displayName: "Died"
|
||||
formula.full_lifespan:
|
||||
displayName: "Lifespan"
|
||||
formula.age_now:
|
||||
displayName: "Age"
|
||||
note.gender:
|
||||
displayName: "Gender"
|
||||
file.path:
|
||||
displayName: "Location"
|
||||
|
||||
summaries:
|
||||
# Custom summary to show generation span
|
||||
generation_span: 'if(values.length > 0, values.max().year() - values.min().year(), 0)'
|
||||
|
||||
views:
|
||||
# Default view: All family members ordered by birth date
|
||||
- type: table
|
||||
name: "All Family Members"
|
||||
order:
|
||||
- note.born
|
||||
- file.name
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id" # Only show notes with cr_id
|
||||
summaries:
|
||||
note.born: Earliest
|
||||
note.died: Latest
|
||||
formula.full_lifespan: Average
|
||||
|
||||
# Living members only
|
||||
- type: table
|
||||
name: "Living Members"
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id"
|
||||
- "!note.died" # No death date
|
||||
order:
|
||||
- note.born
|
||||
summaries:
|
||||
formula.age_now: Average
|
||||
|
||||
# Deceased members only
|
||||
- type: table
|
||||
name: "Deceased Members"
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id"
|
||||
- "note.died" # Has death date
|
||||
order:
|
||||
- note.died
|
||||
summaries:
|
||||
formula.full_lifespan: Average
|
||||
note.died: Latest
|
||||
|
||||
# Recent additions (last 30 days)
|
||||
- type: table
|
||||
name: "Recently Added"
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id"
|
||||
- "file.ctime > now() - '30 days'"
|
||||
order:
|
||||
- file.ctime
|
||||
limit: 20
|
||||
|
||||
# Members without parents defined
|
||||
- type: table
|
||||
name: "Missing Parents"
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id"
|
||||
- "!note.father && !note.mother"
|
||||
order:
|
||||
- file.name
|
||||
|
||||
# Members without complete data
|
||||
- type: table
|
||||
name: "Incomplete Data"
|
||||
filters:
|
||||
and:
|
||||
- "note.cr_id"
|
||||
- "!note.born || !note.name"
|
||||
order:
|
||||
- file.name
|
||||
376
docs/bases-integration.md
Normal file
376
docs/bases-integration.md
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
# Bases Integration Guide
|
||||
|
||||
Canvas Roots is designed to work seamlessly with [Obsidian Bases](https://help.obsidian.md/bases), the core plugin for managing structured data in table views.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Why Use Bases with Canvas Roots?](#why-use-bases-with-canvas-roots)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Property Reference](#property-reference)
|
||||
- [Example Views](#example-views)
|
||||
- [Advanced Formulas](#advanced-formulas)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
**Bases** provides a table-based interface for viewing and editing note properties, making it ideal for managing genealogical data:
|
||||
|
||||
- **Table View**: Edit multiple people at once in a spreadsheet-like interface
|
||||
- **Filtering**: Focus on specific family members or data subsets
|
||||
- **Formulas**: Calculate ages, lifespans, and other derived values
|
||||
- **Sorting**: Organize by birth date, name, or any property
|
||||
- **Summaries**: Aggregate statistics across your family tree
|
||||
|
||||
**Canvas Roots** reads the same YAML frontmatter properties that Bases edits, creating a powerful dual-entry workflow:
|
||||
|
||||
```
|
||||
Individual Notes ←→ Bases Table View
|
||||
↓
|
||||
Canvas Roots
|
||||
↓
|
||||
Family Tree Visualization
|
||||
```
|
||||
|
||||
## Why Use Bases with Canvas Roots?
|
||||
|
||||
### Bulk Data Entry
|
||||
Edit multiple family members in a single table view instead of opening individual notes.
|
||||
|
||||
### Data Validation
|
||||
Quickly spot missing data, inconsistencies, or errors across your entire family tree.
|
||||
|
||||
### Calculated Fields
|
||||
Create formulas to automatically calculate:
|
||||
- Current age of living members
|
||||
- Lifespan of deceased members
|
||||
- Generation spans
|
||||
- Data completeness metrics
|
||||
|
||||
### Flexible Filtering
|
||||
Create custom views for:
|
||||
- Living vs. deceased members
|
||||
- Members missing parent information
|
||||
- Recently added people
|
||||
- Specific generations or branches
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Enable Bases Plugin
|
||||
|
||||
Bases is a core Obsidian plugin:
|
||||
|
||||
1. Open Settings → Core plugins
|
||||
2. Enable "Bases"
|
||||
|
||||
### 2. Copy the Template
|
||||
|
||||
Canvas Roots provides a ready-to-use Base template:
|
||||
|
||||
1. Navigate to `docs/assets/templates/family-members.base`
|
||||
2. Copy the file to your vault
|
||||
3. Rename it as desired (e.g., `My Family.base`)
|
||||
|
||||
### 3. Open the Base
|
||||
|
||||
Double-click the `.base` file to open the table view. You should see all notes with a `cr_id` property.
|
||||
|
||||
### 4. Start Editing
|
||||
|
||||
Click any cell to edit properties. Changes are immediately saved to the note's YAML frontmatter.
|
||||
|
||||
## Property Reference
|
||||
|
||||
Canvas Roots uses these core properties in note frontmatter:
|
||||
|
||||
### Required Properties
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `cr_id` | String | Unique identifier (UUID) | `abc-123-def-456` |
|
||||
|
||||
### Relationship Properties
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `father` | Link | Link to father's note | `[[John Smith]]` |
|
||||
| `mother` | Link | Link to mother's note | `[[Jane Doe]]` |
|
||||
| `spouse` | Link or List | Link(s) to spouse(s) | `[[Mary Jones]]` or `["[[Mary]]", "[[Sarah]]"]` |
|
||||
| `child` | Link or List | Link(s) to children | `["[[Bob]]", "[[Alice]]"]` |
|
||||
|
||||
### Biographical Properties
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `name` | String | Full name | `John Robert Smith` |
|
||||
| `born` | Date | Birth date | `1888-05-15` |
|
||||
| `died` | Date | Death date | `1952-08-20` |
|
||||
| `gender` | String | Gender (optional) | `M`, `F`, or custom |
|
||||
|
||||
### Optional Properties
|
||||
|
||||
| Property | Type | Description | Example |
|
||||
|----------|------|-------------|---------|
|
||||
| `birthPlace` | String | Place of birth | `Boston, MA` |
|
||||
| `deathPlace` | String | Place of death | `New York, NY` |
|
||||
| `occupation` | String or List | Occupation(s) | `Teacher` or `["Farmer", "Soldier"]` |
|
||||
|
||||
### Internal Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `cr_root` | Boolean | Marks this person as tree root (set by plugin) |
|
||||
|
||||
## Example Views
|
||||
|
||||
The template includes several pre-configured views:
|
||||
|
||||
### All Family Members
|
||||
Default view showing everyone with a `cr_id`, sorted by birth date.
|
||||
|
||||
**Use case**: Overview of your entire family tree
|
||||
|
||||
### Living Members
|
||||
Shows only people without a `died` date.
|
||||
|
||||
**Use case**: Focus on living relatives, calculate current ages
|
||||
|
||||
### Deceased Members
|
||||
Shows only people with a `died` date.
|
||||
|
||||
**Use case**: Historical research, lifespan analysis
|
||||
|
||||
### Recently Added
|
||||
Shows people added in the last 30 days.
|
||||
|
||||
**Use case**: Track recent research progress
|
||||
|
||||
### Missing Parents
|
||||
Shows people without `father` or `mother` defined.
|
||||
|
||||
**Use case**: Identify gaps in your tree, prioritize research
|
||||
|
||||
### Incomplete Data
|
||||
Shows people missing `born` or `name` properties.
|
||||
|
||||
**Use case**: Data quality checks
|
||||
|
||||
## Advanced Formulas
|
||||
|
||||
### Calculating Current Age
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
age_now: 'if(born && !died, now().year() - born.year(), "")'
|
||||
```
|
||||
|
||||
This calculates the age of living members based on their birth date.
|
||||
|
||||
### Lifespan Calculation
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
full_lifespan: 'if(born && died, died.year() - born.year() + " years", "")'
|
||||
```
|
||||
|
||||
Shows how many years a deceased person lived.
|
||||
|
||||
### Display Name Fallback
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
display_name: 'name || file.name'
|
||||
```
|
||||
|
||||
Shows the `name` property if present, otherwise uses the filename.
|
||||
|
||||
### Generation Span
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
birth_decade: 'if(born, Math.floor(born.year() / 10) * 10 + "s", "")'
|
||||
```
|
||||
|
||||
Groups people by the decade they were born in.
|
||||
|
||||
### Data Completeness
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
completeness: '((!!name + !!born + !!died + !!father + !!mother) / 5 * 100).toFixed(0) + "%"'
|
||||
```
|
||||
|
||||
Shows what percentage of core data fields are filled in.
|
||||
|
||||
### Relationship Count
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
child_count: 'list(child).length'
|
||||
spouse_count: 'list(spouse).length'
|
||||
```
|
||||
|
||||
Counts the number of children or spouses (handles both single values and arrays).
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Consistent Date Formats
|
||||
|
||||
Always use ISO format for dates: `YYYY-MM-DD`
|
||||
|
||||
✅ Good: `1888-05-15`
|
||||
❌ Bad: `May 15, 1888` or `15/5/1888`
|
||||
|
||||
Bases can parse and manipulate ISO dates with built-in date functions.
|
||||
|
||||
### 2. Use Wikilinks for Relationships
|
||||
|
||||
Always use wikilink syntax for relationships:
|
||||
|
||||
✅ Good: `[[John Smith]]`
|
||||
❌ Bad: `John Smith`
|
||||
|
||||
Wikilinks are automatically recognized as Link objects in Bases and are clickable.
|
||||
|
||||
### 3. Handle Arrays Consistently
|
||||
|
||||
For `spouse` and `child`, always use arrays if there's a possibility of multiple values:
|
||||
|
||||
```yaml
|
||||
spouse: ["[[Mary Jones]]"] # Array with one element
|
||||
child: ["[[Bob]]", "[[Alice]]"] # Array with multiple elements
|
||||
```
|
||||
|
||||
This prevents issues when adding additional relationships later.
|
||||
|
||||
### 4. Tag Person Notes
|
||||
|
||||
Add a `#person` tag to all family member notes:
|
||||
|
||||
```yaml
|
||||
---
|
||||
tags:
|
||||
- person
|
||||
cr_id: abc-123
|
||||
---
|
||||
```
|
||||
|
||||
This makes filtering easier and allows you to exclude non-person notes.
|
||||
|
||||
### 5. Use the `name` Property
|
||||
|
||||
While Canvas Roots can use the filename as a fallback, explicitly setting the `name` property gives you more flexibility:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: John Robert Smith
|
||||
---
|
||||
```
|
||||
|
||||
File can be renamed to `john-smith.md` without affecting display.
|
||||
|
||||
### 6. Create Multiple Views
|
||||
|
||||
Don't try to make one view do everything. Create specialized views for different tasks:
|
||||
|
||||
- **Data Entry View**: Minimal columns, easy to add new people
|
||||
- **Research View**: All fields visible, sorted by missing data
|
||||
- **Analysis View**: Formulas and summaries, sorted by patterns
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Base Shows No Results
|
||||
|
||||
**Problem**: Base table is empty even though person notes exist.
|
||||
|
||||
**Solutions**:
|
||||
1. Check that notes have the `cr_id` property
|
||||
2. Verify the `filters` section in your `.base` file
|
||||
3. Ensure notes are in the vault (not in `.obsidian` folder)
|
||||
|
||||
### Links Don't Work
|
||||
|
||||
**Problem**: Clicking a link in the base doesn't open the note.
|
||||
|
||||
**Solutions**:
|
||||
1. Ensure you're using wikilink syntax: `[[Note Name]]`
|
||||
2. Check that the linked note exists
|
||||
3. Verify the link is in the note frontmatter (not just the base formula)
|
||||
|
||||
### Formulas Show Errors
|
||||
|
||||
**Problem**: Formula cells show `ERROR` or blank values.
|
||||
|
||||
**Solutions**:
|
||||
1. Check formula syntax in the `.base` file
|
||||
2. Verify property names are correct (case-sensitive)
|
||||
3. Handle missing values with `if()` checks: `if(born, born.year(), "")`
|
||||
4. Use `list()` function for properties that might be single values or arrays
|
||||
|
||||
### Changes Don't Appear in Canvas
|
||||
|
||||
**Problem**: Edited data in Bases doesn't show in Canvas Roots tree.
|
||||
|
||||
**Solutions**:
|
||||
1. Run the "Re-Layout Current Canvas" command to refresh
|
||||
2. Verify changes were saved to note frontmatter (open the note to check)
|
||||
3. Check the Canvas is reading from the correct note files
|
||||
|
||||
### Arrays Display Strangely
|
||||
|
||||
**Problem**: Spouse or child arrays show as `[object Object]` or similar.
|
||||
|
||||
**Solutions**:
|
||||
1. Use `.length` to count: `list(child).length`
|
||||
2. Use `.join(", ")` to display as text: `list(spouse).map(s => s.toString()).join(", ")`
|
||||
3. Create a formula to format the list properly
|
||||
|
||||
## Integration with Canvas Roots Workflow
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. **Initial Setup**: Create person notes with basic data (name, dates)
|
||||
2. **Bulk Entry**: Use Bases to quickly add relationships and fill in missing data
|
||||
3. **Visualization**: Run Canvas Roots to generate the family tree
|
||||
4. **Refinement**: Identify gaps in the tree, use Bases to add missing people
|
||||
5. **Re-layout**: Update the Canvas with new data
|
||||
6. **Analysis**: Use Bases formulas to analyze patterns and data quality
|
||||
|
||||
### When to Use Bases vs. Individual Notes
|
||||
|
||||
**Use Bases when:**
|
||||
- Adding multiple people at once
|
||||
- Filling in the same property across many notes (e.g., adding birth dates)
|
||||
- Finding patterns or missing data
|
||||
- Doing data quality checks
|
||||
|
||||
**Use Individual Notes when:**
|
||||
- Writing detailed biographical information
|
||||
- Adding sources, images, or extensive notes
|
||||
- Creating complex narrative content
|
||||
- Linking to external research
|
||||
|
||||
### Bi-Directional Sync
|
||||
|
||||
If you enable bi-directional relationship sync in Canvas Roots settings:
|
||||
|
||||
1. Edit `father: [[John]]` in a Base
|
||||
2. Canvas Roots automatically adds `child: [[Current Note]]` to John's note
|
||||
3. The change appears immediately in the Base (if John is visible)
|
||||
4. The Canvas tree reflects the new relationship when re-laid out
|
||||
|
||||
This ensures relationship consistency across your entire tree.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Obsidian Bases Documentation](https://help.obsidian.md/bases)
|
||||
- [Bases Syntax Reference](https://help.obsidian.md/bases/syntax)
|
||||
- [Canvas Roots Property Reference](property-reference.md)
|
||||
- [Example Family Base Template](assets/templates/family-members.base)
|
||||
|
||||
## Questions?
|
||||
|
||||
- **General questions**: [GitHub Discussions](https://github.com/banisterious/obsidian-canvas-roots/discussions)
|
||||
- **Bug reports**: [GitHub Issues](https://github.com/banisterious/obsidian-canvas-roots/issues)
|
||||
- **Feature requests**: [GitHub Issues](https://github.com/banisterious/obsidian-canvas-roots/issues) with "enhancement" label
|
||||
151
docs/canvas-roots-initial-spec.md
Normal file
151
docs/canvas-roots-initial-spec.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Canvas Roots Plugin Specification
|
||||
|
||||
1. Plugin Overview
|
||||
|
||||
- Plugin Name: Canvas Roots
|
||||
- Goal: To automate the creation and layout of complex family trees within an Obsidian Canvas, using structured note data as the source, driven by a powerful D3.js-based layout algorithm.
|
||||
- Key Differentiator: It uses the D3 layout engine for positioning, but renders the output as native, linked Obsidian Canvas nodes and edges, allowing for full user customization and contextual linking.
|
||||
- Core Workflow: User inputs data into person notes → User runs a command on a blank or existing Canvas → Plugin calculates D3 coordinates → Plugin writes the structured data to the Canvas JSON file.
|
||||
|
||||
2. Data Model: The Canvas Roots Schema
|
||||
|
||||
The plugin must primarily source data from the Obsidian note's YAML frontmatter or inline fields (preferred for DataView compatibility).
|
||||
|
||||
2.1 Person Note Fields
|
||||
|
||||
The core data for each person node will be stored in individual Markdown notes. The plugin must read and write these properties automatically.
|
||||
|
||||
- cr_id (Unique String - UUID): MANDATORY. Unique identifier for the person. Used internally for D3 layout and GEDCOM mapping. Should be generated on note creation.
|
||||
- name (String): Display name (defaults to file title).
|
||||
- father ([[Link]]): Link to the father's note file.
|
||||
- mother ([[Link]]): Link to the mother's note file.
|
||||
- spouse ([[Link]] / Array<[[Link]]>): Link(s) to partner(s)/spouse(s).
|
||||
- born (Date YYYY-MM-DD): Birth date (used for display/sorting).
|
||||
- died (Date YYYY-MM-DD): Death date.
|
||||
- cr_root (Boolean): true if this person is currently the center of the rendered tree (optional filter).
|
||||
|
||||
2.2 Bi-Directional Link Automation (MVP Feature)
|
||||
|
||||
The plugin must implement a function that automatically creates the inverse link when a primary relationship is created or modified.
|
||||
|
||||
- Example 1 (Parent/Child): If Father:: [[John Smith]] is added to Jane's note, the plugin must check John Smith's note and ensure Child:: [[Jane Doe]] exists (or vice versa).
|
||||
- Example 2 (Spouse): If Spouse:: [[Jane Doe]] is added to John Smith's note, the plugin must ensure Spouse:: [[John Smith]] is added to Jane Doe's note.
|
||||
|
||||
3. Core Feature: D3-to-Canvas Rendering
|
||||
|
||||
The plugin will use a command to generate the family tree structure directly into the currently open Canvas file (.canvas).
|
||||
|
||||
3.1 Command and Input
|
||||
|
||||
- Command: Canvas Roots: Generate Tree for Current Note
|
||||
- Action: The command is triggered while viewing a Person's note (the "Root Person").
|
||||
|
||||
3.2 The Rendering Process
|
||||
|
||||
- Data Extraction: The plugin recursively fetches all connected person notes (ancestors and descendants) starting from the Root Person, reading the relationship links.
|
||||
- Layout Calculation: The raw graph data is transformed into a D3 hierarchy structure. The D3 layout engine, leveraging algorithms suitable for multi-parent graphs (like those found in libraries such as family-chart), is executed internally to calculate the precise x and y coordinates for every node to create a non-overlapping pedigree/descendant chart.
|
||||
- Canvas JSON Generation:
|
||||
- The plugin reads the existing Canvas file contents (JSON).
|
||||
- For every person, it generates a new Canvas Node ("type": "file") at the calculated D3 position, linking the node to the person's Markdown file.
|
||||
- It generates Canvas Edges to represent all parent-child and spouse relationships.
|
||||
- Relayout Command: A secondary command (Canvas Roots: Re-Layout Current Canvas) must exist to re-run the layout calculation on existing Canvas nodes. This allows the user to click the button to snap nodes back into the D3-calculated structure after manual rearrangement.
|
||||
|
||||
4. Technical Dependencies & Canvas Implementation
|
||||
|
||||
4.1 Obsidian API Requirements
|
||||
|
||||
- Obsidian API (TypeScript): Essential for core function.
|
||||
- D3.js Layout Logic: The specific logic for a multi-parent tree layout, similar to that provided by the family-chart library's algorithm, is required for coordinate calculation.
|
||||
- File I/O: app.vault.read()/app.vault.write() are necessary for modifying the Canvas JSON.
|
||||
- UI: Notice is required for user feedback.
|
||||
|
||||
4.2 Canvas JSON Structure Requirements
|
||||
|
||||
The plugin must accurately read and write the Canvas file as defined by the following minimal structure:
|
||||
|
||||
```
|
||||
{
|
||||
"nodes": [
|
||||
// Node for a Person (File Node)
|
||||
{
|
||||
"id": "node-uuid-1", // A unique Canvas ID
|
||||
"x": 0, // D3 calculated X-coordinate
|
||||
"y": 0, // D3 calculated Y-coordinate
|
||||
"width": 200, // Fixed or calculated width
|
||||
"height": 100, // Fixed or calculated height
|
||||
"type": "file", // Node type: always 'file' for a person
|
||||
"file": "path/to/person/note.md",
|
||||
"color": "6" // Optional color code for styling
|
||||
},
|
||||
// Node for a Marriage (Pure Text Node or Group) - OPTIONAL for MVP
|
||||
{
|
||||
"id": "node-uuid-2",
|
||||
"x": 50,
|
||||
"y": 150,
|
||||
"width": 50,
|
||||
"height": 30,
|
||||
"type": "text",
|
||||
"text": "&" // Symbol representing a union
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
// Edge for a Relationship (e.g., Parent -> Child)
|
||||
{
|
||||
"id": "edge-uuid-3", // Unique Canvas ID
|
||||
"fromNode": "node-uuid-1", // ID of the parent node
|
||||
"fromSide": "bottom", // Side of the parent node to connect from
|
||||
"toNode": "node-uuid-4", // ID of the child node
|
||||
"toSide": "top" // Side of the child node to connect to
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
4.3 ID Mapping and Persistence Logic
|
||||
|
||||
The plugin must maintain a relationship between the stable person data and the transient Canvas representation:
|
||||
|
||||
1. Node Identification: When reading the Canvas JSON, the plugin must iterate through existing nodes. If a node is of type "file", it must extract the note file path ("file": "...").
|
||||
2. ID Synchronization: The plugin must use the cr_id property inside the person's Markdown note as the primary, stable key for the D3 calculation. The Canvas node id is a transient value.
|
||||
3. Position Update: When running the Relayout command, the plugin must match the calculated D3 coordinates (x, y) to the existing Canvas node based on the linked file path, ensuring existing notes are moved, not duplicated.
|
||||
4. Coordinate Offset: The final calculated x and y coordinates from the D3 layout must be offset (e.g., added to the current Canvas center position) before being written to the Canvas JSON to ensure the new tree segment appears within the user's current viewport.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5. Data Interchange & Advanced Features
|
||||
|
||||
This section outlines features related to importing external genealogical data (GEDCOM) and enabling deep data analysis, ensuring flexibility for users who prefer either quick visualization or deep vault synchronization.
|
||||
|
||||
5.1 GEDCOM Integration Modes
|
||||
|
||||
GEDCOM (Genealogical Data Communication) is the industry standard for family tree data. The plugin must support two distinct ingestion methods via a configuration setting or command option:
|
||||
|
||||
- Mode 1: Canvas Visualization (Quick Import)
|
||||
- Function: Accepts a .ged file and parses the relationship data directly into the Canvas JSON format.
|
||||
- Output: Creates a single Canvas file containing linked, D3-positioned file nodes and edges. Crucially, it does not create individual Markdown notes in the vault, allowing users to visualize an external tree quickly without cluttering their main note structure.
|
||||
|
||||
Mode 2: Vault Deep-Sync (Full Integration)
|
||||
- Function: Accepts a `.ged` file and parses the complete data structure.
|
||||
- Output: Creates a new Markdown file for every individual, place, and source mentioned in the GEDCOM.
|
||||
- Property Population: Populates the YAML frontmatter/Properties with all relevant GEDCOM fields (birth/death dates, locations, sources).
|
||||
- Bi-directional Links: Automatically establishes [[Link]] connections within the notes to support the relationship tracking defined in Section 2.2.
|
||||
- Rich Data/Media: Includes logic to handle and parse links to multimedia or extensive notes referenced in the GEDCOM file.
|
||||
|
||||
5.2 Round-Trip Data Synchronization
|
||||
|
||||
A key goal is maintaining data integrity across systems:
|
||||
|
||||
- Property Population: All parsed GEDCOM fields must be written into the corresponding Obsidian note's frontmatter (Properties), ensuring maximum compatibility with tools like Dataview or Obsidian's built-in Bases for queries and analysis.
|
||||
- GEDCOM Export (Round-Trip): The plugin must feature a command to compile the current vault data (based on all notes containing a `cr_id`) back into a valid `.ged` file, allowing users to move their edited data back to dedicated genealogy software.
|
||||
|
||||
5.3 Relationship Breadth & Analysis
|
||||
|
||||
The visualization and data model must support the complexity inherent in genealogical data:
|
||||
|
||||
- Expanded Relationships: The D3 layout logic (from the chosen algorithm) must be configured to support visualizing sibling chains and the spouses of siblings, extending the tree beyond direct ancestral lines.
|
||||
- Analysis Queries: The plugin should expose utility functions or pre-built DataViewJS templates to allow users to query their rich, structured data (e.g., "List all living descendants," "Table of individuals by age at death," or showing notes linked to specific sources).
|
||||
259
docs/css-system.md
Normal file
259
docs/css-system.md
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# CSS Build System
|
||||
|
||||
## Overview
|
||||
|
||||
Canvas Roots uses a component-based CSS build system that automatically concatenates, lints, and formats CSS files from the `styles/` directory into a single `styles.css` file for Obsidian.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
styles/
|
||||
├── variables.css # CSS custom properties and design tokens
|
||||
├── base.css # Base structural elements
|
||||
├── layout.css # Layout utilities
|
||||
├── canvas.css # Canvas-specific styling
|
||||
├── nodes.css # Family tree node styling
|
||||
├── edges.css # Relationship edge styling
|
||||
├── settings.css # Settings interface
|
||||
├── modals.css # Modal dialogs
|
||||
├── animations.css # Keyframes and transitions
|
||||
├── responsive.css # Responsive breakpoints
|
||||
└── theme.css # Theme compatibility
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Building CSS
|
||||
|
||||
```bash
|
||||
# Full build with linting and formatting
|
||||
npm run build:css
|
||||
|
||||
# Build only (skip linting)
|
||||
npm run build:css -- --build-only
|
||||
|
||||
# Watch mode for development
|
||||
npm run build:css:watch
|
||||
```
|
||||
|
||||
### Linting CSS
|
||||
|
||||
```bash
|
||||
# Lint CSS files
|
||||
npm run lint:css
|
||||
|
||||
# Lint and auto-fix
|
||||
npm run lint:css:fix
|
||||
```
|
||||
|
||||
### Formatting CSS
|
||||
|
||||
```bash
|
||||
# Format CSS files with Prettier
|
||||
npm run format:css
|
||||
```
|
||||
|
||||
## Build Process
|
||||
|
||||
The CSS build system follows this pipeline:
|
||||
|
||||
1. **Format** - Prettier formats all component files
|
||||
2. **Lint** - Stylelint checks for errors and enforces rules
|
||||
3. **Build** - Components are concatenated in dependency order
|
||||
4. **Output** - Final `styles.css` is generated with build metadata
|
||||
|
||||
## Component Order
|
||||
|
||||
Components are concatenated in a specific order to ensure proper CSS cascade:
|
||||
|
||||
1. Variables (CSS custom properties)
|
||||
2. Base styles
|
||||
3. Layout utilities
|
||||
4. Feature-specific components
|
||||
5. Animations
|
||||
6. Responsive styles
|
||||
7. Theme compatibility (last)
|
||||
|
||||
## CSS Naming Conventions
|
||||
|
||||
### Class Names
|
||||
|
||||
Use BEM-style naming with `cr-` or `canvas-roots-` prefix:
|
||||
|
||||
```css
|
||||
/* Block */
|
||||
.cr-person-node { }
|
||||
|
||||
/* Block with element */
|
||||
.cr-person-node__name { }
|
||||
|
||||
/* Block with modifier */
|
||||
.cr-person-node--highlighted { }
|
||||
```
|
||||
|
||||
### Custom Properties
|
||||
|
||||
Use `--cr-` prefix for Canvas Roots variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--cr-spacing-md: 16px;
|
||||
--cr-node-width: 200px;
|
||||
--cr-primary: var(--interactive-accent);
|
||||
}
|
||||
```
|
||||
|
||||
Obsidian's variables (prefixed with `--`) can be used directly for theme compatibility.
|
||||
|
||||
## Stylelint Configuration
|
||||
|
||||
The project uses Stylelint with these configurations:
|
||||
|
||||
- `stylelint-config-standard` - Standard CSS rules
|
||||
- `stylelint-config-prettier` - Prettier compatibility
|
||||
|
||||
### Key Rules
|
||||
|
||||
- **Class Pattern**: `^(cr|canvas-roots)-[a-z0-9-]+(__[a-z0-9-]+)?(--[a-z0-9-]+)?$`
|
||||
- **Custom Property Pattern**: `^(md|cr)-[a-z0-9-]+$`
|
||||
- **Max Nesting Depth**: 3 levels
|
||||
- **String Quotes**: Double quotes
|
||||
- **Color Hex**: Short notation, lowercase
|
||||
|
||||
### Rule Overrides
|
||||
|
||||
Some files have relaxed rules:
|
||||
|
||||
- `variables.css` - No custom property pattern enforcement
|
||||
- `theme.css` - No class pattern enforcement (allows `.theme-light`, `.theme-dark`)
|
||||
|
||||
## Adding New Components
|
||||
|
||||
To add a new CSS component:
|
||||
|
||||
1. **Create the file** in `styles/` directory:
|
||||
```bash
|
||||
touch styles/my-component.css
|
||||
```
|
||||
|
||||
2. **Add to build order** in `build-css.js`:
|
||||
```javascript
|
||||
componentOrder: [
|
||||
// ... existing components
|
||||
'my-component.css', // Add here in correct order
|
||||
// ... remaining components
|
||||
]
|
||||
```
|
||||
|
||||
3. **Write your CSS** following naming conventions
|
||||
|
||||
4. **Build** to generate updated `styles.css`:
|
||||
```bash
|
||||
npm run build:css
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Watch Mode
|
||||
|
||||
For active development, use watch mode:
|
||||
|
||||
```bash
|
||||
npm run build:css:watch
|
||||
```
|
||||
|
||||
This will:
|
||||
- Watch for changes in `styles/` directory
|
||||
- Automatically rebuild on file changes
|
||||
- Show build status and errors in real-time
|
||||
|
||||
### Testing Styles
|
||||
|
||||
1. Build CSS: `npm run build:css`
|
||||
2. Deploy to vault: `npm run deploy`
|
||||
3. Reload Obsidian (Ctrl/Cmd + R)
|
||||
|
||||
## Integration with Main Build
|
||||
|
||||
The CSS build is automatically integrated into the main build process:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This runs:
|
||||
1. TypeScript compilation
|
||||
2. JavaScript bundling (esbuild)
|
||||
3. CSS building (with `--no-fail-on-lint` flag)
|
||||
|
||||
The `--no-fail-on-lint` flag allows the build to continue even if there are CSS linting warnings, but errors will still stop the build.
|
||||
|
||||
## Theme Compatibility
|
||||
|
||||
### Using Obsidian Variables
|
||||
|
||||
Always prefer Obsidian's CSS variables for colors and common properties:
|
||||
|
||||
```css
|
||||
.cr-my-element {
|
||||
color: var(--text-normal); /* Text color */
|
||||
background: var(--background-primary); /* Background */
|
||||
border-color: var(--background-modifier-border); /* Borders */
|
||||
}
|
||||
```
|
||||
|
||||
### Light and Dark Themes
|
||||
|
||||
Use theme-specific overrides in `theme.css`:
|
||||
|
||||
```css
|
||||
.theme-light {
|
||||
/* Light theme specific overrides */
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
/* Dark theme specific overrides */
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails at Linting Stage
|
||||
|
||||
Check the error output for specific CSS issues:
|
||||
|
||||
```bash
|
||||
npm run lint:css
|
||||
```
|
||||
|
||||
Auto-fix common issues:
|
||||
|
||||
```bash
|
||||
npm run lint:css:fix
|
||||
```
|
||||
|
||||
### Orphaned Files Warning
|
||||
|
||||
If you see warnings about orphaned CSS files, either:
|
||||
- Add them to `componentOrder` in `build-css.js`
|
||||
- Or add them to `excludedFiles` if they shouldn't be included
|
||||
|
||||
### Component Not Included
|
||||
|
||||
Verify the component is:
|
||||
1. In the `styles/` directory
|
||||
2. Listed in `componentOrder` array
|
||||
3. Has a `.css` extension
|
||||
|
||||
## Performance
|
||||
|
||||
The CSS build is fast:
|
||||
- Typical build time: <100ms
|
||||
- Full pipeline (format + lint + build): <1 second
|
||||
- Watch mode rebuild: Near-instant
|
||||
|
||||
## References
|
||||
|
||||
- [Stylelint Documentation](https://stylelint.io/)
|
||||
- [Prettier Documentation](https://prettier.io/)
|
||||
- [Obsidian CSS Variables](https://docs.obsidian.md/Reference/CSS+variables/)
|
||||
137
docs/development.md
Normal file
137
docs/development.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Development Guide
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
- `npm run dev` - Start development mode with watch (builds to local main.js)
|
||||
- `npm run build` - Production build with type checking
|
||||
- `npm run lint` - Check code for linting errors
|
||||
- `npm run lint:fix` - Auto-fix linting errors
|
||||
|
||||
## Deployment to Obsidian Vault
|
||||
|
||||
### Quick Deploy
|
||||
Deploy the built plugin to your Obsidian vault:
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Build the plugin (`npm run build`)
|
||||
2. Copy `main.js`, `manifest.json`, and `styles.css` to your vault's plugin directory
|
||||
3. You'll need to reload Obsidian to see changes (Ctrl+R or Cmd+R)
|
||||
|
||||
### Development with Auto-Deploy
|
||||
For active development with automatic deployment on file changes:
|
||||
|
||||
```bash
|
||||
npm run dev:deploy
|
||||
```
|
||||
|
||||
**Note:** This requires `inotify-tools` to be installed:
|
||||
```bash
|
||||
sudo apt-get install inotify-tools
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Watch for TypeScript file changes
|
||||
- Automatically rebuild
|
||||
- Auto-deploy to vault
|
||||
- Show build status and timestamp
|
||||
|
||||
### Vault Path Configuration
|
||||
|
||||
The deployment scripts target:
|
||||
```
|
||||
/mnt/d/Vaults/Banister/.obsidian/plugins/canvas-roots
|
||||
```
|
||||
|
||||
To change this, edit the `VAULT_PATH` variable in:
|
||||
- [deploy.sh](../deploy.sh)
|
||||
- [dev-deploy.sh](../dev-deploy.sh)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
canvas-roots/
|
||||
├── main.ts # Plugin entry point
|
||||
├── src/
|
||||
│ ├── settings.ts # Plugin settings
|
||||
│ ├── models/ # Data models
|
||||
│ │ ├── person.ts # Person/family data structures
|
||||
│ │ └── canvas.ts # Canvas node/edge types
|
||||
│ ├── canvas/ # Canvas manipulation (to be implemented)
|
||||
│ ├── layout/ # D3 layout algorithms (to be implemented)
|
||||
│ ├── utils/ # Utility functions (to be implemented)
|
||||
│ └── gedcom/ # GEDCOM import/export (to be implemented)
|
||||
├── docs/ # Documentation
|
||||
├── manifest.json # Obsidian plugin metadata
|
||||
├── package.json # NPM configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── esbuild.config.mjs # Build configuration
|
||||
├── .eslintrc.json # ESLint configuration
|
||||
└── styles.css # Optional plugin styles
|
||||
```
|
||||
|
||||
## Testing in Obsidian
|
||||
|
||||
1. Build and deploy the plugin: `npm run deploy`
|
||||
2. Open Obsidian
|
||||
3. Go to Settings → Community plugins
|
||||
4. Enable "Canvas Roots"
|
||||
5. The plugin commands will be available in the Command Palette (Ctrl/Cmd+P):
|
||||
- "Canvas Roots: Generate Tree for Current Note"
|
||||
- "Canvas Roots: Re-Layout Current Canvas"
|
||||
|
||||
### Reloading After Changes
|
||||
|
||||
After deploying changes, reload Obsidian:
|
||||
- **Quick reload**: Press Ctrl+R (Windows/Linux) or Cmd+R (Mac)
|
||||
- **Full reload**: Settings → Community plugins → Toggle plugin off/on
|
||||
|
||||
## Hot Reload (Advanced)
|
||||
|
||||
For instant plugin reloading without restarting Obsidian:
|
||||
|
||||
1. Install the [Hot Reload](https://github.com/pjeby/hot-reload) plugin
|
||||
2. It will automatically detect changes to `main.js` in your vault's plugin directory
|
||||
3. Use `npm run dev:deploy` to build and deploy on file changes
|
||||
4. Hot Reload will automatically reload the plugin
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logs
|
||||
Open the Developer Console in Obsidian:
|
||||
- Windows/Linux: Ctrl+Shift+I
|
||||
- Mac: Cmd+Option+I
|
||||
|
||||
Look for:
|
||||
- "Loading Canvas Roots plugin" when the plugin loads
|
||||
- Any error messages or console logs
|
||||
|
||||
### TypeScript Errors
|
||||
The build command includes type checking:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This will show any TypeScript errors before building.
|
||||
|
||||
## Version Management
|
||||
|
||||
When ready to release a new version:
|
||||
|
||||
1. Update the version in `package.json`
|
||||
2. Run the version bump script:
|
||||
```bash
|
||||
npm version patch # or minor, or major
|
||||
```
|
||||
|
||||
This will automatically update `manifest.json` and `versions.json`.
|
||||
63
docs/eslint-setup.md
Normal file
63
docs/eslint-setup.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# ESLint Configuration
|
||||
|
||||
## Current Setup
|
||||
|
||||
The project uses ESLint v8 with TypeScript ESLint for code linting.
|
||||
|
||||
### Configuration File
|
||||
- [.eslintrc.json](../.eslintrc.json) - Standard TypeScript ESLint configuration
|
||||
|
||||
### Available Commands
|
||||
- `npm run lint` - Check code for linting errors
|
||||
- `npm run lint:fix` - Automatically fix linting errors where possible
|
||||
|
||||
## Obsidian ESLint Plugin Status
|
||||
|
||||
The `eslint-plugin-obsidianmd` package is installed but **not currently active** in the configuration.
|
||||
|
||||
### Why?
|
||||
The Obsidian ESLint plugin is ESM-only and requires ESLint v9+ with flat config (`eslint.config.mjs`). However, our current `@typescript-eslint` packages (v6.x) only support ESLint v7-8.
|
||||
|
||||
### Future Upgrade Path
|
||||
|
||||
When upgrading in the future:
|
||||
|
||||
1. Upgrade TypeScript ESLint packages to v8.x:
|
||||
```bash
|
||||
npm install --save-dev @typescript-eslint/eslint-plugin@^8.0.0 @typescript-eslint/parser@^8.0.0
|
||||
```
|
||||
|
||||
2. Upgrade ESLint to v9:
|
||||
```bash
|
||||
npm install --save-dev eslint@^9.0.0
|
||||
```
|
||||
|
||||
3. Convert `.eslintrc.json` to `eslint.config.mjs` with flat config format:
|
||||
```javascript
|
||||
import tsparser from "@typescript-eslint/parser";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default [
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: { project: "./tsconfig.json" },
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Obsidian-Specific Best Practices (Manual)
|
||||
|
||||
Until the plugin is active, follow these Obsidian best practices manually:
|
||||
|
||||
1. **Never use `workspace.getActiveViewOfType(null)`** - Use `workspace.getActiveFile()` instead
|
||||
2. **Use `app.fileManager.trashFile()` or `app.vault.trash()`** instead of `app.vault.delete()`
|
||||
3. **Avoid direct DOM manipulation** - Use Obsidian's API methods
|
||||
4. **Test in both desktop and mobile** if `isDesktopOnly: false` in manifest
|
||||
|
||||
## References
|
||||
- [Obsidian ESLint Plugin](https://github.com/obsidianmd/eslint-plugin)
|
||||
- [ESLint v9 Migration Guide](https://eslint.org/docs/latest/use/configure/migration-guide)
|
||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === 'production');
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['main.ts'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
'electron',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/language',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/view',
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
...builtins],
|
||||
format: 'cjs',
|
||||
target: 'es2022',
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: 'main.js',
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
81
main.ts
Normal file
81
main.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { Plugin, Notice } from 'obsidian';
|
||||
import { CanvasRootsSettings, DEFAULT_SETTINGS, CanvasRootsSettingTab } from './src/settings';
|
||||
|
||||
export default class CanvasRootsPlugin extends Plugin {
|
||||
settings: CanvasRootsSettings;
|
||||
|
||||
async onload() {
|
||||
console.log('Loading Canvas Roots plugin');
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new CanvasRootsSettingTab(this.app, this));
|
||||
|
||||
// Add command: Generate Tree for Current Note
|
||||
this.addCommand({
|
||||
id: 'generate-tree-for-current-note',
|
||||
name: 'Generate Tree for Current Note',
|
||||
callback: () => {
|
||||
this.generateTreeForCurrentNote();
|
||||
}
|
||||
});
|
||||
|
||||
// Add command: Re-Layout Current Canvas
|
||||
this.addCommand({
|
||||
id: 'relayout-current-canvas',
|
||||
name: 'Re-Layout Current Canvas',
|
||||
callback: () => {
|
||||
this.relayoutCurrentCanvas();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
console.log('Unloading Canvas Roots plugin');
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
private async generateTreeForCurrentNote() {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
|
||||
if (!activeFile) {
|
||||
new Notice('No active note. Please open a person note first.');
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(`Generating tree for: ${activeFile.basename}`);
|
||||
|
||||
// TODO: Implement tree generation logic
|
||||
// 1. Extract person data from current note
|
||||
// 2. Traverse relationships to build family graph
|
||||
// 3. Calculate D3 layout
|
||||
// 4. Generate Canvas nodes and edges
|
||||
// 5. Write to Canvas file
|
||||
}
|
||||
|
||||
private async relayoutCurrentCanvas() {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
|
||||
if (!activeFile || activeFile.extension !== 'canvas') {
|
||||
new Notice('No active canvas. Please open a canvas file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice('Re-layouting canvas...');
|
||||
|
||||
// TODO: Implement relayout logic
|
||||
// 1. Read current Canvas JSON
|
||||
// 2. Extract existing nodes and their linked files
|
||||
// 3. Recalculate D3 layout
|
||||
// 4. Update node positions in Canvas JSON
|
||||
// 5. Write back to Canvas file
|
||||
}
|
||||
}
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "canvas-roots",
|
||||
"name": "Canvas Roots",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "Automate the creation and layout of complex family trees within Obsidian Canvas using D3.js-based layout algorithms.",
|
||||
"author": "banisterious",
|
||||
"authorUrl": "https://github.com/banisterious/obsidian-canvas-roots",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
8285
package-lock.json
generated
Normal file
8285
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
57
package.json
Normal file
57
package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "obsidian-canvas-roots",
|
||||
"version": "0.1.0",
|
||||
"description": "Obsidian plugin for creating family trees in Canvas using D3.js layouts",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production && npm run build:css -- --no-fail-on-lint",
|
||||
"build:css": "node build-css.js",
|
||||
"build:css:watch": "node build-css.js --watch",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:css": "stylelint \"styles/**/*.css\"",
|
||||
"lint:css:fix": "stylelint \"styles/**/*.css\" --fix",
|
||||
"format:css": "prettier --write \"styles/**/*.css\"",
|
||||
"deploy": "./deploy.sh",
|
||||
"dev:deploy": "./dev-deploy.sh"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"plugin",
|
||||
"family-tree",
|
||||
"genealogy",
|
||||
"canvas",
|
||||
"d3"
|
||||
],
|
||||
"author": "banisterious",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/banisterious/obsidian-canvas-roots.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^20.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"esbuild": "0.19.11",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-obsidianmd": "^0.1.9",
|
||||
"obsidian": "latest",
|
||||
"prettier": "^3.6.2",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"tslib": "2.6.2",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"d3-dag": "^1.1.0"
|
||||
}
|
||||
}
|
||||
39
src/models/canvas.ts
Normal file
39
src/models/canvas.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Canvas data structures matching Obsidian's Canvas file format
|
||||
*/
|
||||
|
||||
export interface CanvasNode {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type: 'file' | 'text' | 'link' | 'group';
|
||||
file?: string;
|
||||
text?: string;
|
||||
url?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface CanvasEdge {
|
||||
id: string;
|
||||
fromNode: string;
|
||||
fromSide: 'top' | 'right' | 'bottom' | 'left';
|
||||
toNode: string;
|
||||
toSide: 'top' | 'right' | 'bottom' | 'left';
|
||||
color?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface CanvasData {
|
||||
nodes: CanvasNode[];
|
||||
edges: CanvasEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship types for edges
|
||||
*/
|
||||
export enum RelationshipType {
|
||||
PARENT_CHILD = 'parent-child',
|
||||
SPOUSE = 'spouse'
|
||||
}
|
||||
59
src/models/person.ts
Normal file
59
src/models/person.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Person data model matching the Canvas Roots Schema
|
||||
*/
|
||||
export interface Person {
|
||||
/** Unique identifier (UUID) - MANDATORY */
|
||||
cr_id: string;
|
||||
|
||||
/** Display name (defaults to file title) */
|
||||
name: string;
|
||||
|
||||
/** Path to the person's note file */
|
||||
filePath: string;
|
||||
|
||||
/** Link to father's note */
|
||||
father?: string;
|
||||
|
||||
/** Link to mother's note */
|
||||
mother?: string;
|
||||
|
||||
/** Link(s) to spouse(s) */
|
||||
spouse?: string | string[];
|
||||
|
||||
/** Birth date (YYYY-MM-DD) */
|
||||
born?: string;
|
||||
|
||||
/** Death date (YYYY-MM-DD) */
|
||||
died?: string;
|
||||
|
||||
/** Whether this person is the root of the tree */
|
||||
cr_root?: boolean;
|
||||
|
||||
/** Children links */
|
||||
child?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized person data for graph processing
|
||||
*/
|
||||
export interface PersonNode {
|
||||
id: string;
|
||||
name: string;
|
||||
filePath: string;
|
||||
fatherId?: string;
|
||||
motherId?: string;
|
||||
spouseIds: string[];
|
||||
childIds: string[];
|
||||
born?: string;
|
||||
died?: string;
|
||||
isRoot?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Family unit representing a marriage/partnership
|
||||
*/
|
||||
export interface FamilyUnit {
|
||||
id: string;
|
||||
spouseIds: string[];
|
||||
childIds: string[];
|
||||
}
|
||||
124
src/settings.ts
Normal file
124
src/settings.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import CanvasRootsPlugin from '../main';
|
||||
|
||||
export interface CanvasRootsSettings {
|
||||
defaultNodeWidth: number;
|
||||
defaultNodeHeight: number;
|
||||
horizontalSpacing: number;
|
||||
verticalSpacing: number;
|
||||
gedcomImportMode: 'canvas-only' | 'vault-sync';
|
||||
autoGenerateCrId: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: CanvasRootsSettings = {
|
||||
defaultNodeWidth: 200,
|
||||
defaultNodeHeight: 100,
|
||||
horizontalSpacing: 50,
|
||||
verticalSpacing: 100,
|
||||
gedcomImportMode: 'canvas-only',
|
||||
autoGenerateCrId: true
|
||||
};
|
||||
|
||||
export class CanvasRootsSettingTab extends PluginSettingTab {
|
||||
plugin: CanvasRootsPlugin;
|
||||
|
||||
constructor(app: App, plugin: CanvasRootsPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Canvas Roots Settings' });
|
||||
|
||||
// Layout Settings
|
||||
containerEl.createEl('h3', { text: 'Layout Settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Node Width')
|
||||
.setDesc('Width of person nodes in pixels')
|
||||
.addText(text => text
|
||||
.setPlaceholder('200')
|
||||
.setValue(String(this.plugin.settings.defaultNodeWidth))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
this.plugin.settings.defaultNodeWidth = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Node Height')
|
||||
.setDesc('Height of person nodes in pixels')
|
||||
.addText(text => text
|
||||
.setPlaceholder('100')
|
||||
.setValue(String(this.plugin.settings.defaultNodeHeight))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
this.plugin.settings.defaultNodeHeight = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Horizontal Spacing')
|
||||
.setDesc('Space between nodes horizontally in pixels')
|
||||
.addText(text => text
|
||||
.setPlaceholder('50')
|
||||
.setValue(String(this.plugin.settings.horizontalSpacing))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
this.plugin.settings.horizontalSpacing = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Vertical Spacing')
|
||||
.setDesc('Space between generations vertically in pixels')
|
||||
.addText(text => text
|
||||
.setPlaceholder('100')
|
||||
.setValue(String(this.plugin.settings.verticalSpacing))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
this.plugin.settings.verticalSpacing = numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
// Data Settings
|
||||
containerEl.createEl('h3', { text: 'Data Settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-generate CR ID')
|
||||
.setDesc('Automatically generate cr_id for person notes that don\'t have one')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoGenerateCrId)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoGenerateCrId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// GEDCOM Settings
|
||||
containerEl.createEl('h3', { text: 'GEDCOM Import Settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Import Mode')
|
||||
.setDesc('Canvas-only: quick visualization. Vault-sync: create notes for all individuals.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('canvas-only', 'Canvas Visualization Only')
|
||||
.addOption('vault-sync', 'Full Vault Synchronization')
|
||||
.setValue(this.plugin.settings.gedcomImportMode)
|
||||
.onChange(async (value: 'canvas-only' | 'vault-sync') => {
|
||||
this.plugin.settings.gedcomImportMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
271
styles.css
Normal file
271
styles.css
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/* ==========================================================================
|
||||
Canvas Roots Plugin Stylesheet - BUILT VERSION
|
||||
========================================================================== */
|
||||
|
||||
/* Canvas Roots: Family Trees in Obsidian Canvas
|
||||
* An Obsidian plugin for creating and visualizing family trees using D3.js layouts.
|
||||
*
|
||||
* GitHub: https://github.com/banisterious/obsidian-canvas-roots
|
||||
*
|
||||
* BUILD INFORMATION:
|
||||
* - Generated: 2025-11-17 22:56:03 UTC
|
||||
* - Components: 11 files
|
||||
* - Build System: Node.js CSS Build Script v1.0.0
|
||||
*
|
||||
* NOTE: This file is automatically generated from component files in styles/
|
||||
* Do not edit this file directly - edit the component files instead!
|
||||
* Run 'npm run build:css' to regenerate this file.
|
||||
*/
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: VARIABLES.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 31 lines, 0.72 KB */
|
||||
|
||||
/* Canvas Roots CSS Variables */
|
||||
|
||||
:root {
|
||||
/* Spacing */
|
||||
--cr-spacing-xs: 4px;
|
||||
--cr-spacing-sm: 8px;
|
||||
--cr-spacing-md: 16px;
|
||||
--cr-spacing-lg: 24px;
|
||||
--cr-spacing-xl: 32px;
|
||||
|
||||
/* Node dimensions */
|
||||
--cr-node-width: 200px;
|
||||
--cr-node-height: 100px;
|
||||
--cr-node-padding: var(--cr-spacing-md);
|
||||
|
||||
/* Colors - using Obsidian's CSS variables for theme compatibility */
|
||||
--cr-primary: var(--interactive-accent);
|
||||
--cr-bg: var(--background-primary);
|
||||
--cr-text: var(--text-normal);
|
||||
--cr-border: var(--background-modifier-border);
|
||||
|
||||
/* Transitions */
|
||||
--cr-transition-fast: 150ms ease;
|
||||
--cr-transition-normal: 250ms ease;
|
||||
--cr-transition-slow: 350ms ease;
|
||||
|
||||
/* Borders */
|
||||
--cr-border-width: 1px;
|
||||
--cr-border-radius: 6px;
|
||||
}
|
||||
|
||||
|
||||
/* End of variables.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: BASE.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 9 lines, 0.13 KB */
|
||||
|
||||
/* Base Canvas Roots styles */
|
||||
|
||||
.canvas-roots-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* End of base.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: LAYOUT.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 22 lines, 0.24 KB */
|
||||
|
||||
/* Layout utilities for Canvas Roots */
|
||||
|
||||
.cr-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cr-flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cr-gap-sm {
|
||||
gap: var(--cr-spacing-sm);
|
||||
}
|
||||
|
||||
.cr-gap-md {
|
||||
gap: var(--cr-spacing-md);
|
||||
}
|
||||
|
||||
.cr-gap-lg {
|
||||
gap: var(--cr-spacing-lg);
|
||||
}
|
||||
|
||||
|
||||
/* End of layout.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: CANVAS.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 4 lines, 0.07 KB */
|
||||
|
||||
/* Canvas-specific styling */
|
||||
|
||||
/* Placeholder for canvas-specific styles */
|
||||
|
||||
|
||||
/* End of canvas.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: NODES.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 12 lines, 0.3 KB */
|
||||
|
||||
/* Family tree node styling */
|
||||
|
||||
.cr-person-node {
|
||||
width: var(--cr-node-width);
|
||||
height: var(--cr-node-height);
|
||||
padding: var(--cr-node-padding);
|
||||
border: var(--cr-border-width) solid var(--cr-border);
|
||||
border-radius: var(--cr-border-radius);
|
||||
background: var(--cr-bg);
|
||||
color: var(--cr-text);
|
||||
}
|
||||
|
||||
|
||||
/* End of nodes.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: EDGES.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 4 lines, 0.07 KB */
|
||||
|
||||
/* Relationship edge styling */
|
||||
|
||||
/* Placeholder for edge styles */
|
||||
|
||||
|
||||
/* End of edges.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: SETTINGS.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 4 lines, 0.07 KB */
|
||||
|
||||
/* Settings interface styling */
|
||||
|
||||
/* Placeholder for settings styles */
|
||||
|
||||
|
||||
/* End of settings.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: MODALS.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 4 lines, 0.06 KB */
|
||||
|
||||
/* Modal dialog styling */
|
||||
|
||||
/* Placeholder for modal styles */
|
||||
|
||||
|
||||
/* End of modals.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: ANIMATIONS.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 16 lines, 0.18 KB */
|
||||
|
||||
/* Animations and transitions */
|
||||
|
||||
@keyframes cr-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cr-fade-in {
|
||||
animation: cr-fade-in var(--cr-transition-normal);
|
||||
}
|
||||
|
||||
|
||||
/* End of animations.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: RESPONSIVE.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 9 lines, 0.13 KB */
|
||||
|
||||
/* Responsive breakpoints */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--cr-node-width: 150px;
|
||||
--cr-node-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* End of responsive.css */
|
||||
|
||||
/* ==========================================================================
|
||||
COMPONENT: THEME.CSS
|
||||
========================================================================== */
|
||||
|
||||
/* Component Size: 11 lines, 0.19 KB */
|
||||
|
||||
/* Theme compatibility */
|
||||
|
||||
/* Ensure compatibility with light and dark themes */
|
||||
.theme-light {
|
||||
/* Light theme overrides if needed */
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
/* Dark theme overrides if needed */
|
||||
}
|
||||
|
||||
|
||||
/* End of theme.css */
|
||||
|
||||
/* ==========================================================================
|
||||
BUILD STATISTICS
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Build completed: 2025-11-17 22:56:03 UTC
|
||||
* Components processed: 11
|
||||
* Total lines: 126
|
||||
* Total size: 2.17 KB
|
||||
* Build duration: 0.01 seconds
|
||||
*
|
||||
* Component breakdown:
|
||||
* - variables.css: 31 lines, 0.72 KB
|
||||
* - base.css: 9 lines, 0.13 KB
|
||||
* - layout.css: 22 lines, 0.24 KB
|
||||
* - canvas.css: 4 lines, 0.07 KB
|
||||
* - nodes.css: 12 lines, 0.3 KB
|
||||
* - edges.css: 4 lines, 0.07 KB
|
||||
* - settings.css: 4 lines, 0.07 KB
|
||||
* - modals.css: 4 lines, 0.06 KB
|
||||
* - animations.css: 16 lines, 0.18 KB
|
||||
* - responsive.css: 9 lines, 0.13 KB
|
||||
* - theme.css: 11 lines, 0.19 KB
|
||||
*/
|
||||
|
||||
/* End of generated stylesheet */
|
||||
15
styles/animations.css
Normal file
15
styles/animations.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* Animations and transitions */
|
||||
|
||||
@keyframes cr-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cr-fade-in {
|
||||
animation: cr-fade-in var(--cr-transition-normal);
|
||||
}
|
||||
8
styles/base.css
Normal file
8
styles/base.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/* Base Canvas Roots styles */
|
||||
|
||||
.canvas-roots-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
3
styles/canvas.css
Normal file
3
styles/canvas.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Canvas-specific styling */
|
||||
|
||||
/* Placeholder for canvas-specific styles */
|
||||
3
styles/edges.css
Normal file
3
styles/edges.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Relationship edge styling */
|
||||
|
||||
/* Placeholder for edge styles */
|
||||
21
styles/layout.css
Normal file
21
styles/layout.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* Layout utilities for Canvas Roots */
|
||||
|
||||
.cr-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cr-flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cr-gap-sm {
|
||||
gap: var(--cr-spacing-sm);
|
||||
}
|
||||
|
||||
.cr-gap-md {
|
||||
gap: var(--cr-spacing-md);
|
||||
}
|
||||
|
||||
.cr-gap-lg {
|
||||
gap: var(--cr-spacing-lg);
|
||||
}
|
||||
3
styles/modals.css
Normal file
3
styles/modals.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Modal dialog styling */
|
||||
|
||||
/* Placeholder for modal styles */
|
||||
11
styles/nodes.css
Normal file
11
styles/nodes.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* Family tree node styling */
|
||||
|
||||
.cr-person-node {
|
||||
width: var(--cr-node-width);
|
||||
height: var(--cr-node-height);
|
||||
padding: var(--cr-node-padding);
|
||||
border: var(--cr-border-width) solid var(--cr-border);
|
||||
border-radius: var(--cr-border-radius);
|
||||
background: var(--cr-bg);
|
||||
color: var(--cr-text);
|
||||
}
|
||||
8
styles/responsive.css
Normal file
8
styles/responsive.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/* Responsive breakpoints */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--cr-node-width: 150px;
|
||||
--cr-node-height: 80px;
|
||||
}
|
||||
}
|
||||
3
styles/settings.css
Normal file
3
styles/settings.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Settings interface styling */
|
||||
|
||||
/* Placeholder for settings styles */
|
||||
10
styles/theme.css
Normal file
10
styles/theme.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* Theme compatibility */
|
||||
|
||||
/* Ensure compatibility with light and dark themes */
|
||||
.theme-light {
|
||||
/* Light theme overrides if needed */
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
/* Dark theme overrides if needed */
|
||||
}
|
||||
30
styles/variables.css
Normal file
30
styles/variables.css
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* Canvas Roots CSS Variables */
|
||||
|
||||
:root {
|
||||
/* Spacing */
|
||||
--cr-spacing-xs: 4px;
|
||||
--cr-spacing-sm: 8px;
|
||||
--cr-spacing-md: 16px;
|
||||
--cr-spacing-lg: 24px;
|
||||
--cr-spacing-xl: 32px;
|
||||
|
||||
/* Node dimensions */
|
||||
--cr-node-width: 200px;
|
||||
--cr-node-height: 100px;
|
||||
--cr-node-padding: var(--cr-spacing-md);
|
||||
|
||||
/* Colors - using Obsidian's CSS variables for theme compatibility */
|
||||
--cr-primary: var(--interactive-accent);
|
||||
--cr-bg: var(--background-primary);
|
||||
--cr-text: var(--text-normal);
|
||||
--cr-border: var(--background-modifier-border);
|
||||
|
||||
/* Transitions */
|
||||
--cr-transition-fast: 150ms ease;
|
||||
--cr-transition-normal: 250ms ease;
|
||||
--cr-transition-slow: 350ms ease;
|
||||
|
||||
/* Borders */
|
||||
--cr-border-width: 1px;
|
||||
--cr-border-radius: 6px;
|
||||
}
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2022"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.4.0"
|
||||
}
|
||||
Loading…
Reference in a new issue