added properties pane
This commit is contained in:
parent
02e57a7745
commit
abe30a06ee
|
|
@ -7,7 +7,7 @@
|
|||
<!-- Primary Meta Tags -->
|
||||
<title>NoteDiscovery - Your Self-Hosted Knowledge Base</title>
|
||||
<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, tags, templates, and code highlighting. Self-hosted, free, and open source.">
|
||||
<meta name="description" content="A lightweight, privacy-focused Markdown note-taking application with wikilinks, 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, templates, tags, yaml frontmatter">
|
||||
<meta name="author" content="Gamosoft">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://www.notediscovery.com">
|
||||
<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, tags, templates, and code highlighting. Self-hosted, free, and open source.">
|
||||
<meta property="og:description" content="A lightweight, privacy-focused Markdown note-taking application with wikilinks, 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:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
|
|
@ -571,7 +571,7 @@
|
|||
<div class="feature">
|
||||
<div class="feature-icon">🏷️</div>
|
||||
<h3>Smart Tags</h3>
|
||||
<p>Organize with YAML frontmatter tags. Filter and discover notes instantly.</p>
|
||||
<p>Organize with YAML frontmatter tags. Collapsible properties panel shows metadata at a glance.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
|
|
@ -580,6 +580,12 @@
|
|||
<p>Create from custom templates with dynamic placeholders. Meeting notes, journals, and more.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h3>Wikilinks</h3>
|
||||
<p>Link notes with [[double brackets]] Obsidian-style. Broken links shown dimmed.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-icon">📂</div>
|
||||
<h3>Simple Storage</h3>
|
||||
|
|
@ -653,8 +659,8 @@
|
|||
<div class="benefit-item">
|
||||
<span class="emoji">🔗</span>
|
||||
<div class="text">
|
||||
<strong>Internal Links</strong>
|
||||
Connect notes with backlinks
|
||||
<strong>Wikilinks</strong>
|
||||
[[Link notes]] Obsidian-style
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -689,6 +695,14 @@
|
|||
Password protection when needed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">⚙️</span>
|
||||
<div class="text">
|
||||
<strong>Properties Panel</strong>
|
||||
View frontmatter metadata at a glance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -745,7 +759,7 @@
|
|||
"url": "https://www.notediscovery.com",
|
||||
"applicationCategory": "ProductivityApplication",
|
||||
"operatingSystem": "Linux, Windows, macOS",
|
||||
"featureList": "Markdown editor, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal links, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
|
||||
"featureList": "Markdown editor, Wikilinks, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal links, Properties panel, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,42 @@ tags: [python, tutorial, backend]
|
|||
- **Collapsible panel** - Saves state across sessions
|
||||
- **Auto-sync** - Updates after saving notes
|
||||
|
||||
## ⚙️ Note Properties Panel
|
||||
|
||||
View and interact with YAML frontmatter metadata directly in the preview.
|
||||
|
||||
### Features
|
||||
- **Collapsible panel** - Compact bar at the top of preview, expands on click
|
||||
- **Auto-hides** - Only appears when note has frontmatter
|
||||
- **Clickable tags** - Filter notes by clicking any tag
|
||||
- **Smart formatting** - Dates formatted nicely, booleans shown as ✓/✗
|
||||
- **URL detection** - Links in metadata are clickable
|
||||
- **Real-time updates** - Changes as you edit frontmatter
|
||||
- **Performance optimized** - Cached parsing, no re-parse if unchanged
|
||||
|
||||
### Collapsed View
|
||||
Shows tags as pills plus up to 3 priority fields (date, author, status, etc.)
|
||||
|
||||
### Expanded View
|
||||
Click to expand and see all metadata fields in a clean grid layout.
|
||||
|
||||
### Supported Formats
|
||||
```yaml
|
||||
---
|
||||
tags: [project, important] # Inline array
|
||||
date: 2024-01-15 # Formatted as "Jan 15, 2024"
|
||||
author: John Doe # String value
|
||||
status: draft # String value
|
||||
priority: high # String value
|
||||
source: https://example.com # Clickable link
|
||||
draft: true # Shows as "✓ Yes"
|
||||
custom-field: any value # Keys with hyphens supported
|
||||
items: # YAML list format
|
||||
- item 1
|
||||
- item 2
|
||||
---
|
||||
```
|
||||
|
||||
## 🔍 Search & Filtering
|
||||
|
||||
### Text Search
|
||||
|
|
|
|||
250
frontend/app.js
250
frontend/app.js
|
|
@ -86,6 +86,11 @@ function noteApp() {
|
|||
noteStats: null,
|
||||
statsExpanded: false,
|
||||
|
||||
// Note metadata (frontmatter) state
|
||||
noteMetadata: null,
|
||||
metadataExpanded: false,
|
||||
_lastFrontmatter: null, // Cache to avoid re-parsing unchanged frontmatter
|
||||
|
||||
// Sidebar resize state
|
||||
sidebarWidth: CONFIG.DEFAULT_SIDEBAR_WIDTH,
|
||||
isResizing: false,
|
||||
|
|
@ -1709,6 +1714,9 @@ function noteApp() {
|
|||
this.calculateStats();
|
||||
}
|
||||
|
||||
// Parse frontmatter metadata
|
||||
this.parseMetadata();
|
||||
|
||||
// Store search query for highlighting
|
||||
if (searchQuery) {
|
||||
this.currentSearchHighlight = searchQuery;
|
||||
|
|
@ -2253,6 +2261,9 @@ function noteApp() {
|
|||
this.calculateStats();
|
||||
}
|
||||
|
||||
// Parse metadata in real-time
|
||||
this.parseMetadata();
|
||||
|
||||
this.saveTimeout = setTimeout(() => {
|
||||
this.saveNote();
|
||||
}, CONFIG.AUTOSAVE_DELAY);
|
||||
|
|
@ -3099,6 +3110,245 @@ function noteApp() {
|
|||
};
|
||||
},
|
||||
|
||||
// Parse YAML frontmatter metadata from note content
|
||||
parseMetadata() {
|
||||
if (!this.noteContent) {
|
||||
this.noteMetadata = null;
|
||||
this._lastFrontmatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.noteContent;
|
||||
|
||||
// Check if content starts with frontmatter
|
||||
if (!content.trim().startsWith('---')) {
|
||||
this.noteMetadata = null;
|
||||
this._lastFrontmatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lines = content.split('\n');
|
||||
if (lines[0].trim() !== '---') {
|
||||
this.noteMetadata = null;
|
||||
this._lastFrontmatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find closing ---
|
||||
let endIdx = -1;
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '---') {
|
||||
endIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (endIdx === -1) {
|
||||
this.noteMetadata = null;
|
||||
this._lastFrontmatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Performance optimization: skip parsing if frontmatter unchanged
|
||||
const frontmatterRaw = lines.slice(0, endIdx + 1).join('\n');
|
||||
if (frontmatterRaw === this._lastFrontmatter) {
|
||||
return; // No change, keep existing metadata
|
||||
}
|
||||
this._lastFrontmatter = frontmatterRaw;
|
||||
|
||||
const frontmatterLines = lines.slice(1, endIdx);
|
||||
const metadata = {};
|
||||
let currentKey = null;
|
||||
let currentValue = [];
|
||||
|
||||
for (const line of frontmatterLines) {
|
||||
// Check for new key: value pair (supports keys with hyphens/underscores)
|
||||
const keyMatch = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
|
||||
|
||||
if (keyMatch) {
|
||||
// Save previous key if exists
|
||||
if (currentKey) {
|
||||
metadata[currentKey] = this.parseYamlValue(currentValue.join('\n'));
|
||||
}
|
||||
|
||||
currentKey = keyMatch[1];
|
||||
const value = keyMatch[2].trim();
|
||||
currentValue = [value];
|
||||
} else if (line.match(/^\s+-\s+/) && currentKey) {
|
||||
// List item continuation (e.g., " - item")
|
||||
currentValue.push(line);
|
||||
} else if (line.startsWith(' ') && currentKey) {
|
||||
// Indented content (multiline value)
|
||||
currentValue.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last key
|
||||
if (currentKey) {
|
||||
metadata[currentKey] = this.parseYamlValue(currentValue.join('\n'));
|
||||
}
|
||||
|
||||
this.noteMetadata = Object.keys(metadata).length > 0 ? metadata : null;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to parse frontmatter:', error);
|
||||
this.noteMetadata = null;
|
||||
this._lastFrontmatter = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Parse a YAML value (handles arrays, strings, numbers, booleans)
|
||||
parseYamlValue(value) {
|
||||
if (!value || value.trim() === '') return null;
|
||||
|
||||
value = value.trim();
|
||||
|
||||
// Check for inline array: [item1, item2]
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
const inner = value.slice(1, -1);
|
||||
return inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(s => s);
|
||||
}
|
||||
|
||||
// Check for YAML list format (multiple lines starting with -)
|
||||
if (value.includes('\n -') || value.startsWith(' -')) {
|
||||
const items = [];
|
||||
const lines = value.split('\n');
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*-\s*(.+)$/);
|
||||
if (match) {
|
||||
items.push(match[1].trim().replace(/^["']|["']$/g, ''));
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? items : value;
|
||||
}
|
||||
|
||||
// Check for boolean
|
||||
if (value.toLowerCase() === 'true') return true;
|
||||
if (value.toLowerCase() === 'false') return false;
|
||||
|
||||
// Check for number
|
||||
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
// Return as string (remove quotes if present)
|
||||
return value.replace(/^["']|["']$/g, '');
|
||||
},
|
||||
|
||||
// Check if a string is a URL
|
||||
isUrl(str) {
|
||||
if (typeof str !== 'string') return false;
|
||||
return /^https?:\/\/\S+$/i.test(str.trim());
|
||||
},
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
// Format metadata value for display
|
||||
formatMetadataValue(key, value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
// Arrays are handled separately in the template
|
||||
if (Array.isArray(value)) return value;
|
||||
|
||||
// Format dates nicely
|
||||
if (key === 'date' || key === 'created' || key === 'modified' || key === 'updated') {
|
||||
const date = new Date(value);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Booleans
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '✓ Yes' : '✗ No';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
},
|
||||
|
||||
// Format metadata value as HTML (for URL support)
|
||||
formatMetadataValueHtml(key, value) {
|
||||
const formatted = this.formatMetadataValue(key, value);
|
||||
|
||||
// Check if it's a URL
|
||||
if (this.isUrl(formatted)) {
|
||||
const escaped = this.escapeHtml(formatted);
|
||||
// Truncate long URLs for display
|
||||
const displayUrl = formatted.length > 40
|
||||
? formatted.substring(0, 37) + '...'
|
||||
: formatted;
|
||||
return `<a href="${escaped}" target="_blank" rel="noopener noreferrer" class="metadata-link">${this.escapeHtml(displayUrl)}</a>`;
|
||||
}
|
||||
|
||||
return this.escapeHtml(formatted);
|
||||
},
|
||||
|
||||
// Get priority metadata fields (shown in collapsed view)
|
||||
getPriorityMetadataFields() {
|
||||
if (!this.noteMetadata) return [];
|
||||
|
||||
// Fields to show in collapsed view, in order of priority
|
||||
const priority = ['date', 'created', 'author', 'status', 'priority', 'type', 'category'];
|
||||
const fields = [];
|
||||
|
||||
for (const key of priority) {
|
||||
if (this.noteMetadata[key] !== undefined && !Array.isArray(this.noteMetadata[key])) {
|
||||
const formatted = this.formatMetadataValue(key, this.noteMetadata[key]);
|
||||
const isUrl = this.isUrl(formatted);
|
||||
fields.push({
|
||||
key,
|
||||
value: formatted,
|
||||
valueHtml: isUrl ? this.formatMetadataValueHtml(key, this.noteMetadata[key]) : this.escapeHtml(formatted),
|
||||
isUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fields.slice(0, 3); // Show max 3 fields in collapsed view
|
||||
},
|
||||
|
||||
// Get all metadata fields except tags (for expanded view)
|
||||
getAllMetadataFields() {
|
||||
if (!this.noteMetadata) return [];
|
||||
|
||||
return Object.entries(this.noteMetadata)
|
||||
.filter(([key]) => key !== 'tags') // Tags shown separately
|
||||
.map(([key, value]) => {
|
||||
const isArray = Array.isArray(value);
|
||||
const formatted = this.formatMetadataValue(key, value);
|
||||
const isUrl = !isArray && this.isUrl(formatted);
|
||||
return {
|
||||
key,
|
||||
value: formatted,
|
||||
valueHtml: isUrl ? this.formatMetadataValueHtml(key, value) : this.escapeHtml(formatted),
|
||||
isArray,
|
||||
isUrl
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Check if note has any displayable metadata
|
||||
getHasMetadata() {
|
||||
const has = this.noteMetadata && Object.keys(this.noteMetadata).length > 0;
|
||||
return has;
|
||||
},
|
||||
|
||||
// Get tags from metadata
|
||||
getMetadataTags() {
|
||||
if (!this.noteMetadata || !this.noteMetadata.tags) return [];
|
||||
return Array.isArray(this.noteMetadata.tags) ? this.noteMetadata.tags : [this.noteMetadata.tags];
|
||||
},
|
||||
|
||||
// Load sidebar width from localStorage
|
||||
loadSidebarWidth() {
|
||||
const saved = localStorage.getItem('sidebarWidth');
|
||||
|
|
|
|||
|
|
@ -290,6 +290,24 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Note Properties Panel */
|
||||
.note-properties {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.note-properties .metadata-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
.note-properties .metadata-link {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none !important;
|
||||
border-bottom: 1px dashed var(--accent-primary);
|
||||
transition: border-bottom-style 0.15s;
|
||||
}
|
||||
.note-properties .metadata-link:hover {
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
/* Standard internal links (non-wikilink, e.g. [text](path)) */
|
||||
.markdown-preview a:not([href^="http"]):not([href^="https"]):not([href^="//"]):not([href^="mailto:"]):not([data-wikilink]) {
|
||||
color: var(--accent-primary);
|
||||
|
|
@ -1498,8 +1516,119 @@
|
|||
style="background-color: var(--bg-primary); min-height: 0;"
|
||||
:style="viewMode === 'split' ? `width: ${100 - editorWidth}%;` : 'width: 100%;'"
|
||||
>
|
||||
<!-- Note Properties/Metadata Panel -->
|
||||
<div
|
||||
x-show="getHasMetadata() && currentNote"
|
||||
class="note-properties mx-6 mt-4 mb-2"
|
||||
>
|
||||
<!-- Collapsed View -->
|
||||
<div
|
||||
x-show="!metadataExpanded"
|
||||
@click="metadataExpanded = true"
|
||||
class="flex flex-wrap items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all"
|
||||
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
|
||||
onmouseover="this.style.borderColor='var(--accent-primary)'"
|
||||
onmouseout="this.style.borderColor='var(--border-primary)'"
|
||||
>
|
||||
<!-- Property icon -->
|
||||
<span class="text-xs opacity-60">⚙️</span>
|
||||
|
||||
<!-- Tags as pills -->
|
||||
<template x-for="tag in getMetadataTags().slice(0, 5)" :key="tag">
|
||||
<span
|
||||
@click.stop="toggleTag(tag.toLowerCase())"
|
||||
class="metadata-tag px-2 py-0.5 text-xs rounded-full cursor-pointer transition-colors"
|
||||
style="background-color: var(--accent-light, rgba(124, 58, 237, 0.15)); color: var(--accent-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--accent-primary)'; this.style.color='white'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-light, rgba(124, 58, 237, 0.15))'; this.style.color='var(--accent-primary)'"
|
||||
x-text="'#' + tag"
|
||||
></span>
|
||||
</template>
|
||||
<template x-if="getMetadataTags().length > 5">
|
||||
<span class="text-xs" style="color: var(--text-tertiary);">+<span x-text="getMetadataTags().length - 5"></span> more</span>
|
||||
</template>
|
||||
|
||||
<!-- Priority fields (date, author, status) -->
|
||||
<template x-for="field in getPriorityMetadataFields()" :key="field.key">
|
||||
<span class="text-xs flex items-center gap-1" style="color: var(--text-secondary);">
|
||||
<span class="opacity-50">·</span>
|
||||
<span class="capitalize" x-text="field.key + ':'"></span>
|
||||
<span style="color: var(--text-primary);" x-html="field.valueHtml" @click="field.isUrl && $event.stopPropagation()"></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Expand hint -->
|
||||
<span class="ml-auto text-xs opacity-40">▼</span>
|
||||
</div>
|
||||
|
||||
<!-- Expanded View -->
|
||||
<div
|
||||
x-show="metadataExpanded"
|
||||
class="rounded-lg overflow-hidden"
|
||||
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
@click="metadataExpanded = false"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer"
|
||||
style="border-bottom: 1px solid var(--border-primary);"
|
||||
>
|
||||
<span class="text-xs font-medium" style="color: var(--text-secondary);">
|
||||
⚙️ Properties
|
||||
</span>
|
||||
<span class="text-xs opacity-40">▲ Collapse</span>
|
||||
</div>
|
||||
|
||||
<!-- Properties Grid -->
|
||||
<div class="px-3 py-2 space-y-2">
|
||||
<!-- Tags row -->
|
||||
<template x-if="getMetadataTags().length > 0">
|
||||
<div class="flex flex-wrap gap-1.5 items-start">
|
||||
<span class="text-xs w-16 shrink-0 pt-0.5" style="color: var(--text-tertiary);">tags</span>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="tag in getMetadataTags()" :key="tag">
|
||||
<span
|
||||
@click="toggleTag(tag.toLowerCase())"
|
||||
class="metadata-tag px-2 py-0.5 text-xs rounded-full cursor-pointer transition-colors"
|
||||
style="background-color: var(--accent-light, rgba(124, 58, 237, 0.15)); color: var(--accent-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--accent-primary)'; this.style.color='white'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-light, rgba(124, 58, 237, 0.15))'; this.style.color='var(--accent-primary)'"
|
||||
x-text="'#' + tag"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Other fields -->
|
||||
<template x-for="field in getAllMetadataFields()" :key="field.key">
|
||||
<div class="flex gap-1.5 items-start text-xs">
|
||||
<span class="w-16 shrink-0 pt-0.5" style="color: var(--text-tertiary);" x-text="field.key"></span>
|
||||
<!-- Array values -->
|
||||
<template x-if="field.isArray">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="item in field.value" :key="item">
|
||||
<span
|
||||
class="px-1.5 py-0.5 rounded text-xs"
|
||||
style="background-color: var(--bg-hover); color: var(--text-primary);"
|
||||
x-text="item"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Single values -->
|
||||
<template x-if="!field.isArray">
|
||||
<span style="color: var(--text-primary);" x-html="field.valueHtml"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-6 markdown-preview"
|
||||
:class="{ 'pt-2': getHasMetadata() && currentNote }"
|
||||
x-html="renderedMarkdown"
|
||||
@click="handleInternalLink($event)"
|
||||
></div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue