NoteDiscovery/frontend/index.html

1312 lines
63 KiB
HTML
Raw Normal View History

2025-11-05 16:48:41 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NoteDiscovery</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon.svg">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon.svg">
<link rel="apple-touch-icon" href="/static/logo.svg">
<!-- Tailwind CSS from CDN (v3.4.17) -->
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
2025-11-05 16:48:41 +00:00
<!-- Alpine.js (v3.14.1) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
2025-11-05 16:48:41 +00:00
<!-- Marked.js for Markdown parsing (v12.0.2) -->
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
2025-11-05 16:48:41 +00:00
2025-11-11 13:51:55 +00:00
<!-- MathJax for LaTeX math rendering -->
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: true,
processEnvironments: true
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
},
startup: {
pageReady: () => {
return MathJax.startup.defaultPageReady();
}
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
2025-11-11 13:51:55 +00:00
<!-- Highlight.js for code syntax highlighting (v11.9.0) -->
2025-11-05 16:48:41 +00:00
<link rel="stylesheet" id="highlight-theme" href="">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/csharp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/shell.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/http.min.js"></script>
<!-- Mermaid.js for diagram rendering (v10.9.0) -->
2025-11-17 16:29:00 +00:00
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10.9.0/dist/mermaid.esm.min.mjs';
2025-11-17 16:37:29 +00:00
// Note: Mermaid is initialized dynamically by app.js renderMermaid() based on current theme
2025-11-17 16:29:00 +00:00
window.mermaid = mermaid;
</script>
2025-11-05 16:48:41 +00:00
<!-- Theme styles will be loaded dynamically -->
<link rel="stylesheet" id="theme-stylesheet" href="">
<!-- Custom styles -->
<style>
[x-cloak] { display: none !important; }
/* Theme-aware styles using CSS variables */
body {
background-color: var(--bg-primary, #ffffff);
color: var(--text-primary, #111827);
}
.bg-themed {
background-color: var(--bg-primary);
}
.bg-themed-secondary {
background-color: var(--bg-secondary);
}
.text-themed {
color: var(--text-primary);
}
.text-themed-secondary {
color: var(--text-secondary);
}
.border-themed {
border-color: var(--border-primary);
}
.bg-accent {
background-color: var(--accent-primary);
}
.bg-accent:hover {
background-color: var(--accent-hover);
}
.text-accent {
color: var(--accent-primary);
}
/* Markdown preview styling */
.markdown-preview {
line-height: 1.8;
color: var(--text-primary);
font-size: 16px;
}
/* Remove top margin from first element */
.markdown-preview > *:first-child {
margin-top: 0 !important;
}
/* Headings */
.markdown-preview h1 {
font-size: 2.25rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--text-primary);
border-bottom: 2px solid var(--border-primary);
padding-bottom: 0.5rem;
}
.markdown-preview h2 {
font-size: 1.875rem;
font-weight: 700;
margin-top: 1.75rem;
margin-bottom: 0.875rem;
color: var(--text-primary);
border-bottom: 1px solid var(--border-primary);
padding-bottom: 0.3rem;
}
.markdown-preview h3 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.markdown-preview h4 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.25rem;
margin-bottom: 0.625rem;
color: var(--text-primary);
}
.markdown-preview h5 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.markdown-preview h6 {
font-size: 1rem;
font-weight: 600;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
/* Paragraphs and text */
.markdown-preview p {
margin-bottom: 1rem;
line-height: 1.7;
}
.markdown-preview strong {
font-weight: 600;
color: var(--text-primary);
}
.markdown-preview em {
font-style: italic;
}
/* Lists */
.markdown-preview ul, .markdown-preview ol {
margin-bottom: 1rem;
margin-left: 1.5rem;
padding-left: 0.5rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview li {
margin-bottom: 0.5rem;
line-height: 1.6;
}
.markdown-preview li > ul,
.markdown-preview li > ol {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
/* Inline code */
.markdown-preview code:not(pre code) {
background-color: var(--bg-tertiary);
color: var(--accent-primary);
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-weight: 500;
}
/* Code blocks */
.markdown-preview pre {
background-color: var(--bg-tertiary);
margin-bottom: 1.5rem;
border-radius: 0.5rem;
overflow-x: auto;
border: 1px solid var(--border-primary);
}
.markdown-preview pre code {
background: transparent;
padding: 1rem;
display: block;
font-size: 0.875rem;
line-height: 1.6;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
/* Blockquotes */
.markdown-preview blockquote {
border-left: 4px solid var(--accent-primary);
padding-left: 1rem;
margin: 1.5rem 0;
font-style: italic;
color: var(--text-secondary);
background-color: var(--bg-tertiary);
padding: 1rem;
padding-left: 1rem;
border-radius: 0.25rem;
}
.markdown-preview blockquote p {
margin-bottom: 0;
}
/* Links */
.markdown-preview a {
color: var(--accent-primary);
text-decoration: underline;
text-decoration-color: transparent;
transition: text-decoration-color 0.2s;
cursor: pointer;
}
.markdown-preview a:hover {
text-decoration-color: var(--accent-primary);
}
/* Internal links (not starting with http/https) get a special style */
.markdown-preview a:not([href^="http"]):not([href^="https"]):not([href^="//"]):not([href^="mailto:"]) {
color: var(--accent-primary);
font-weight: 500;
}
.markdown-preview a:not([href^="http"]):not([href^="https"]):not([href^="//"]):not([href^="mailto:"]):before {
content: "→ ";
font-size: 0.9em;
opacity: 0.7;
}
/* Tables */
.markdown-preview table {
width: 100%;
margin-bottom: 1.5rem;
border-collapse: collapse;
border: 1px solid var(--border-primary);
}
.markdown-preview th, .markdown-preview td {
border: 1px solid var(--border-primary);
padding: 0.75rem 1rem;
text-align: left;
}
.markdown-preview th {
background-color: var(--bg-tertiary);
font-weight: 600;
color: var(--text-primary);
}
.markdown-preview tr:nth-child(even) {
background-color: var(--bg-secondary);
}
/* Horizontal rule */
.markdown-preview hr {
border: none;
border-top: 2px solid var(--border-primary);
margin: 2rem 0;
}
/* Images */
.markdown-preview img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
}
/* Task lists */
.markdown-preview input[type="checkbox"] {
margin-right: 0.5rem;
}
2025-11-11 13:51:55 +00:00
/* Math/LaTeX styling */
.markdown-preview .MathJax {
color: var(--text-primary);
overflow-x: auto;
overflow-y: hidden;
}
/* Display math (block) */
.markdown-preview mjx-container[display="true"] {
margin: 1.5rem 0;
overflow-x: auto;
}
/* Inline math */
.markdown-preview mjx-container[display="false"] {
margin: 0 0.2rem;
}
2025-11-17 16:29:00 +00:00
/* Mermaid diagram styling - minimal, let Mermaid handle its own rendering */
.markdown-preview .mermaid-rendered svg {
max-width: 100%;
height: auto;
}
2025-11-05 16:48:41 +00:00
/* Editor styling */
.editor-textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
tab-size: 2;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
/* Link drop zone cursor */
.editor-textarea.link-drop-target {
cursor: copy !important;
}
/* Folder tree styling */
.folder-item {
user-select: none;
2025-11-18 09:03:47 +00:00
transition: all 0.2s ease;
2025-11-05 16:48:41 +00:00
}
.folder-item:hover {
background-color: var(--bg-hover);
}
.folder-expanded {
font-weight: 600;
}
.note-item {
padding-left: 1.5rem;
2025-11-18 09:03:47 +00:00
transition: all 0.2s ease;
}
/* Drag and drop improvements */
.note-item[draggable="true"],
.folder-item[draggable="true"] {
cursor: grab;
}
.note-item[draggable="true"]:active,
.folder-item[draggable="true"]:active {
cursor: grabbing;
}
/* Smooth transitions for drag states */
.folder-item,
.note-item {
transition: opacity 0.2s ease, transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}
/* Better drop zone visibility */
.folder-item:has(+ .folder-contents) {
position: relative;
2025-11-05 16:48:41 +00:00
}
.hover-buttons {
transition: opacity 0.2s;
}
.folder-item:hover .hover-buttons {
opacity: 1 !important;
pointer-events: auto !important;
}
/* Note delete button on hover */
.note-item:hover .note-delete-btn {
opacity: 1 !important;
}
/* Folder hierarchy lines (Obsidian-style) */
.folder-contents {
position: relative;
padding-left: 1rem;
margin-left: 0.5rem;
}
.folder-contents::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--text-tertiary);
2025-11-05 16:48:41 +00:00
opacity: 0.4;
}
/* Scrollbar styling */
.custom-scrollbar::-webkit-scrollbar,
.editor-textarea::-webkit-scrollbar,
.markdown-preview::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.custom-scrollbar::-webkit-scrollbar-track,
.editor-textarea::-webkit-scrollbar-track,
.markdown-preview::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
.custom-scrollbar::-webkit-scrollbar-thumb,
.editor-textarea::-webkit-scrollbar-thumb,
.markdown-preview::-webkit-scrollbar-thumb {
background: var(--border-secondary);
border-radius: 6px;
border: 2px solid var(--bg-tertiary);
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover,
.editor-textarea::-webkit-scrollbar-thumb:hover,
.markdown-preview::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Firefox scrollbar */
.custom-scrollbar,
.editor-textarea,
.markdown-preview {
scrollbar-width: thin;
scrollbar-color: var(--border-secondary) var(--bg-tertiary);
}
/* Resize handle */
.resize-handle {
position: relative;
user-select: none;
flex-shrink: 0;
}
.resize-handle:hover {
background-color: var(--accent-primary) !important;
}
.resize-handle:active {
background-color: var(--accent-hover) !important;
}
/* Split view resize handle */
.split-resize-handle {
position: relative;
user-select: none;
flex-shrink: 0;
}
.split-resize-handle:hover {
background-color: var(--accent-primary) !important;
}
.split-resize-handle:active {
background-color: var(--accent-hover) !important;
}
2025-11-05 16:48:41 +00:00
/* Enhanced Shell/Bash Syntax Highlighting */
.markdown-preview pre code.language-shell .hljs-meta,
.markdown-preview pre code.language-bash .hljs-meta,
.markdown-preview pre code.language-sh .hljs-meta {
color: #7c3aed !important; /* Purple for prompts like $ # */
font-weight: 600;
}
.markdown-preview pre code.language-shell .hljs-built_in,
.markdown-preview pre code.language-bash .hljs-built_in,
.markdown-preview pre code.language-sh .hljs-built_in {
color: #10b981 !important; /* Green for built-in commands */
font-weight: 500;
}
.markdown-preview pre code.language-shell .hljs-string,
.markdown-preview pre code.language-bash .hljs-string,
.markdown-preview pre code.language-sh .hljs-string {
color: #f59e0b !important; /* Orange for strings */
}
.markdown-preview pre code.language-shell .hljs-variable,
.markdown-preview pre code.language-bash .hljs-variable,
.markdown-preview pre code.language-sh .hljs-variable {
color: #06b6d4 !important; /* Cyan for variables */
font-weight: 500;
}
.markdown-preview pre code.language-shell .hljs-comment,
.markdown-preview pre code.language-bash .hljs-comment,
.markdown-preview pre code.language-sh .hljs-comment {
color: #6b7280 !important; /* Gray for comments */
font-style: italic;
}
.markdown-preview pre code.language-shell .hljs-keyword,
.markdown-preview pre code.language-bash .hljs-keyword,
.markdown-preview pre code.language-sh .hljs-keyword {
color: #ec4899 !important; /* Pink for keywords like if, then, else */
font-weight: 600;
}
.markdown-preview pre code.language-shell .hljs-number,
.markdown-preview pre code.language-bash .hljs-number,
.markdown-preview pre code.language-sh .hljs-number {
color: #8b5cf6 !important; /* Purple for numbers */
}
.markdown-preview pre code.language-shell .hljs-literal,
.markdown-preview pre code.language-bash .hljs-literal,
.markdown-preview pre code.language-sh .hljs-literal {
color: #3b82f6 !important; /* Blue for literals like true, false */
font-weight: 500;
}
/* Enhanced PowerShell Syntax Highlighting */
.markdown-preview pre code.language-powershell .hljs-built_in,
.markdown-preview pre code.language-ps1 .hljs-built_in,
.markdown-preview pre code.language-ps .hljs-built_in {
color: #10b981 !important; /* Green for cmdlets like Get-Process, Set-Location */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-literal,
.markdown-preview pre code.language-ps1 .hljs-literal,
.markdown-preview pre code.language-ps .hljs-literal {
color: #3b82f6 !important; /* Blue for $true, $false, $null */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-variable,
.markdown-preview pre code.language-ps1 .hljs-variable,
.markdown-preview pre code.language-ps .hljs-variable {
color: #06b6d4 !important; /* Cyan for $variables */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-string,
.markdown-preview pre code.language-ps1 .hljs-string,
.markdown-preview pre code.language-ps .hljs-string {
color: #f59e0b !important; /* Orange for strings */
}
.markdown-preview pre code.language-powershell .hljs-keyword,
.markdown-preview pre code.language-ps1 .hljs-keyword,
.markdown-preview pre code.language-ps .hljs-keyword {
color: #ec4899 !important; /* Pink for keywords like if, else, foreach */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-params,
.markdown-preview pre code.language-ps1 .hljs-params,
.markdown-preview pre code.language-ps .hljs-params {
color: #8b5cf6 !important; /* Purple for -Parameters */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-type,
.markdown-preview pre code.language-ps1 .hljs-type,
.markdown-preview pre code.language-ps .hljs-type {
color: #14b8a6 !important; /* Teal for [Types] */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-comment,
.markdown-preview pre code.language-ps1 .hljs-comment,
.markdown-preview pre code.language-ps .hljs-comment {
color: #6b7280 !important; /* Gray for comments */
font-style: italic;
}
.markdown-preview pre code.language-powershell .hljs-number,
.markdown-preview pre code.language-ps1 .hljs-number,
.markdown-preview pre code.language-ps .hljs-number {
color: #8b5cf6 !important; /* Purple for numbers */
}
.markdown-preview pre code.language-powershell .hljs-operator,
.markdown-preview pre code.language-ps1 .hljs-operator,
.markdown-preview pre code.language-ps .hljs-operator {
color: #f472b6 !important; /* Pink for operators */
}
2025-11-13 14:09:26 +00:00
/* Mobile Responsive Styles */
@media (max-width: 768px) {
/* Hide sidebar by default on mobile */
.mobile-sidebar {
position: fixed;
left: -100%;
top: 0;
height: 100vh;
width: 80% !important;
max-width: 300px !important;
z-index: 1000;
transition: left 0.3s ease;
}
/* Show sidebar when open */
.mobile-sidebar-open {
left: 0;
}
/* Mobile overlay */
.mobile-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
.mobile-overlay-visible {
display: block;
}
/* Hamburger button */
.mobile-menu-button {
display: flex !important;
}
/* Hide desktop resize handle on mobile */
.sidebar-resize-handle,
.resize-handle {
display: none;
}
/* Make editor take full width on mobile */
.mobile-editor {
width: 100% !important;
}
/* Stack view modes vertically on mobile */
.mobile-view-toggle {
flex-direction: column;
gap: 0.25rem;
}
/* Smaller buttons on mobile */
.mobile-toolbar button {
padding: 0.5rem;
font-size: 0.875rem;
}
/* Smaller text input on mobile */
.mobile-toolbar input {
min-width: 100px !important;
max-width: 200px !important;
font-size: 1rem !important;
}
/* Hide split view on mobile (not enough space) */
.mobile-hide-split {
display: none !important;
}
/* Make toolbar wrap on very small screens */
.mobile-toolbar {
flex-wrap: wrap;
gap: 0.5rem;
}
/* Larger touch targets */
.note-item,
.folder-item {
padding: 0.75rem !important;
min-height: 44px;
}
}
/* Hide mobile menu button on desktop */
@media (min-width: 769px) {
.mobile-menu-button {
display: none !important;
}
}
2025-11-05 16:48:41 +00:00
</style>
</head>
<body x-data="noteApp()" x-init="init()" style="background-color: var(--bg-primary);">
2025-11-13 14:09:26 +00:00
<!-- Mobile Overlay -->
<div
@click="mobileSidebarOpen = false"
:class="{'mobile-overlay': true, 'mobile-overlay-visible': mobileSidebarOpen}"
x-show="mobileSidebarOpen"
></div>
2025-11-05 16:48:41 +00:00
<!-- Main Container -->
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
2025-11-13 14:09:26 +00:00
<div
class="flex flex-col mobile-sidebar"
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); border-right: 1px solid var(--border-primary); min-width: 200px; max-width: 600px;'"
>
2025-11-05 16:48:41 +00:00
<!-- Header -->
<div class="p-4 border-b" style="border-color: var(--border-primary);">
<div class="flex items-center gap-2 mb-3">
<img src="/static/logo.svg" alt="NoteDiscovery Logo" class="w-8 h-8">
<h1 class="text-xl font-bold" style="color: var(--text-primary);" x-text="appName"></h1>
</div>
<!-- Theme Selector -->
<div class="mb-3">
<label class="text-xs mb-1 block" style="color: var(--text-tertiary);">Theme</label>
<select
x-model="currentTheme"
@change="setTheme(currentTheme)"
class="w-full px-3 py-2 text-sm rounded border focus:outline-none focus:ring-2 focus:ring-opacity-50"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border-primary); cursor: pointer;"
>
<template x-for="theme in availableThemes" :key="theme.id">
<option :value="theme.id" x-text="theme.name"></option>
</template>
</select>
</div>
2025-11-14 13:34:19 +00:00
<p class="text-xs mb-3" style="color: var(--text-secondary);" x-text="appTagline"></p>
<!-- Logout Button (only show if auth is enabled) -->
<div x-data="{ authEnabled: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => authEnabled = d.security?.enabled || false)">
<a x-show="authEnabled"
href="/logout"
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
style="background-color: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-primary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'; this.style.color='var(--text-primary)'"
onmouseout="this.style.backgroundColor='var(--bg-secondary)'; this.style.color='var(--text-secondary)'">
<span>🔒</span>
<span>Logout</span>
</a>
</div>
2025-11-05 16:48:41 +00:00
</div>
<!-- Search -->
<div class="p-3 border-b" style="border-color: var(--border-primary);">
<div class="relative">
<input
type="text"
x-model="searchQuery"
@input="searchNotes()"
placeholder="Search notes..."
class="w-full px-3 py-2 pl-8 pr-8 text-sm rounded-lg focus:outline-none focus:ring-2"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
>
<!-- Search Icon -->
<svg class="absolute left-2 top-2.5 w-4 h-4" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<!-- Clear Button -->
<button
x-show="searchQuery.length > 0"
@click="searchQuery = ''; searchResults = []; currentSearchHighlight = ''; clearSearchHighlights();"
class="absolute right-2 top-2.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
style="color: var(--text-tertiary);"
title="Clear search"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Match Navigation (shown when there are matches in current note) -->
<div
x-show="totalMatches > 0"
class="flex items-center justify-between mt-2 px-2 py-1 rounded text-xs"
style="background-color: var(--bg-secondary); color: var(--text-secondary);"
>
<span x-text="`Match ${currentMatchIndex + 1} of ${totalMatches}`"></span>
<div class="flex gap-1">
<button
@click="previousMatch()"
class="px-2 py-1 rounded hover:bg-opacity-80 transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary);"
title="Previous match (Shift+F3)"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button
@click="nextMatch()"
class="px-2 py-1 rounded hover:bg-opacity-80 transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary);"
title="Next match (F3)"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
2025-11-17 13:20:57 +00:00
<!-- New Item Dropdown -->
<div class="p-3 border-b relative" style="border-color: var(--border-primary);">
2025-11-05 16:48:41 +00:00
<button
2025-11-17 13:20:57 +00:00
@click="toggleNewDropdown()"
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"
2025-11-05 16:48:41 +00:00
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
2025-11-17 13:20:57 +00:00
<span>+ New</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
2025-11-05 16:48:41 +00:00
</button>
2025-11-17 13:20:57 +00:00
<!-- 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);"
2025-11-05 16:48:41 +00:00
>
2025-11-17 13:20:57 +00:00
<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>
2025-11-05 16:48:41 +00:00
</div>
<!-- Notes List -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<template x-if="searchResults.length > 0">
<div class="p-2">
<div class="text-xs px-2 py-1" style="color: var(--text-tertiary);">Search Results</div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="loadNote(note.path, true, searchQuery)"
class="px-3 py-2 mb-1 text-sm rounded cursor-pointer"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
>
<div class="font-medium truncate" x-text="note.name"></div>
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-text="note.folder || 'Root'"></div>
</div>
</template>
</div>
</template>
<template x-if="searchResults.length === 0">
<div class="p-2">
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
<div class="flex items-center justify-between mb-1">
<span>Folders & Notes</span>
<div class="flex gap-1">
<button
@click="expandAllFolders()"
class="text-xs px-2 py-0.5 rounded hover:bg-opacity-20"
style="color: var(--text-secondary);"
title="Expand all folders"
>⊞</button>
<button
@click="collapseAllFolders()"
class="text-xs px-2 py-0.5 rounded hover:bg-opacity-20"
style="color: var(--text-secondary);"
title="Collapse all folders"
>⊟</button>
</div>
</div>
2025-11-18 09:27:01 +00:00
<!-- Dual-purpose: hint text OR root drop zone when dragging -->
<div
2025-11-18 09:47:23 +00:00
class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center"
:class="{
'border-2 border-dashed bg-accent-light': (draggedNote || draggedFolder) && dragOverFolder === '',
'border-2 border-dashed': (draggedNote || draggedFolder) && dragOverFolder !== '',
'border-2 border-transparent': !draggedNote && !draggedFolder
}"
:style="{
'color': (draggedNote || draggedFolder) ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)',
'opacity': (draggedNote || draggedFolder) ? '1' : '0.8',
'border-color': (draggedNote || draggedFolder) && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)',
'background-color': dragOverFolder === '' ? 'var(--accent-light)' : ''
}"
2025-11-18 09:27:01 +00:00
@dragover.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';"
@dragenter.prevent="if(draggedNote || draggedFolder) dragOverFolder = '';"
@dragleave="if(draggedNote || draggedFolder) dragOverFolder = null;"
@drop="if(draggedNote || draggedFolder) onFolderDrop('')"
2025-11-18 09:47:23 +00:00
style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;"
2025-11-18 09:27:01 +00:00
>
<span x-show="!draggedNote && !draggedFolder">💡 Drag=Move | <kbd style="padding: 1px 4px; border-radius: 3px; background: var(--bg-tertiary); font-size: 10px;">Ctrl</kbd>+Drag=Link</span>
2025-11-18 09:47:23 +00:00
<span x-show="draggedNote || draggedFolder">📂 Root folder</span>
2025-11-05 16:48:41 +00:00
</div>
</div>
2025-11-08 11:23:07 +00:00
<!-- Folders (rendered recursively for unlimited nesting) -->
2025-11-05 16:48:41 +00:00
<template x-for="[folderKey, folder] in Object.entries(folderTree).filter(([k]) => k !== '__root__').sort((a, b) => a[1].name.toLowerCase().localeCompare(b[1].name.toLowerCase()))" :key="folderKey">
2025-11-08 11:28:31 +00:00
<div x-html="renderFolderRecursive(folder, 0, true)"></div>
2025-11-05 16:48:41 +00:00
</template>
<!-- Root notes (no folder) - shown after folders -->
<template x-for="note in (folderTree.__root__ && folderTree.__root__.notes ? folderTree.__root__.notes : [])" :key="note.path">
<div
draggable="true"
@dragstart="onNoteDragStart(note.path, $event)"
@dragend="onNoteDragEnd()"
@click="loadNote(note.path)"
2025-11-18 09:27:01 +00:00
class="note-item px-3 py-2 mb-1 text-sm rounded relative border-2 border-transparent"
2025-11-05 16:48:41 +00:00
:style="(currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);') + (draggedNote || draggedFolder ? ' cursor: grabbing;' : ' cursor: pointer;')"
@mouseover="if(currentNote !== note.path) $el.style.backgroundColor='var(--bg-hover)'"
@mouseout="if(currentNote !== note.path) $el.style.backgroundColor='transparent'"
>
<span class="truncate" x-text="note.name"></span>
<button
@click.stop="deleteNote(note.path, note.name)"
class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
style="opacity: 0; color: var(--error);"
title="Delete note"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</template>
<!-- Empty state -->
<template x-if="notes.length === 0 && allFolders.length === 0">
<div class="px-3 py-4 text-sm text-center" style="color: var(--text-tertiary);">
No notes or folders yet.<br>Create your first note or folder!
</div>
</template>
</div>
</template>
</div>
<!-- Footer -->
<div class="p-3 border-t" style="border-color: var(--border-primary);">
<div class="text-xs text-center" style="color: var(--text-tertiary);">
<span x-text="notes.length"></span> notes
</div>
</div>
</div>
<!-- Resize Handle -->
<div
@mousedown="startResize($event)"
class="resize-handle"
style="width: 4px; cursor: col-resize; background-color: transparent; transition: background-color 0.2s;"
@mouseover="$el.style.backgroundColor='var(--accent-primary)'"
@mouseout="if(!isResizing) $el.style.backgroundColor='transparent'"
></div>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden">
<template x-if="!currentNote">
<!-- Welcome Screen -->
<div class="flex-1 flex items-center justify-center" style="background-color: var(--bg-primary);">
<div class="text-center">
<svg class="mx-auto h-16 w-16 mb-4" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="appName"></h2>
<p class="mb-6" style="color: var(--text-secondary);" x-text="appTagline"></p>
<button
2025-11-17 13:24:40 +00:00
@click="createNote()"
2025-11-05 16:48:41 +00:00
class="px-6 py-3 text-sm font-medium text-white rounded-lg"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
Create Your First Note
</button>
</div>
</div>
</template>
<template x-if="currentNote">
<!-- Editor Area -->
<div class="flex-1 flex flex-col overflow-hidden" style="background-color: var(--bg-primary);">
<!-- Toolbar -->
2025-11-13 14:09:26 +00:00
<div class="px-4 py-3 flex items-center justify-between mobile-toolbar" style="background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-primary);">
2025-11-05 16:48:41 +00:00
<div class="flex items-center space-x-2">
2025-11-13 14:09:26 +00:00
<!-- Mobile Menu Button -->
<button
@click="mobileSidebarOpen = !mobileSidebarOpen"
class="mobile-menu-button p-2 rounded-lg"
style="color: var(--text-primary); display: none;"
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>
2025-11-05 16:48:41 +00:00
<input
type="text"
x-model="currentNoteName"
@blur="renameNote()"
class="text-xl font-semibold border-none focus:outline-none focus:ring-2 rounded px-3 py-1.5"
2025-11-13 14:09:26 +00:00
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); min-width: 150px; max-width: 300px;"
2025-11-05 16:48:41 +00:00
>
<!-- Undo Button -->
<button
@click="undo()"
:disabled="undoHistory.length <= 1"
class="p-2 rounded-lg"
:style="undoHistory.length <= 1 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
title="Undo (Ctrl+Z)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
</svg>
</button>
<!-- Redo Button -->
<button
@click="redo()"
:disabled="redoHistory.length === 0"
class="p-2 rounded-lg"
:style="redoHistory.length === 0 ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
title="Redo (Ctrl+Y)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6"></path>
</svg>
</button>
<!-- Delete Button -->
<button
@click="deleteCurrentNote()"
class="p-2 rounded-lg"
style="color: var(--error);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
title="Delete note"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
<span x-show="isSaving" class="text-xs" style="color: var(--text-secondary);">Saving...</span>
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);">✓ Saved</span>
</div>
<div class="flex items-center space-x-2">
<!-- View Toggle -->
<div class="flex rounded-lg p-1" style="background-color: var(--bg-tertiary);">
<button
@click="viewMode = 'edit'"
2025-11-13 14:09:26 +00:00
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'edit' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
>
Edit
</button>
<button
@click="viewMode = 'split'"
2025-11-13 14:09:26 +00:00
class="mobile-hide-split px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'split' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
>
Split
</button>
<button
@click="viewMode = 'preview'"
2025-11-13 14:09:26 +00:00
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'preview' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
>
Preview
</button>
</div>
2025-11-12 10:16:12 +00:00
<!-- Export HTML Button -->
<button
x-show="currentNote"
@click="exportToHTML()"
2025-11-13 14:09:26 +00:00
class="px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-sm rounded transition hover:opacity-80"
2025-11-12 10:16:12 +00:00
style="background-color: var(--accent-secondary); color: var(--text-primary);"
title="Export as HTML"
>
2025-11-13 14:09:26 +00:00
<span class="hidden md:inline">📄 Export HTML</span>
<span class="md:hidden">📄</span>
2025-11-12 10:16:12 +00:00
</button>
2025-11-05 16:48:41 +00:00
</div>
</div>
<!-- Editor/Preview -->
<div class="flex-1 flex relative" style="min-height: 0;">
2025-11-05 16:48:41 +00:00
<!-- Editor -->
<div
x-show="viewMode === 'edit' || viewMode === 'split'"
class="flex flex-col"
style="min-height: 0;"
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
2025-11-05 16:48:41 +00:00
>
<textarea
x-model="noteContent"
@input="autoSave()"
@drop="onEditorDrop($event)"
@dragover.prevent="onEditorDragOver($event)"
@dragenter="onEditorDragEnter($event)"
@dragleave="onEditorDragLeave($event)"
:class="'flex-1 w-full p-6 resize-none focus:outline-none editor-textarea' + (draggedNoteForLink ? ' link-drop-target' : '')"
:style="'background-color: var(--bg-primary); color: var(--text-primary);' + (draggedNoteForLink ? ' outline: 2px dashed var(--accent-primary); outline-offset: -2px; box-shadow: 0 0 20px var(--accent-light);' : '')"
:placeholder="draggedNoteForLink ? '💡 Drop here to create link at cursor position...' : 'Start writing in markdown...'"
></textarea>
</div>
<!-- Split view resize handle -->
<div
x-show="viewMode === 'split'"
@mousedown="startSplitResize($event)"
class="split-resize-handle"
2025-11-11 14:50:33 +00:00
style="width: 6px; cursor: col-resize; background: linear-gradient(90deg, transparent 0%, var(--border-secondary) 50%, transparent 100%); transition: all 0.2s; position: relative; z-index: 10; opacity: 0.5;"
@mouseover="$el.style.opacity='1'; $el.style.background='var(--accent-primary)'"
@mouseout="if(!isResizingSplit) { $el.style.opacity='0.5'; $el.style.background='linear-gradient(90deg, transparent 0%, var(--border-secondary) 50%, transparent 100%)' }"
></div>
2025-11-05 16:48:41 +00:00
<!-- Preview -->
<div
x-show="viewMode === 'preview' || viewMode === 'split'"
class="overflow-y-auto overflow-x-hidden custom-scrollbar"
style="background-color: var(--bg-primary); min-height: 0;"
:style="viewMode === 'split' ? `width: ${100 - editorWidth}%;` : 'width: 100%;'"
2025-11-05 16:48:41 +00:00
>
<div
class="p-6 markdown-preview"
x-html="renderedMarkdown"
@click="handleInternalLink($event)"
></div>
</div>
</div>
<!-- Stats Status Bar (only shows if plugin enabled) -->
<div x-show="statsPluginEnabled && currentNote && noteStats"
class="border-t"
style="background-color: var(--bg-secondary); border-color: var(--border-primary);">
<!-- Collapsed State (default) -->
<div x-show="!statsExpanded"
@click="statsExpanded = true"
class="px-4 py-2 flex items-center justify-between cursor-pointer hover:bg-opacity-80 transition-colors"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'">
<div class="flex items-center space-x-4 text-sm" style="color: var(--text-secondary);">
<span class="font-medium">📊</span>
<span x-text="noteStats ? noteStats.words.toLocaleString() + ' words' : ''"></span>
<span>·</span>
<span x-text="noteStats ? '~' + noteStats.reading_time_minutes + 'm read' : ''"></span>
<template x-if="noteStats && noteStats.links > 0">
<span>
· <span x-text="noteStats.links + ' links'"></span>
</span>
</template>
<template x-if="noteStats && noteStats.tasks.total > 0">
<span>
· <span x-text="noteStats.tasks.completed + '/' + noteStats.tasks.total + ' tasks'"></span>
</span>
</template>
</div>
<span class="text-xs" style="color: var(--text-tertiary);">▼ Click for details</span>
</div>
<!-- Expanded State -->
<div x-show="statsExpanded"
class="px-4 py-3"
style="background-color: var(--bg-tertiary);">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-sm" style="color: var(--text-primary);">📊 Note Statistics</h3>
<button
@click="statsExpanded = false"
class="text-xs px-2 py-1 rounded hover:bg-opacity-80"
style="color: var(--text-secondary); background-color: var(--bg-hover);">
▲ Collapse
</button>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<!-- Basic Stats -->
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Words</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.words.toLocaleString() : '0'"></div>
</div>
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Characters</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.characters.toLocaleString() : '0'"></div>
</div>
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Reading Time</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? '~' + noteStats.reading_time_minutes + ' min' : '0 min'"></div>
</div>
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Lines</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.lines.toLocaleString() : '0'"></div>
</div>
<!-- Links -->
<template x-if="noteStats && noteStats.links > 0">
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Links</div>
<div class="font-semibold" style="color: var(--text-primary);">
<span x-text="noteStats.links"></span>
<span class="text-xs font-normal" style="color: var(--text-tertiary);">
(<span x-text="noteStats.internal_links"></span> internal)
</span>
</div>
</div>
</template>
<!-- Headings -->
<template x-if="noteStats && noteStats.headings.total > 0">
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Headings</div>
<div class="font-semibold" style="color: var(--text-primary);">
<span x-text="noteStats.headings.total"></span>
<span class="text-xs font-normal" style="color: var(--text-tertiary);">
(H1: <span x-text="noteStats.headings.h1"></span>, H2: <span x-text="noteStats.headings.h2"></span>)
</span>
</div>
</div>
</template>
<!-- Code Blocks -->
<template x-if="noteStats && noteStats.code_blocks > 0">
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Code Blocks</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats.code_blocks"></div>
</div>
</template>
<!-- Tasks -->
<template x-if="noteStats && noteStats.tasks.total > 0">
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Tasks</div>
<div class="font-semibold" style="color: var(--text-primary);">
<span x-text="noteStats.tasks.completed + '/' + noteStats.tasks.total"></span>
<span class="text-xs font-normal" style="color: var(--text-tertiary);">
(<span x-text="noteStats.tasks.completion_rate"></span>%)
</span>
</div>
</div>
</template>
<!-- Images -->
<template x-if="noteStats && noteStats.images > 0">
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Images</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats.images"></div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>