Merge pull request #43 from gamosoft/features/templates
This commit is contained in:
commit
a132354ddb
|
|
@ -210,6 +210,7 @@ Want to learn more?
|
||||||
- 🎨 **[THEMES.md](documentation/THEMES.md)** - Theme customization and creating custom themes
|
- 🎨 **[THEMES.md](documentation/THEMES.md)** - Theme customization and creating custom themes
|
||||||
- ✨ **[FEATURES.md](documentation/FEATURES.md)** - Complete feature list and keyboard shortcuts
|
- ✨ **[FEATURES.md](documentation/FEATURES.md)** - Complete feature list and keyboard shortcuts
|
||||||
- 🏷️ **[TAGS.md](documentation/TAGS.md)** - Organize notes with tags and combined filtering
|
- 🏷️ **[TAGS.md](documentation/TAGS.md)** - Organize notes with tags and combined filtering
|
||||||
|
- 📋 **[TEMPLATES.md](documentation/TEMPLATES.md)** - Create notes from reusable templates with dynamic placeholders
|
||||||
- 🧮 **[MATHJAX.md](documentation/MATHJAX.md)** - LaTeX/Math notation examples and syntax reference
|
- 🧮 **[MATHJAX.md](documentation/MATHJAX.md)** - LaTeX/Math notation examples and syntax reference
|
||||||
- 📊 **[MERMAID.md](documentation/MERMAID.md)** - Diagram creation with Mermaid (flowcharts, sequence diagrams, and more)
|
- 📊 **[MERMAID.md](documentation/MERMAID.md)** - Diagram creation with Mermaid (flowcharts, sequence diagrams, and more)
|
||||||
- 🔌 **[PLUGINS.md](documentation/PLUGINS.md)** - Plugin system and available plugins
|
- 🔌 **[PLUGINS.md](documentation/PLUGINS.md)** - Plugin system and available plugins
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ from .utils import (
|
||||||
validate_path_security,
|
validate_path_security,
|
||||||
get_all_tags,
|
get_all_tags,
|
||||||
get_notes_by_tag,
|
get_notes_by_tag,
|
||||||
|
get_templates,
|
||||||
|
get_template_content,
|
||||||
|
apply_template_placeholders,
|
||||||
)
|
)
|
||||||
from .plugins import PluginManager
|
from .plugins import PluginManager
|
||||||
from .themes import get_available_themes, get_theme_css
|
from .themes import get_available_themes, get_theme_css
|
||||||
|
|
@ -777,6 +780,101 @@ async def get_notes_by_tag_endpoint(tag_name: str):
|
||||||
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get notes by tag"))
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get notes by tag"))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Template Endpoints ---
|
||||||
|
|
||||||
|
@api_router.get("/templates")
|
||||||
|
@limiter.limit("120/minute")
|
||||||
|
async def list_templates(request: Request):
|
||||||
|
"""
|
||||||
|
List all available templates from _templates folder.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of template metadata
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
templates = get_templates(config['storage']['notes_dir'])
|
||||||
|
return {"templates": templates}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to list templates"))
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/templates/{template_name}")
|
||||||
|
@limiter.limit("120/minute")
|
||||||
|
async def get_template(request: Request, template_name: str):
|
||||||
|
"""
|
||||||
|
Get content of a specific template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_name: Name of the template (without .md extension)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Template name and content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content = get_template_content(config['storage']['notes_dir'], template_name)
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": template_name,
|
||||||
|
"content": content
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get template"))
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/templates/create-note")
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
async def create_note_from_template(request: Request, data: dict):
|
||||||
|
"""
|
||||||
|
Create a new note from a template with placeholder replacement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dictionary containing templateName and notePath
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Success status, path, and created content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template_name = data.get('templateName', '')
|
||||||
|
note_path = data.get('notePath', '')
|
||||||
|
|
||||||
|
if not template_name or not note_path:
|
||||||
|
raise HTTPException(status_code=400, detail="Template name and note path required")
|
||||||
|
|
||||||
|
# Get template content
|
||||||
|
template_content = get_template_content(config['storage']['notes_dir'], template_name)
|
||||||
|
|
||||||
|
if template_content is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found")
|
||||||
|
|
||||||
|
# Apply placeholder replacements
|
||||||
|
final_content = apply_template_placeholders(template_content, note_path)
|
||||||
|
|
||||||
|
# Save the note
|
||||||
|
success = save_note(config['storage']['notes_dir'], note_path, final_content)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to create note from template")
|
||||||
|
|
||||||
|
# Run plugin hooks
|
||||||
|
plugin_manager.run_hook('on_note_create', note_path=note_path, initial_content=final_content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"path": note_path,
|
||||||
|
"message": "Note created from template successfully",
|
||||||
|
"content": final_content
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to create note from template"))
|
||||||
|
|
||||||
|
|
||||||
# --- Notes Endpoints ---
|
# --- Notes Endpoints ---
|
||||||
|
|
||||||
@api_router.get("/notes")
|
@api_router.get("/notes")
|
||||||
|
|
|
||||||
115
backend/utils.py
115
backend/utils.py
|
|
@ -647,3 +647,118 @@ def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]:
|
||||||
|
|
||||||
return matching_notes
|
return matching_notes
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Template Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_templates(notes_dir: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Get all templates from the _templates folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
notes_dir: Base notes directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of template metadata (name, path, modified)
|
||||||
|
"""
|
||||||
|
templates = []
|
||||||
|
templates_path = Path(notes_dir) / "_templates"
|
||||||
|
|
||||||
|
if not templates_path.exists():
|
||||||
|
return templates
|
||||||
|
|
||||||
|
# Security check: ensure _templates folder is within notes directory
|
||||||
|
if not validate_path_security(notes_dir, templates_path):
|
||||||
|
print(f"Security: Templates directory is outside notes directory: {templates_path}")
|
||||||
|
return templates
|
||||||
|
|
||||||
|
try:
|
||||||
|
for template_file in templates_path.glob("*.md"):
|
||||||
|
try:
|
||||||
|
# Security check: ensure each template is within notes directory
|
||||||
|
if not validate_path_security(notes_dir, template_file):
|
||||||
|
print(f"Security: Skipping template outside notes directory: {template_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
stat = template_file.stat()
|
||||||
|
templates.append({
|
||||||
|
"name": template_file.stem,
|
||||||
|
"path": str(template_file.relative_to(notes_dir).as_posix()),
|
||||||
|
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading template {template_file}: {e}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error accessing templates directory: {e}")
|
||||||
|
|
||||||
|
return sorted(templates, key=lambda x: x['name'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_content(notes_dir: str, template_name: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the content of a specific template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
notes_dir: Base notes directory
|
||||||
|
template_name: Name of the template (without .md extension)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Template content or None if not found
|
||||||
|
"""
|
||||||
|
template_path = Path(notes_dir) / "_templates" / f"{template_name}.md"
|
||||||
|
|
||||||
|
if not template_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Security check: ensure template is within notes directory
|
||||||
|
if not validate_path_security(notes_dir, template_path):
|
||||||
|
print(f"Security: Template path is outside notes directory: {template_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(template_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading template {template_name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def apply_template_placeholders(content: str, note_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Replace template placeholders with actual values.
|
||||||
|
|
||||||
|
Supported placeholders:
|
||||||
|
{{date}} - Current date (YYYY-MM-DD)
|
||||||
|
{{time}} - Current time (HH:MM:SS)
|
||||||
|
{{datetime}} - Current datetime (YYYY-MM-DD HH:MM:SS)
|
||||||
|
{{timestamp}} - Unix timestamp
|
||||||
|
{{title}} - Note name without extension
|
||||||
|
{{folder}} - Parent folder name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Template content with placeholders
|
||||||
|
note_path: Path of the note being created
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Content with placeholders replaced
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
note = Path(note_path)
|
||||||
|
|
||||||
|
replacements = {
|
||||||
|
'{{date}}': now.strftime('%Y-%m-%d'),
|
||||||
|
'{{time}}': now.strftime('%H:%M:%S'),
|
||||||
|
'{{datetime}}': now.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'{{timestamp}}': str(int(now.timestamp())),
|
||||||
|
'{{title}}': note.stem,
|
||||||
|
'{{folder}}': note.parent.name if str(note.parent) != '.' else 'Root',
|
||||||
|
}
|
||||||
|
|
||||||
|
result = content
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
result = result.replace(placeholder, value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
<!-- Primary Meta Tags -->
|
<!-- Primary Meta Tags -->
|
||||||
<title>NoteDiscovery - Your Self-Hosted Knowledge Base</title>
|
<title>NoteDiscovery - Your Self-Hosted Knowledge Base</title>
|
||||||
<meta name="title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
<meta name="title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
||||||
<meta name="description" content="A lightweight, privacy-focused Markdown note-taking application with LaTeX math, Mermaid diagrams, and code highlighting. Self-hosted, free, and open source.">
|
<meta name="description" content="A lightweight, privacy-focused Markdown note-taking application with LaTeX math, Mermaid diagrams, tags, templates, and code highlighting. Self-hosted, free, and open source.">
|
||||||
<meta name="keywords" content="note taking, markdown editor, self-hosted, knowledge base, open source, privacy, notes app, docker, second brain, obsidian alternative, notion, evernote, onenote, latex, mermaid, diagrams, math equations">
|
<meta name="keywords" content="note taking, markdown editor, self-hosted, knowledge base, open source, privacy, notes app, docker, second brain, obsidian alternative, notion, evernote, onenote, latex, mermaid, diagrams, math equations, templates, tags, yaml frontmatter">
|
||||||
<meta name="author" content="Gamosoft">
|
<meta name="author" content="Gamosoft">
|
||||||
<meta name="robots" content="index, follow">
|
<meta name="robots" content="index, follow">
|
||||||
<link rel="canonical" href="https://www.notediscovery.com">
|
<link rel="canonical" href="https://www.notediscovery.com">
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://www.notediscovery.com">
|
<meta property="og:url" content="https://www.notediscovery.com">
|
||||||
<meta property="og:title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
<meta property="og:title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
||||||
<meta property="og:description" content="A lightweight, privacy-focused Markdown note-taking application with LaTeX math, Mermaid diagrams, and code highlighting. Self-hosted, free, and open source.">
|
<meta property="og:description" content="A lightweight, privacy-focused Markdown note-taking application with LaTeX math, Mermaid diagrams, tags, templates, and code highlighting. Self-hosted, free, and open source.">
|
||||||
<meta property="og:image" content="https://www.notediscovery.com/og-image.png">
|
<meta property="og:image" content="https://www.notediscovery.com/og-image.png">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630">
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<meta name="twitter:site" content="@gamosoft">
|
<meta name="twitter:site" content="@gamosoft">
|
||||||
<meta name="twitter:creator" content="@gamosoft">
|
<meta name="twitter:creator" content="@gamosoft">
|
||||||
<meta name="twitter:title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
<meta name="twitter:title" content="NoteDiscovery - Your Self-Hosted Knowledge Base">
|
||||||
<meta name="twitter:description" content="A lightweight, privacy-focused note-taking application with LaTeX math, Mermaid diagrams, and code highlighting. Self-hosted, free, and open source.">
|
<meta name="twitter:description" content="A lightweight, privacy-focused note-taking application with LaTeX math, Mermaid diagrams, tags, templates, and code highlighting. Self-hosted, free, and open source.">
|
||||||
<meta name="twitter:image" content="https://www.notediscovery.com/og-image.png">
|
<meta name="twitter:image" content="https://www.notediscovery.com/og-image.png">
|
||||||
<meta name="twitter:image:alt" content="NoteDiscovery - Self-hosted note-taking application interface">
|
<meta name="twitter:image:alt" content="NoteDiscovery - Self-hosted note-taking application interface">
|
||||||
|
|
||||||
|
|
@ -509,7 +509,7 @@
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h2>Take Control of Your Notes</h2>
|
<h2>Take Control of Your Notes</h2>
|
||||||
<p>A lightweight, privacy-focused note-taking application with powerful features: LaTeX math equations, Mermaid diagrams, code highlighting, and more. Write, organize, and discover your notes with a beautiful, modern interface—all running on your own server.</p>
|
<p>A lightweight, privacy-focused note-taking application with powerful features: LaTeX math equations, Mermaid diagrams, smart tags, custom templates, code highlighting, and more. Write, organize, and discover your notes with a beautiful, modern interface—all running on your own server.</p>
|
||||||
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button">Get Started on GitHub →</a>
|
<a href="https://github.com/gamosoft/NoteDiscovery" class="cta-button">Get Started on GitHub →</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -568,6 +568,18 @@
|
||||||
<p>Syntax highlighting for popular coding languages. Developer-friendly notes.</p>
|
<p>Syntax highlighting for popular coding languages. Developer-friendly notes.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-icon">🏷️</div>
|
||||||
|
<h3>Smart Tags</h3>
|
||||||
|
<p>Organize with YAML frontmatter tags. Filter and discover notes instantly.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-icon">📋</div>
|
||||||
|
<h3>Note Templates</h3>
|
||||||
|
<p>Create from custom templates with dynamic placeholders. Meeting notes, journals, and more.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">📂</div>
|
<div class="feature-icon">📂</div>
|
||||||
<h3>Simple Storage</h3>
|
<h3>Simple Storage</h3>
|
||||||
|
|
@ -642,7 +654,15 @@
|
||||||
<span class="emoji">🔗</span>
|
<span class="emoji">🔗</span>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<strong>Internal Links</strong>
|
<strong>Internal Links</strong>
|
||||||
Connect notes with markdown links
|
Connect notes with backlinks
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-item">
|
||||||
|
<span class="emoji">🖼️</span>
|
||||||
|
<div class="text">
|
||||||
|
<strong>Image Support</strong>
|
||||||
|
Drag & drop images directly
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,120 @@ Self-documenting endpoint listing all available API routes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🏷️ Tags
|
||||||
|
|
||||||
|
### List All Tags
|
||||||
|
`GET /api/tags`
|
||||||
|
|
||||||
|
Returns all tags found in notes with their usage counts.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tags": {
|
||||||
|
"python": 5,
|
||||||
|
"tutorial": 3,
|
||||||
|
"backend": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Notes by Tag
|
||||||
|
`GET /api/tags/{tag_name}`
|
||||||
|
|
||||||
|
Returns all notes that have a specific tag.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tag": "python",
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"path": "tutorials/python-basics.md",
|
||||||
|
"name": "python-basics",
|
||||||
|
"folder": "tutorials",
|
||||||
|
"tags": ["python", "tutorial"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Templates
|
||||||
|
|
||||||
|
### List Templates
|
||||||
|
`GET /api/templates`
|
||||||
|
|
||||||
|
Returns all available note templates from the `_templates` folder.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"name": "meeting-notes",
|
||||||
|
"path": "_templates/meeting-notes.md",
|
||||||
|
"modified": "2025-11-26T10:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "daily-journal",
|
||||||
|
"path": "_templates/daily-journal.md",
|
||||||
|
"modified": "2025-11-26T10:25:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Template Content
|
||||||
|
`GET /api/templates/{template_name}`
|
||||||
|
|
||||||
|
Returns the content of a specific template.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `template_name` - Template name (without .md extension)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "meeting-notes",
|
||||||
|
"content": "# Meeting Notes\n\nDate: {{date}}\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Note from Template
|
||||||
|
`POST /api/templates/create-note`
|
||||||
|
|
||||||
|
Creates a new note from a template with placeholder replacement.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"templateName": "meeting-notes",
|
||||||
|
"notePath": "meetings/weekly-sync.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Placeholders:**
|
||||||
|
- `{{date}}` - Current date (YYYY-MM-DD)
|
||||||
|
- `{{time}}` - Current time (HH:MM:SS)
|
||||||
|
- `{{datetime}}` - Current datetime
|
||||||
|
- `{{timestamp}}` - Unix timestamp
|
||||||
|
- `{{title}}` - Note name without extension
|
||||||
|
- `{{folder}}` - Parent folder name
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"path": "meetings/weekly-sync.md",
|
||||||
|
"message": "Note created from template successfully",
|
||||||
|
"content": "# Meeting Notes\n\nDate: 2025-11-26\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📝 Response Format
|
## 📝 Response Format
|
||||||
|
|
||||||
All endpoints return JSON responses:
|
All endpoints return JSON responses:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
- **Three view modes**: Edit, Split, Preview
|
- **Three view modes**: Edit, Split, Preview
|
||||||
- **Auto-save** - Never lose your work
|
- **Auto-save** - Never lose your work
|
||||||
- **Undo/Redo** - Ctrl+Z / Ctrl+Y support
|
- **Undo/Redo** - Ctrl+Z / Ctrl+Y support
|
||||||
|
- **Note templates** - Create notes from templates with dynamic placeholders
|
||||||
- **Syntax highlighting** for code blocks (50+ languages)
|
- **Syntax highlighting** for code blocks (50+ languages)
|
||||||
- **Copy code blocks** - One-click copy button on hover
|
- **Copy code blocks** - One-click copy button on hover
|
||||||
- **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax (see [MATHJAX.md](MATHJAX.md))
|
- **LaTeX/Math rendering** - Beautiful mathematical equations with MathJax (see [MATHJAX.md](MATHJAX.md))
|
||||||
|
|
@ -162,6 +163,51 @@ graph TD
|
||||||
|
|
||||||
📄 **See the [MERMAID](MERMAID.md) note for diagram examples and syntax reference.**
|
📄 **See the [MERMAID](MERMAID.md) note for diagram examples and syntax reference.**
|
||||||
|
|
||||||
|
## 📄 Note Templates
|
||||||
|
|
||||||
|
Create notes from reusable templates with dynamic placeholder replacement.
|
||||||
|
|
||||||
|
### Creating Templates
|
||||||
|
1. Create markdown files in `data/_templates/` folder
|
||||||
|
2. Use placeholders for dynamic content
|
||||||
|
3. Templates appear in the "New from Template" menu
|
||||||
|
|
||||||
|
### Available Placeholders
|
||||||
|
- `{{date}}` - Current date (YYYY-MM-DD)
|
||||||
|
- `{{time}}` - Current time (HH:MM:SS)
|
||||||
|
- `{{datetime}}` - Current date and time
|
||||||
|
- `{{timestamp}}` - Unix timestamp
|
||||||
|
- `{{title}}` - Note name (without extension)
|
||||||
|
- `{{folder}}` - Parent folder name
|
||||||
|
|
||||||
|
### Example Template
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [meeting]
|
||||||
|
date: {{date}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{title}}
|
||||||
|
|
||||||
|
**Created:** {{datetime}}
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Templates
|
||||||
|
1. Click the "New" dropdown button
|
||||||
|
2. Select "New from Template"
|
||||||
|
3. Choose a template and enter a note name
|
||||||
|
4. The new note will be created with placeholders replaced
|
||||||
|
|
||||||
|
### Built-in Templates
|
||||||
|
- **meeting-notes** - Template for meeting notes
|
||||||
|
- **daily-journal** - Daily journal with morning goals and evening reflection
|
||||||
|
- **project-plan** - Project planning template with objectives and timeline
|
||||||
|
|
||||||
|
📚 **See [TEMPLATES.md](TEMPLATES.md)** for detailed documentation and example templates you can copy to your instance.
|
||||||
|
|
||||||
## ⚡ Keyboard Shortcuts
|
## ⚡ Keyboard Shortcuts
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
# 📋 Note Templates
|
||||||
|
|
||||||
|
Create notes from reusable templates with dynamic placeholder replacement.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Templates allow you to quickly create notes with predefined structures and content. Perfect for recurring note types like meeting notes, daily journals, project plans, and more.
|
||||||
|
|
||||||
|
## How to Use Templates
|
||||||
|
|
||||||
|
### 1. Create Template Files
|
||||||
|
|
||||||
|
Templates are stored in the `data/_templates/` folder as markdown files. You will need to create such folder if it doesn't exist:
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
└── _templates/
|
||||||
|
├── meeting-notes.md
|
||||||
|
├── daily-journal.md
|
||||||
|
└── project-plan.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Access Templates
|
||||||
|
|
||||||
|
Click the **"New"** button (or **+** on any folder) and select **"New from Template"**:
|
||||||
|
|
||||||
|
1. Choose a template from the dropdown
|
||||||
|
2. Enter a name for your new note
|
||||||
|
3. Click "Create Note"
|
||||||
|
|
||||||
|
The template will be copied with all placeholders replaced automatically!
|
||||||
|
|
||||||
|
### 3. Use Placeholders
|
||||||
|
|
||||||
|
Templates support dynamic placeholders that are replaced when you create a note:
|
||||||
|
|
||||||
|
| Placeholder | Description | Example |
|
||||||
|
|------------|-------------|---------|
|
||||||
|
| `{{date}}` | Current date | `2025-11-26` |
|
||||||
|
| `{{time}}` | Current time | `14:30:45` |
|
||||||
|
| `{{datetime}}` | Current date and time | `2025-11-26 14:30:45` |
|
||||||
|
| `{{timestamp}}` | Unix timestamp | `1732632645` |
|
||||||
|
| `{{title}}` | Note name (without .md) | `Weekly Meeting` |
|
||||||
|
| `{{folder}}` | Parent folder name | `Projects` |
|
||||||
|
|
||||||
|
### Example Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [meeting]
|
||||||
|
date: {{date}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Meeting Notes - {{title}}
|
||||||
|
|
||||||
|
**Date:** {{datetime}}
|
||||||
|
**Participants:**
|
||||||
|
-
|
||||||
|
|
||||||
|
## Agenda
|
||||||
|
-
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
When you create a note called "Team Sync" from this template, it becomes:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [meeting]
|
||||||
|
date: 2025-11-26
|
||||||
|
---
|
||||||
|
|
||||||
|
# Meeting Notes - Team Sync
|
||||||
|
|
||||||
|
**Date:** 2025-11-26 14:30:45
|
||||||
|
**Participants:**
|
||||||
|
-
|
||||||
|
|
||||||
|
## Agenda
|
||||||
|
-
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Templates
|
||||||
|
|
||||||
|
We provide three example templates in `documentation/templates/` that you can copy to your `data/_templates/` folder:
|
||||||
|
|
||||||
|
1. **meeting-notes.md** - Structured meeting notes with agenda, discussion, and action items
|
||||||
|
2. **daily-journal.md** - Daily journal with morning goals and evening reflection
|
||||||
|
3. **project-plan.md** - Project planning template with objectives, timeline, and status tracking
|
||||||
|
|
||||||
|
### Using Example Templates
|
||||||
|
|
||||||
|
**Option 1: Copy Manually**
|
||||||
|
```bash
|
||||||
|
cp documentation/templates/*.md data/_templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Create Your Own**
|
||||||
|
1. Create a `.md` file in `data/_templates/`
|
||||||
|
2. Add content and placeholders
|
||||||
|
3. Save and it's ready to use!
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- ✅ Templates can include YAML frontmatter for tags and metadata
|
||||||
|
- ✅ Use descriptive template names (they appear in the dropdown)
|
||||||
|
- ✅ Templates work in any folder - the context is preserved
|
||||||
|
- ✅ You can edit templates anytime - changes apply to new notes only
|
||||||
|
- ✅ Combine templates with tags for powerful organization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**See also:**
|
||||||
|
- [Tags Documentation](TAGS.md) - Learn about organizing with tags
|
||||||
|
- [Features Overview](FEATURES.md) - All application features
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
tags: [journal, daily]
|
||||||
|
date: {{date}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Daily Journal - {{date}}
|
||||||
|
|
||||||
|
## 🌅 Morning
|
||||||
|
**Time:** {{time}}
|
||||||
|
|
||||||
|
### Goals for Today
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
|
||||||
|
## 🌙 Evening Reflection
|
||||||
|
|
||||||
|
|
||||||
|
## 💡 Ideas
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
tags: [meeting]
|
||||||
|
date: {{date}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Meeting Notes - {{title}}
|
||||||
|
|
||||||
|
**Date:** {{datetime}}
|
||||||
|
**Participants:**
|
||||||
|
-
|
||||||
|
|
||||||
|
## Agenda
|
||||||
|
-
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
tags: [project]
|
||||||
|
created: {{datetime}}
|
||||||
|
folder: {{folder}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project: {{title}}
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
|
||||||
|
## 🎯 Objectives
|
||||||
|
-
|
||||||
|
|
||||||
|
## 📅 Timeline
|
||||||
|
- **Start Date:** {{date}}
|
||||||
|
- **Target Completion:**
|
||||||
|
|
||||||
|
## 📦 Deliverables
|
||||||
|
-
|
||||||
|
|
||||||
|
## 👥 Team
|
||||||
|
|
||||||
|
|
||||||
|
## 📊 Status
|
||||||
|
- [ ] Planning
|
||||||
|
- [ ] In Progress
|
||||||
|
- [ ] Review
|
||||||
|
- [ ] Completed
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
141
frontend/app.js
141
frontend/app.js
|
|
@ -99,6 +99,14 @@ function noteApp() {
|
||||||
|
|
||||||
// Dropdown state
|
// Dropdown state
|
||||||
showNewDropdown: false,
|
showNewDropdown: false,
|
||||||
|
dropdownTargetFolder: '', // Folder context for "New" dropdown (empty = root)
|
||||||
|
dropdownPosition: { top: 0, left: 0 }, // Position for contextual dropdown
|
||||||
|
|
||||||
|
// Template state
|
||||||
|
showTemplateModal: false,
|
||||||
|
availableTemplates: [],
|
||||||
|
selectedTemplate: '',
|
||||||
|
newTemplateNoteName: '',
|
||||||
|
|
||||||
// Homepage state
|
// Homepage state
|
||||||
selectedHomepageFolder: '',
|
selectedHomepageFolder: '',
|
||||||
|
|
@ -258,6 +266,7 @@ function noteApp() {
|
||||||
await this.loadThemes();
|
await this.loadThemes();
|
||||||
await this.initTheme();
|
await this.initTheme();
|
||||||
await this.loadNotes();
|
await this.loadNotes();
|
||||||
|
await this.loadTemplates();
|
||||||
await this.checkStatsPlugin();
|
await this.checkStatsPlugin();
|
||||||
this.loadSidebarWidth();
|
this.loadSidebarWidth();
|
||||||
this.loadEditorWidth();
|
this.loadEditorWidth();
|
||||||
|
|
@ -591,6 +600,72 @@ function noteApp() {
|
||||||
this.applyFilters();
|
this.applyFilters();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Template Methods
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Load available templates from _templates folder
|
||||||
|
async loadTemplates() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/templates');
|
||||||
|
const data = await response.json();
|
||||||
|
this.availableTemplates = data.templates || [];
|
||||||
|
} catch (error) {
|
||||||
|
ErrorHandler.handle('load templates', error, false); // Don't show alert, templates are optional
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a new note from a template
|
||||||
|
async createNoteFromTemplate() {
|
||||||
|
if (!this.selectedTemplate || !this.newTemplateNoteName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine the note path based on dropdown context
|
||||||
|
let notePath = this.newTemplateNoteName.trim();
|
||||||
|
if (!notePath.endsWith('.md')) {
|
||||||
|
notePath += '.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a target folder context, create in that folder
|
||||||
|
if (this.dropdownTargetFolder || this.selectedHomepageFolder) {
|
||||||
|
const targetFolder = this.dropdownTargetFolder || this.selectedHomepageFolder;
|
||||||
|
notePath = `${targetFolder}/${notePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create note from template
|
||||||
|
const response = await fetch('/api/templates/create-note', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
templateName: this.selectedTemplate,
|
||||||
|
notePath: notePath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(error.detail || 'Failed to create note from template');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Close modal and reset state
|
||||||
|
this.showTemplateModal = false;
|
||||||
|
this.selectedTemplate = '';
|
||||||
|
this.newTemplateNoteName = '';
|
||||||
|
|
||||||
|
// Reload notes and open the new note
|
||||||
|
await this.loadNotes();
|
||||||
|
await this.loadNote(data.path);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
ErrorHandler.handle('create note from template', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Clear all tag filters
|
// Clear all tag filters
|
||||||
clearTagFilters() {
|
clearTagFilters() {
|
||||||
this.selectedTags = [];
|
this.selectedTags = [];
|
||||||
|
|
@ -908,17 +983,11 @@ function noteApp() {
|
||||||
</div>
|
</div>
|
||||||
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" @click.stop>
|
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" @click.stop>
|
||||||
<button
|
<button
|
||||||
@click="createNote('${folder.path.replace(/'/g, "\\'")}')"
|
@click="dropdownTargetFolder = '${folder.path.replace(/'/g, "\\'")}'; toggleNewDropdown($event)"
|
||||||
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
||||||
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
||||||
title="New note here"
|
title="Add item here"
|
||||||
>📄</button>
|
>+</button>
|
||||||
<button
|
|
||||||
@click="createFolder('${folder.path.replace(/'/g, "\\'")}')"
|
|
||||||
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
|
||||||
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
|
|
||||||
title="New subfolder"
|
|
||||||
>📁</button>
|
|
||||||
<button
|
<button
|
||||||
@click="renameFolder('${folder.path.replace(/'/g, "\\'")}', '${folder.name.replace(/'/g, "\\'")}')"
|
@click="renameFolder('${folder.path.replace(/'/g, "\\'")}', '${folder.name.replace(/'/g, "\\'")}')"
|
||||||
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
|
||||||
|
|
@ -1897,23 +1966,45 @@ function noteApp() {
|
||||||
// DROPDOWN MENU SYSTEM
|
// DROPDOWN MENU SYSTEM
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
toggleNewDropdown() {
|
toggleNewDropdown(event) {
|
||||||
this.showNewDropdown = !this.showNewDropdown;
|
this.showNewDropdown = true; // Always open (or keep open)
|
||||||
|
|
||||||
|
if (event && event.target) {
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
// Position dropdown next to the clicked element
|
||||||
|
let top = rect.bottom + 4; // 4px spacing
|
||||||
|
let left = rect.left;
|
||||||
|
|
||||||
|
// Keep dropdown on screen
|
||||||
|
const dropdownWidth = 200;
|
||||||
|
const dropdownHeight = 150;
|
||||||
|
if (left + dropdownWidth > window.innerWidth) {
|
||||||
|
left = rect.right - dropdownWidth;
|
||||||
|
}
|
||||||
|
if (top + dropdownHeight > window.innerHeight) {
|
||||||
|
top = rect.top - dropdownHeight - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdownPosition = { top, left };
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
this.showNewDropdown = false;
|
this.showNewDropdown = false;
|
||||||
|
this.dropdownTargetFolder = ''; // Reset folder context
|
||||||
},
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// UNIFIED CREATION FUNCTIONS (reusable from anywhere)
|
// UNIFIED CREATION FUNCTIONS (reusable from anywhere)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async createNote(folderPath = '') {
|
async createNote(folderPath = null) {
|
||||||
|
// Use provided folder path, or dropdown target folder context
|
||||||
|
const targetFolder = folderPath !== null ? folderPath : this.dropdownTargetFolder;
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
|
|
||||||
const promptText = folderPath
|
const promptText = targetFolder
|
||||||
? `Create note in "${folderPath}".\nEnter note name:`
|
? `Create note in "${targetFolder}".\nEnter note name:`
|
||||||
: 'Enter note name (you can use folder/name):';
|
: 'Enter note name (you can use folder/name):';
|
||||||
|
|
||||||
const noteName = prompt(promptText);
|
const noteName = prompt(promptText);
|
||||||
|
|
@ -1926,8 +2017,8 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let notePath;
|
let notePath;
|
||||||
if (folderPath) {
|
if (targetFolder) {
|
||||||
notePath = `${folderPath}/${sanitizedName}.md`;
|
notePath = `${targetFolder}/${sanitizedName}.md`;
|
||||||
} else {
|
} else {
|
||||||
notePath = sanitizedName.endsWith('.md') ? sanitizedName : `${sanitizedName}.md`;
|
notePath = sanitizedName.endsWith('.md') ? sanitizedName : `${sanitizedName}.md`;
|
||||||
}
|
}
|
||||||
|
|
@ -1947,8 +2038,8 @@ function noteApp() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (folderPath) {
|
if (targetFolder) {
|
||||||
this.expandedFolders.add(folderPath);
|
this.expandedFolders.add(targetFolder);
|
||||||
}
|
}
|
||||||
await this.loadNotes();
|
await this.loadNotes();
|
||||||
await this.loadNote(notePath);
|
await this.loadNote(notePath);
|
||||||
|
|
@ -1960,11 +2051,13 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async createFolder(parentPath = '') {
|
async createFolder(parentPath = null) {
|
||||||
|
// Use provided parent path, or dropdown target folder context
|
||||||
|
const targetFolder = parentPath !== null ? parentPath : this.dropdownTargetFolder;
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
|
|
||||||
const promptText = parentPath
|
const promptText = targetFolder
|
||||||
? `Create subfolder in "${parentPath}".\nEnter folder name:`
|
? `Create subfolder in "${targetFolder}".\nEnter folder name:`
|
||||||
: 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):';
|
: 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):';
|
||||||
|
|
||||||
const folderName = prompt(promptText);
|
const folderName = prompt(promptText);
|
||||||
|
|
@ -1976,7 +2069,7 @@ function noteApp() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderPath = parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName;
|
const folderPath = targetFolder ? `${targetFolder}/${sanitizedName}` : sanitizedName;
|
||||||
|
|
||||||
// Check if folder already exists
|
// Check if folder already exists
|
||||||
const existingFolder = this.allFolders.find(folder => folder === folderPath);
|
const existingFolder = this.allFolders.find(folder => folder === folderPath);
|
||||||
|
|
@ -1993,8 +2086,8 @@ function noteApp() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
if (parentPath) {
|
if (targetFolder) {
|
||||||
this.expandedFolders.add(parentPath);
|
this.expandedFolders.add(targetFolder);
|
||||||
}
|
}
|
||||||
this.expandedFolders.add(folderPath);
|
this.expandedFolders.add(folderPath);
|
||||||
await this.loadNotes();
|
await this.loadNotes();
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Responsive Styles */
|
/* Mobile Responsive Styles */
|
||||||
|
/* Z-index hierarchy (for reference):
|
||||||
|
- Mobile sidebar: 1000
|
||||||
|
- Mobile overlay: 999
|
||||||
|
- Template modal: 1100 (must be above sidebar)
|
||||||
|
- Dropdown menu: 50 (below everything)
|
||||||
|
*/
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Hide sidebar by default on mobile */
|
/* Hide sidebar by default on mobile */
|
||||||
.mobile-sidebar {
|
.mobile-sidebar {
|
||||||
|
|
@ -897,9 +903,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Item Dropdown -->
|
<!-- New Item Dropdown -->
|
||||||
<div class="p-3 border-b relative" style="border-color: var(--border-primary);">
|
<div class="p-3 border-b" style="border-color: var(--border-primary);">
|
||||||
<button
|
<button
|
||||||
@click="toggleNewDropdown()"
|
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
|
||||||
class="w-full px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 flex items-center justify-center gap-2"
|
class="w-full px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 flex items-center justify-center gap-2"
|
||||||
style="background-color: var(--accent-primary);"
|
style="background-color: var(--accent-primary);"
|
||||||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||||
|
|
@ -910,43 +916,6 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown Menu -->
|
|
||||||
<div
|
|
||||||
x-show="showNewDropdown"
|
|
||||||
@click.outside="closeDropdown()"
|
|
||||||
x-transition:enter="transition ease-out duration-100"
|
|
||||||
x-transition:enter-start="transform opacity-0 scale-95"
|
|
||||||
x-transition:enter-end="transform opacity-100 scale-100"
|
|
||||||
x-transition:leave="transition ease-in duration-75"
|
|
||||||
x-transition:leave-start="transform opacity-100 scale-100"
|
|
||||||
x-transition:leave-end="transform opacity-0 scale-95"
|
|
||||||
class="absolute left-3 right-3 mt-2 rounded-lg shadow-lg z-50"
|
|
||||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);"
|
|
||||||
>
|
|
||||||
<div class="py-1">
|
|
||||||
<button
|
|
||||||
@click="createNote()"
|
|
||||||
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
|
||||||
style="color: var(--text-primary);"
|
|
||||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
|
||||||
onmouseout="this.style.backgroundColor='transparent'"
|
|
||||||
>
|
|
||||||
<span class="text-lg">📝</span>
|
|
||||||
<span>New Note</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="createFolder()"
|
|
||||||
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
|
||||||
style="color: var(--text-primary);"
|
|
||||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
|
||||||
onmouseout="this.style.backgroundColor='transparent'"
|
|
||||||
>
|
|
||||||
<span class="text-lg">📁</span>
|
|
||||||
<span>New Folder</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes List -->
|
<!-- Notes List -->
|
||||||
|
|
@ -1141,6 +1110,22 @@
|
||||||
<template x-if="!currentNote && !currentImage">
|
<template x-if="!currentNote && !currentImage">
|
||||||
<!-- Notes Homepage -->
|
<!-- Notes Homepage -->
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);">
|
<div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);">
|
||||||
|
<!-- Mobile Menu Button (Homepage) -->
|
||||||
|
<div class="mobile-menu-button px-4 py-3" style="background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-primary); display: none;">
|
||||||
|
<button
|
||||||
|
@click="mobileSidebarOpen = !mobileSidebarOpen"
|
||||||
|
class="p-2 rounded-lg"
|
||||||
|
style="color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
|
title="Toggle sidebar"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Welcome Hero - Show when app is empty -->
|
<!-- Welcome Hero - Show when app is empty -->
|
||||||
<template x-if="isAppEmpty">
|
<template x-if="isAppEmpty">
|
||||||
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
|
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
|
||||||
|
|
@ -1618,6 +1603,140 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contextual New Item Dropdown -->
|
||||||
|
<div
|
||||||
|
x-show="showNewDropdown"
|
||||||
|
@click.outside="closeDropdown()"
|
||||||
|
x-transition:enter="transition ease-out duration-100"
|
||||||
|
x-transition:enter-start="transform opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="transform opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="transform opacity-0 scale-95"
|
||||||
|
class="rounded-lg shadow-lg"
|
||||||
|
:style="`position: fixed; top: ${dropdownPosition.top}px; left: ${dropdownPosition.left}px; z-index: 1000; background-color: var(--bg-secondary); border: 1px solid var(--border-primary); min-width: 200px;`"
|
||||||
|
x-cloak
|
||||||
|
>
|
||||||
|
<div class="py-1">
|
||||||
|
<button
|
||||||
|
@click="createNote()"
|
||||||
|
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
||||||
|
style="color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
|
>
|
||||||
|
<span class="text-lg">📝</span>
|
||||||
|
<span>New Note</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="createFolder()"
|
||||||
|
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
||||||
|
style="color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
|
>
|
||||||
|
<span class="text-lg">📁</span>
|
||||||
|
<span>New Folder</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="showTemplateModal = true; showNewDropdown = false; mobileSidebarOpen = false"
|
||||||
|
class="w-full px-4 py-2.5 text-sm text-left flex items-center gap-3 transition-colors"
|
||||||
|
style="color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='transparent'"
|
||||||
|
>
|
||||||
|
<span class="text-lg">📄</span>
|
||||||
|
<span>New from Template</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template Selection Modal -->
|
||||||
|
<div x-show="showTemplateModal"
|
||||||
|
@click.self="showTemplateModal = false; selectedTemplate = ''; newTemplateNoteName = ''"
|
||||||
|
@keydown.escape.window="showTemplateModal && (showTemplateModal = false, selectedTemplate = '', newTemplateNoteName = '')"
|
||||||
|
class="fixed inset-0 flex items-center justify-center"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.5); z-index: 1100;"
|
||||||
|
x-cloak>
|
||||||
|
<div class="rounded-lg shadow-xl p-6 max-w-md w-full mx-4"
|
||||||
|
style="background-color: var(--bg-secondary); color: var(--text-primary);"
|
||||||
|
@click.stop>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary);">Create from Template</h2>
|
||||||
|
|
||||||
|
<!-- Template Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">
|
||||||
|
Select Template
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
x-model="selectedTemplate"
|
||||||
|
class="w-full px-3 py-2 rounded border"
|
||||||
|
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
|
||||||
|
>
|
||||||
|
<option value="">-- Choose a template --</option>
|
||||||
|
<template x-for="template in availableTemplates" :key="template.name">
|
||||||
|
<option :value="template.name" x-text="template.name"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note Name Input -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">
|
||||||
|
Note Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
x-model="newTemplateNoteName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter note name"
|
||||||
|
class="w-full px-3 py-2 rounded border"
|
||||||
|
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
|
||||||
|
@keydown.enter="createNoteFromTemplate()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info about placeholders -->
|
||||||
|
<div class="mb-4 text-xs p-3 rounded" style="background-color: var(--bg-tertiary); color: var(--text-secondary);">
|
||||||
|
<strong>Available placeholders:</strong><br>
|
||||||
|
<code style="font-family: monospace;">{{date}}</code>,
|
||||||
|
<code style="font-family: monospace;">{{time}}</code>,
|
||||||
|
<code style="font-family: monospace;">{{datetime}}</code>,
|
||||||
|
<code style="font-family: monospace;">{{title}}</code>,
|
||||||
|
<code style="font-family: monospace;">{{folder}}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state when no templates available -->
|
||||||
|
<div x-show="availableTemplates.length === 0" class="mb-4 text-sm p-3 rounded" style="background-color: var(--bg-tertiary); color: var(--text-secondary);">
|
||||||
|
No templates found. Create templates in the <code style="font-family: monospace;">_templates</code> folder.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="createNoteFromTemplate()"
|
||||||
|
:disabled="!selectedTemplate || !newTemplateNoteName.trim() || availableTemplates.length === 0"
|
||||||
|
class="flex-1 px-4 py-2 rounded font-medium text-white transition-colors"
|
||||||
|
style="background-color: var(--accent-primary);"
|
||||||
|
:style="(!selectedTemplate || !newTemplateNoteName.trim() || availableTemplates.length === 0) ? 'opacity: 0.5; cursor: not-allowed;' : ''"
|
||||||
|
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--accent-hover)'"
|
||||||
|
onmouseout="if(!this.disabled) this.style.backgroundColor='var(--accent-primary)'"
|
||||||
|
>
|
||||||
|
Create Note
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="showTemplateModal = false; selectedTemplate = ''; newTemplateNoteName = ''"
|
||||||
|
class="flex-1 px-4 py-2 rounded font-medium transition-colors"
|
||||||
|
style="background-color: var(--bg-tertiary); color: var(--text-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||||
|
onmouseout="this.style.backgroundColor='var(--bg-tertiary)'"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue