NoteDiscovery/frontend/index.html

4022 lines
237 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">
<!-- PWA Manifest -->
<link rel="manifest" href="/static/manifest.json">
<!-- PWA Meta Tags -->
<meta name="application-name" content="NoteDiscovery">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="NoteDiscovery">
<meta name="mobile-web-app-capable" content="yes">
<!-- Theme color: initial value, updated dynamically by app.js when theme changes -->
<meta name="theme-color" content="#667eea">
<meta name="msapplication-TileColor" content="#667eea">
<meta name="msapplication-tap-highlight" content="no">
<!-- Tailwind CSS from CDN (v3.4.17) -->
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<!-- Preload translations before Alpine initializes -->
<script>
(function() {
// Load translations synchronously to ensure they're available before Alpine renders
const locale = localStorage.getItem('locale') || 'en-US';
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/locales/' + locale, false); // Synchronous request
try {
xhr.send();
if (xhr.status === 200) {
window.__preloadedTranslations = JSON.parse(xhr.responseText);
} else {
// Fallback to en-US if preferred locale fails
if (locale !== 'en-US') {
xhr.open('GET', '/api/locales/en-US', false);
xhr.send();
if (xhr.status === 200) {
window.__preloadedTranslations = JSON.parse(xhr.responseText);
localStorage.setItem('locale', 'en-US');
}
}
}
} catch (e) {
console.error('Failed to preload translations:', e);
window.__preloadedTranslations = {};
}
})();
</script>
<!-- Alpine.js (v3.14.1) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<!-- Marked.js for Markdown parsing (v12.0.2) -->
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<!-- DOMPurify for HTML sanitization (XSS prevention) -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<!-- 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>
<!-- Highlight.js for code syntax highlighting (v11.9.0) -->
<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 -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.esm.min.mjs';
// Note: Mermaid is initialized dynamically by app.js renderMermaid() based on current theme
window.mermaid = mermaid;
</script>
<!-- vis-network for graph visualization (v9.1.9) -->
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
<!-- QR Code generator for share links -->
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
<!-- 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);
}
/* Truncate utility for long text */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.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;
}
/* Readable line length - constrain text width for better readability */
.markdown-preview.readable-width {
max-width: 750px;
margin-left: auto;
margin-right: auto;
}
/* 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);
}
/* Wikilinks - Obsidian style internal links */
.markdown-preview a[data-wikilink] {
color: var(--link-internal, var(--accent-primary));
text-decoration: none;
font-weight: 500;
background-color: var(--link-internal-bg, transparent);
padding: 0 2px;
border-radius: 2px;
transition: background-color 0.15s, color 0.15s;
}
.markdown-preview a[data-wikilink]:hover {
background-color: var(--link-internal-hover-bg, var(--bg-tertiary));
text-decoration: underline;
}
/* Broken wikilinks - note doesn't exist (Obsidian shows these dimmed) */
.markdown-preview a[data-wikilink].wikilink-broken {
color: var(--link-broken, var(--text-tertiary));
opacity: 0.7;
}
.markdown-preview a[data-wikilink].wikilink-broken:hover {
opacity: 1;
}
/* Search highlight base style (used in note preview and search results) */
.search-highlight {
padding: 2px 4px;
border-radius: 3px;
background-color: rgba(255, 193, 7, 0.4);
color: var(--text-primary);
transition: all 0.2s;
}
.search-highlight.active-match {
background-color: var(--accent-primary);
color: white;
}
/* 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;
}
/* Graph View */
#graph-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#graph-overlay .vis-network {
outline: none;
width: 100% !important;
height: 100% !important;
}
.graph-legend {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 12px;
border-radius: 8px;
font-size: 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
z-index: 10;
}
.graph-legend-item {
display: flex;
align-items: center;
gap: 6px;
margin: 4px 0;
}
.graph-legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
/* 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);
}
/* 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[align="left"], .markdown-preview td[align="left"] { text-align: left; }
.markdown-preview th[align="right"], .markdown-preview td[align="right"] { text-align: right; }
.markdown-preview th[align="center"], .markdown-preview td[align="center"] { text-align: center; }
.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;
}
/* Embedded Media (audio, video, PDF) */
.markdown-preview .media-embed {
margin: 1.5rem 0;
border-radius: 0.5rem;
overflow: hidden;
}
/* Audio embeds */
.markdown-preview .media-audio {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
border: 1px solid var(--border-primary);
}
.markdown-preview .media-audio audio {
width: 100%;
max-width: 500px;
border-radius: 2rem;
}
.markdown-preview .media-audio .media-caption {
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Video embeds */
.markdown-preview .media-video {
background: #000;
display: flex;
justify-content: center;
}
.markdown-preview .media-video video {
width: 100%;
max-width: 800px;
max-height: 450px;
border-radius: 0;
}
/* PDF embeds (iframe) */
.markdown-preview .media-pdf {
border: 1px solid var(--border-primary);
border-radius: 0.5rem;
overflow: hidden;
background: var(--bg-secondary);
}
.markdown-preview .media-pdf iframe {
width: 100%;
height: 600px;
border: none;
display: block;
}
/* PDF links (for external PDFs) */
.markdown-preview .pdf-link {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 1.25rem;
margin: 1rem 0;
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
border: 1px solid var(--border-primary);
border-radius: 0.5rem;
color: var(--text-primary);
text-decoration: none;
transition: all 0.15s ease;
}
.markdown-preview .pdf-link:hover {
background: var(--bg-secondary);
border-color: var(--accent-primary);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.markdown-preview .pdf-link:hover .pdf-link-content {
color: var(--accent-primary);
}
.markdown-preview .pdf-link-content {
font-weight: 600;
color: var(--text-primary);
}
.markdown-preview .pdf-link-note {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* Task lists */
.markdown-preview input[type="checkbox"] {
margin-right: 0.5rem;
}
/* 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;
}
/* Mermaid diagram styling - minimal, let Mermaid handle its own rendering */
.markdown-preview .mermaid-rendered svg {
max-width: 100%;
height: auto;
}
/* Editor styling */
/* Shared editor styles - MUST be identical for textarea and overlay */
.editor-textarea,
.syntax-overlay {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
tab-size: 2;
letter-spacing: normal;
word-spacing: normal;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
padding: 1.5rem;
margin: 0;
border: none;
box-sizing: border-box;
}
.editor-textarea {
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
resize: none;
overflow-y: scroll; /* Always show scrollbar for consistent text width */
overflow-x: auto;
}
/* Syntax Highlighting Overlay */
.editor-wrapper {
position: relative;
flex: 1;
display: flex;
min-height: 0;
}
.editor-wrapper .editor-textarea {
position: relative;
z-index: 1;
flex: 1;
width: 100%;
}
.editor-wrapper.syntax-highlight .editor-textarea {
color: transparent !important;
caret-color: var(--text-primary);
background: transparent !important;
}
.syntax-overlay {
position: absolute;
inset: 0;
pointer-events: none;
overflow-y: scroll; /* Match textarea scrollbar space */
overflow-x: auto; /* Match textarea for horizontal scroll sync */
color: var(--text-primary);
background-color: var(--bg-primary);
z-index: 0;
}
/* Invisible scrollbar that takes EXACT same space as textarea */
.syntax-overlay::-webkit-scrollbar {
width: 12px;
height: 12px;
background: transparent;
}
.syntax-overlay::-webkit-scrollbar-track {
background: var(--bg-tertiary); /* MUST match textarea track */
}
.syntax-overlay::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 6px;
border: 2px solid transparent; /* MUST match textarea's 2px border */
}
/* Firefox - MUST match textarea's scrollbar-width: thin */
.syntax-overlay {
scrollbar-width: thin;
scrollbar-color: transparent var(--bg-tertiary);
}
/* Zen Mode Styles */
.zen-mode-active {
background-color: var(--bg-primary);
}
.zen-mode-active .zen-hide {
display: none !important;
}
.zen-mode-active .zen-editor-container {
width: 100%;
margin: 0;
padding: 2rem 4rem;
height: 100vh;
}
.zen-mode-active .zen-editor-container .editor-wrapper {
background-color: var(--bg-primary);
}
.zen-mode-active .zen-editor-container .editor-textarea,
.zen-mode-active .zen-editor-container .syntax-overlay {
font-size: 18px;
line-height: 1.8;
}
.zen-exit-button {
position: fixed;
bottom: 2rem;
right: 2rem;
padding: 0.75rem 1.25rem;
border-radius: 9999px;
background-color: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border-primary);
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease, transform 0.2s ease;
z-index: 9999;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.zen-exit-button:hover {
opacity: 1 !important;
background-color: var(--bg-secondary);
transform: scale(1.05);
}
.zen-mode-active:hover .zen-exit-button {
opacity: 0.6;
}
/* Zen mode fade transition */
.zen-fade-enter {
animation: zenFadeIn 0.3s ease-out;
}
@keyframes zenFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Syntax highlight colors (color-only — see issue #212) */
.syntax-overlay .md-heading { color: var(--accent-primary); }
.syntax-overlay .md-bold { color: var(--syntax-bold, #fbbf24); }
.syntax-overlay .md-italic { color: var(--syntax-italic, #a78bfa); }
.syntax-overlay .md-code { color: var(--accent-secondary, var(--accent-primary)); background: var(--bg-tertiary); border-radius: 2px; }
.syntax-overlay .md-link { color: var(--link-color, var(--accent-primary)); }
.syntax-overlay .md-link-url { color: var(--text-tertiary); }
.syntax-overlay .md-list { color: var(--accent-primary); }
.syntax-overlay .md-blockquote{ color: var(--text-tertiary); }
.syntax-overlay .md-hr { color: var(--border-primary); }
.syntax-overlay .md-wikilink { color: var(--link-internal, var(--accent-primary)); }
.syntax-overlay .md-frontmatter { color: var(--text-tertiary); }
.syntax-overlay .md-codeblock { color: var(--text-secondary); background: var(--bg-tertiary); }
/* Link drop zone cursor */
.editor-textarea.link-drop-target {
cursor: copy !important;
}
/* Folder tree styling - compact Obsidian-like */
.folder-item {
user-select: none;
border-radius: 6px;
border-left: 3px solid transparent;
margin-left: 2px;
}
.folder-item:hover {
background-color: var(--bg-hover);
border-left-color: var(--accent-primary);
}
.folder-item.drag-over {
background-color: var(--accent-light) !important;
border: 2px dashed var(--accent-primary) !important;
}
.folder-expanded {
font-weight: 600;
}
.note-item {
border-radius: 6px;
border-left: 3px solid transparent;
margin-left: 2px;
}
.note-item:hover {
background-color: var(--bg-hover);
border-left-color: var(--accent-primary);
transform: translateX(2px);
}
/* Active/selected note styling */
.note-item.active,
.note-item[style*="accent-light"] {
border-left-color: var(--accent-primary) !important;
font-weight: 500;
}
/* Shared hover accent effect for list items */
.hover-accent {
border-radius: 6px;
border-left: 3px solid transparent;
margin-left: 2px;
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.hover-accent:not(.active):hover {
background-color: var(--bg-hover);
border-left-color: var(--accent-primary);
transform: translateX(2px);
}
.hover-accent.active {
border-left-color: var(--accent-primary);
}
.hover-accent.active:hover {
transform: translateX(2px);
}
/* Tag chip - same effect but no margin */
.tag-chip {
border-left: 3px solid transparent;
transition: border-color 0.15s ease, transform 0.1s ease;
}
.tag-chip:hover {
border-left-color: var(--accent-primary);
transform: translateX(2px);
}
/* 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;
transform: scale(0.98);
}
/* Smooth transitions for drag states */
.folder-item,
.note-item {
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
/* Better drop zone visibility */
.folder-item:has(+ .folder-contents) {
position: relative;
}
.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: 2px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--text-tertiary);
opacity: 0.4;
}
/* Icon Rail (VS Code-style navigation) */
.icon-rail {
width: 44px;
min-width: 44px;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
background-color: var(--bg-tertiary);
border-right: 1px solid var(--border-primary);
}
.icon-rail-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin-bottom: 4px;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
color: var(--text-tertiary);
background: transparent;
border: none;
}
.icon-rail-btn:hover {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.icon-rail-btn.active {
background-color: var(--accent-light);
color: var(--accent-primary);
}
.icon-rail-btn svg {
width: 20px;
height: 20px;
}
/* Markdown Formatting Toolbar */
.format-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
color: var(--text-secondary);
background: transparent;
border: none;
}
.format-btn:hover {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.format-btn:active {
background-color: var(--accent-light);
color: var(--accent-primary);
}
.icon-rail-spacer {
flex: 1;
}
.icon-rail-logo {
width: 28px;
height: 28px;
margin-bottom: 8px;
cursor: pointer;
opacity: 0.9;
transition: opacity 0.15s ease;
}
.icon-rail-logo:hover {
opacity: 1;
}
/* Sidebar panel (content area next to icon rail) */
.sidebar-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
transition: flex-basis 180ms ease, width 180ms ease, opacity 120ms ease;
}
/* Collapsed state: panel shrinks to 0 width so the editor can claim the freed space.
The icon rail (which is outside .sidebar-panel) stays visible — only the inner
file/search/outline area disappears. flex: 0 0 0 overrides the default flex: 1 so
the panel actually shrinks instead of competing for space with the rail. */
.sidebar-panel.sidebar-panel-collapsed {
flex: 0 0 0;
width: 0;
opacity: 0;
pointer-events: none;
}
/* Smooth the outer sidebar wrap so the editor gains the space gradually rather than
snapping. Mobile (≤768px) uses position: fixed + left transition for its slide-in
drawer pattern, so this width transition is desktop-only by virtue of being shadowed
there by !important rules. */
.mobile-sidebar {
transition: width 180ms ease, min-width 180ms ease, max-width 180ms ease;
}
.sidebar-panel-header {
padding: 12px;
border-bottom: 1px solid var(--border-primary);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-tertiary);
}
.sidebar-panel-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
/* Mobile bottom tabs */
@media (max-width: 768px) {
.icon-rail {
display: none;
}
.mobile-bottom-tabs {
display: flex !important;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
z-index: 1001;
padding-bottom: env(safe-area-inset-bottom);
}
.mobile-bottom-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: var(--text-tertiary);
background: none;
border: none;
cursor: pointer;
transition: color 0.15s ease, background-color 0.15s ease;
min-width: 44px;
min-height: 44px;
}
.mobile-bottom-tab:active {
background-color: var(--bg-hover);
}
.mobile-bottom-tab.active {
color: var(--accent-primary);
}
.mobile-bottom-tab svg {
width: 22px;
height: 22px;
}
.mobile-bottom-tab span {
font-size: 10px;
}
/* Zen mode mobile adjustments - use 95% width instead of fixed padding */
.zen-mode-active .zen-editor-container {
padding: 1rem 2.5%;
width: 100%;
}
}
@media (min-width: 769px) {
.mobile-bottom-tabs {
display: none !important;
}
}
/* 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;
}
/* 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 */
}
/* 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) {
/* Hide sidebar by default on mobile */
.mobile-sidebar {
position: fixed;
left: -100%;
top: 0;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for iOS Safari */
width: 80% !important;
max-width: 300px !important;
z-index: 1000;
transition: left 0.3s ease;
}
/* Show sidebar when open */
.mobile-sidebar-open {
left: 0;
}
/* On mobile, the sidebar is an overlay drawer — the desktop "collapse panel"
feature must be disregarded. Otherwise a previously-persisted collapsed state
would carry over and the drawer would open empty (icon-rail buttons would do
nothing visible). The desktop toggle button itself is already hidden on mobile
(`hidden md:flex`), so the user has no way to undo it from here. */
.sidebar-panel.sidebar-panel-collapsed {
flex: 1;
width: auto;
opacity: 1;
pointer-events: auto;
}
/* Add bottom padding to scrollable sidebar content for tab bar */
.mobile-sidebar .overflow-y-auto {
padding-bottom: 70px !important;
padding-bottom: calc(70px + env(safe-area-inset-bottom)) !important;
}
/* Homepage cards view - add padding for mobile bottom tabs */
.flex-1.overflow-y-auto {
padding-bottom: 70px;
padding-bottom: calc(70px + env(safe-area-inset-bottom));
}
/* 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;
}
/* Touch-friendly but compact */
.note-item,
.folder-item {
padding: 0.5rem 0.5rem !important;
min-height: 36px;
}
}
/* Hide mobile menu button on desktop */
@media (min-width: 769px) {
.mobile-menu-button {
display: none !important;
}
}
/* Responsive homepage cards */
.homepage-card {
min-height: 70px;
}
@media (min-width: 640px) {
.homepage-card {
min-height: 140px;
}
}
</style>
</head>
<body x-data="noteApp()" x-init="init()" style="background-color: var(--bg-primary);" :class="{'zen-mode-active': zenMode}">
<!-- Zen Mode Exit Button -->
<button
x-show="zenMode"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-60 translate-y-0"
@click="toggleZenMode()"
class="zen-exit-button"
:title="t('zen_mode.exit_hint')"
>
<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="M6 18L18 6M6 6l12 12"></path>
</svg>
<span>Exit Zen</span>
</button>
<!-- Toast notifications (non-blocking; z-index above modals at 11001200; desktop-first top-right) -->
<div
class="fixed z-[1300] flex w-[min(28rem,calc(100vw-2rem))] flex-col items-stretch gap-2 pointer-events-none"
style="top: max(1rem, env(safe-area-inset-top, 0px)); right: max(1rem, env(safe-area-inset-right, 0px));"
role="region"
:aria-label="t('toast.region_label')"
aria-live="polite"
>
<template x-for="item in toasts" :key="item.id">
<div
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0 -translate-y-1"
class="pointer-events-auto flex items-start gap-2 rounded-lg border shadow-lg px-3 py-2.5 text-sm"
:role="item.type === 'error' ? 'alert' : 'status'"
:style="{
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
borderColor: 'var(--border-primary)',
borderLeftWidth: '4px',
borderLeftStyle: 'solid',
borderLeftColor: item.type === 'error' ? 'var(--error)' : (item.type === 'warning' ? 'var(--warning)' : (item.type === 'success' ? 'var(--success)' : 'var(--accent-primary)'))
}"
>
<span class="flex-1 min-w-0 break-words leading-snug" x-text="item.message"></span>
<button
type="button"
class="flex-shrink-0 rounded p-0.5 opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-opacity-50"
style="color: var(--text-secondary); --tw-ring-color: var(--accent-primary);"
@click="dismissToast(item.id)"
:aria-label="t('toast.dismiss')"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</template>
</div>
<!-- Mobile Overlay -->
<div
@click="mobileSidebarOpen = false"
:class="{'mobile-overlay': true, 'mobile-overlay-visible': mobileSidebarOpen}"
x-show="mobileSidebarOpen"
></div>
<!-- Main Container -->
<div class="flex h-screen overflow-hidden">
<!-- Sidebar with Icon Rail -->
<div
class="flex mobile-sidebar zen-hide"
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
:style="sidebarPanelCollapsed
? 'width: auto; background-color: var(--bg-secondary); min-width: 0; max-width: none;'
: 'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
>
<!-- Icon Rail -->
<div class="icon-rail">
<!-- Logo -->
<img
src="/static/logo.svg"
alt="NoteDiscovery"
class="icon-rail-logo"
@click="goHome()"
:title="t('sidebar.go_to_homepage')"
>
<!-- Files -->
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'files'}"
@click="openSidebarPanel('files')"
:title="t('sidebar.files')"
:aria-label="t('sidebar.files')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
</button>
<!-- Search -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'search'}"
@click="openSidebarPanel('search')"
:title="t('sidebar.search')"
:aria-label="t('sidebar.search')"
>
<svg 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>
<!-- Search results badge -->
<span
x-show="searchQuery.trim() && searchResults.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<!-- Tags -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'tags'}"
@click="openSidebarPanel('tags')"
:title="t('tags.title')"
:aria-label="t('tags.title')"
>
<svg fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>
<!-- Filtered notes badge (shows result count when tags selected) -->
<span
x-show="selectedTags.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<!-- Outline (TOC) -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'outline'}"
@click="openSidebarPanel('outline')"
:title="t('outline.title')"
:aria-label="t('outline.title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
<!-- Heading count badge -->
<span
x-show="outline.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="outline.length > 9 ? '9+' : outline.length"
></span>
</button>
<!-- Backlinks -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'backlinks'}"
@click="openSidebarPanel('backlinks')"
:title="t('backlinks.title')"
:aria-label="t('backlinks.title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- Chain link (centered) -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.5 11a2 2 0 00-2.8 0l-2 2a2 2 0 102.8 2.8l.5-.5m-.35-2.5a2 2 0 012.8 0l2-2a2 2 0 00-2.8-2.8l-.5.5"></path>
<!-- Circular refresh arrows -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 3v5h5"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21v-5h-5"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 10A9 9 0 006 5.3L3 8"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 14a9 9 0 0015 4.7l3-2.7"></path>
</svg>
<!-- Backlinks count badge -->
<span
x-show="backlinks.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="backlinks.length > 9 ? '9+' : backlinks.length"
></span>
</button>
<!-- Shared notes (public links) -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'shared'}"
@click="openSidebarPanel('shared')"
:title="t('share.panel_title')"
:aria-label="t('share.panel_title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
<span
x-show="_sharedNotePathsList.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="_sharedNotePathsList.length > 9 ? '9+' : _sharedNotePathsList.length"
></span>
</button>
<div class="icon-rail-spacer"></div>
<!-- Toggle sidebar panel (hides files/search/outline/etc.; icon rail stays).
Hidden on small screens where the sidebar is an overlay drawer instead. -->
<button
class="icon-rail-btn hidden md:flex"
@click="toggleSidebarPanel()"
:title="t(sidebarPanelCollapsed ? 'sidebar.show_panel' : 'sidebar.hide_panel')"
:aria-label="t(sidebarPanelCollapsed ? 'sidebar.show_panel' : 'sidebar.hide_panel')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" stroke-width="2"></rect>
<line x1="9" y1="4" x2="9" y2="20" stroke-width="2"></line>
<!-- Chevron flips: points LEFT when expanded ("click to collapse"),
RIGHT when collapsed ("click to expand"). -->
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
:d="sidebarPanelCollapsed ? 'M13 9 L16 12 L13 15' : 'M16 9 L13 12 L16 15'"
></path>
</svg>
</button>
<!-- Graph -->
<button
class="icon-rail-btn"
:class="{'active': showGraph}"
@click="showGraph = true; initGraph()"
:title="t('graph.title')"
:aria-label="t('graph.title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
</button>
<!-- Settings -->
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'settings'}"
@click="openSidebarPanel('settings')"
:title="t('sidebar.settings')"
:aria-label="t('sidebar.settings')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</button>
</div>
<!-- Sidebar Panel -->
<div
class="sidebar-panel"
:class="{'sidebar-panel-collapsed': sidebarPanelCollapsed}"
style="border-right: 1px solid var(--border-primary);"
>
<!-- ==================== FILES PANEL ==================== -->
<div x-show="activePanel === 'files'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Header with + New -->
<div class="flex-shrink-0 px-2 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.files')"></span>
<button
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
class="px-2 py-1 text-xs font-medium text-white rounded focus:outline-none flex items-center gap-1"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
<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="M12 4v16m8-8H4"></path>
</svg>
New
</button>
</div>
<!-- Folder Tree -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
<!-- Favorites Section -->
<div x-show="favoriteNotes.length > 0" class="mb-3">
<div
@click="favoritesExpanded = !favoritesExpanded"
class="flex items-center justify-between px-2 py-1 cursor-pointer rounded hover:bg-opacity-50 transition-colors"
style="color: var(--text-tertiary);"
@mouseenter="$el.style.backgroundColor = 'var(--bg-hover)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<div class="flex items-center gap-1.5">
<span class="text-xs font-semibold uppercase tracking-wide" x-text="t('sidebar.favorites_title')"></span>
</div>
<svg
class="w-3 h-3 transition-transform"
:class="{'rotate-90': favoritesExpanded}"
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>
</div>
<div x-show="favoritesExpanded" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 -translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="mt-1">
<template x-for="fav in favoriteNotes" :key="fav.path">
<div
@click="loadNote(fav.path)"
class="hover-accent flex items-center justify-between px-2 py-1.5 cursor-pointer group"
:class="{'active': currentNote === fav.path}"
:style="currentNote === fav.path ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
:title="fav.path"
>
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<svg class="w-3.5 h-3.5 flex-shrink-0" style="color: var(--warning, #eab308);" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
</svg>
<span class="text-xs truncate" x-text="fav.name" :title="fav.name"></span>
</div>
<button
@click.stop="toggleFavorite(fav.path)"
class="opacity-0 group-hover:opacity-100 p-0.5 rounded transition-opacity"
:title="t('toolbar.remove_favorite')"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<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>
</template>
</div>
</div>
<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 x-text="t('sidebar.folders_and_notes')"></span>
<div class="flex gap-0.5">
<button
@click="cycleSortMode()"
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.sort_' + sortMode)"
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
><span x-text="getSortIcon()"></span></button>
<button
@click="expandAllFolders()"
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.expand_all')"
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
>📂</button>
<button
@click="collapseAllFolders()"
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.collapse_all')"
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
>📁</button>
</div>
</div>
<!-- Dual-purpose: hint text OR root drop zone when dragging -->
<div
class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center"
:class="{
'border-2 border-dashed bg-accent-light': draggedItem && dragOverFolder === '',
'border-2 border-dashed': draggedItem && dragOverFolder !== '',
'border-2 border-transparent': !draggedItem
}"
:style="{
'color': draggedItem ? (dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--text-secondary)') : 'var(--text-tertiary)',
'opacity': draggedItem ? '1' : '0.8',
'border-color': draggedItem && dragOverFolder === '' ? 'var(--accent-primary)' : 'var(--border-secondary)',
'background-color': dragOverFolder === '' ? 'var(--accent-light)' : ''
}"
@dragover.prevent="if(draggedItem) dragOverFolder = '';"
@dragenter.prevent="if(draggedItem) dragOverFolder = '';"
@dragleave="if(draggedItem) dragOverFolder = null;"
@drop="if(draggedItem) onFolderDrop('')"
style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;"
>
<span x-show="!draggedItem" x-text="t('sidebar.drag_hint')"></span>
<span x-show="draggedItem" x-text="t('sidebar.root_folder')"></span>
</div>
</div>
<!-- Folders (rendered recursively for unlimited nesting) -->
<template x-for="[folderKey, folder] in Object.entries(folderTree).filter(([k, v]) => k !== '__root__' && (!hideUnderscoreFolders || !v.name.startsWith('_'))).sort((a, b) => getFolderSortComparator()(a[1], b[1]))" :key="folderKey">
<div x-html="renderFolderRecursive(folder, 0, true)"></div>
</template>
<!-- Root notes and images (no folder) -->
<div x-html="renderRootItems()"></div>
<!-- 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);">
<span x-text="t('sidebar.no_notes_yet')"></span><br>
<span x-text="t('sidebar.create_first')"></span>
</div>
</template>
</div>
</div>
<!-- END FILES PANEL -->
<!-- ==================== SEARCH PANEL ==================== -->
<div x-show="activePanel === 'search'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Search Header -->
<div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.search_title')"></span>
</div>
<!-- Search Input -->
<div class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
<div class="relative">
<input
type="text"
x-model="searchQuery"
@input="debouncedSearchNotes()"
:placeholder="t('sidebar.search_placeholder')"
class="w-full px-2 py-1.5 pl-7 pr-7 text-xs rounded focus:outline-none focus:ring-1"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
>
<svg class="absolute left-2 top-2 w-3.5 h-3.5" 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>
<button
x-show="searchQuery.length > 0"
@click="searchQuery = ''; searchResults = []; currentSearchHighlight = ''; clearSearchHighlights();"
class="absolute right-1.5 top-1.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="t('sidebar.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 -->
<div
x-show="totalMatches > 0"
class="flex items-center justify-between mt-1.5 px-2 py-0.5 rounded text-xs"
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
>
<span x-text="t('search.match_of', { current: currentMatchIndex + 1, total: totalMatches })"></span>
<div class="flex gap-1">
<button @click="previousMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" :title="t('search.previous')">
<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" style="background-color: var(--bg-primary); color: var(--text-primary);" :title="t('search.next')">
<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>
<!-- Search Results -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<template x-if="searchQuery.trim() && searchResults.length > 0">
<div class="p-2">
<div class="text-xs px-2 py-1 mb-1" style="color: var(--text-tertiary);">
<span x-text="searchResults.length + ' ' + t(searchResults.length === 1 ? 'search.result_singular' : 'search.result_plural')"></span>
</div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type, searchQuery)"
class="hover-accent px-2 py-1.5 text-sm cursor-pointer mb-1"
:class="{'active': currentNote === note.path}"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
>
<div class="font-medium truncate" x-text="note.name" :title="note.name"></div>
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-html="(note.matches && note.matches.length > 0) ? note.matches[0].context : (note.folder || 'Root')" :title="note.folder || 'Root'"></div>
</div>
</template>
</div>
</template>
<template x-if="searchQuery.trim() && isSearching">
<div class="p-6 text-center">
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('search.searching', {query: searchQuery})"></p>
</div>
</template>
<template x-if="searchQuery.trim() && !isSearching && searchResults.length === 0">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" 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.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('search.no_results', {query: searchQuery})"></p>
</div>
</template>
<template x-if="!searchQuery.trim()">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" 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>
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('sidebar.search_hint')"></p>
</div>
</template>
</div>
</div>
<!-- END SEARCH PANEL -->
<!-- ==================== TAGS PANEL ==================== -->
<div x-show="activePanel === 'tags'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Tags Header -->
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('tags.title')"></span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="Object.keys(allTags).length"></span>
</div>
<!-- Active Filters -->
<div x-show="selectedTags.length > 0" class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
<button
@click="clearTagFilters()"
class="w-full px-2 py-1.5 text-xs rounded flex items-center justify-center gap-1"
style="background-color: var(--accent-primary); color: white;"
:title="t('tags.clear_all')"
>
<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>
Clear Filters (<span x-text="selectedTags.length"></span>)
</button>
</div>
<!-- Scrollable content: Tags + Filtered Results -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- Tag Chips -->
<div class="p-3">
<template x-if="Object.keys(allTags).length > 0">
<div class="flex flex-wrap gap-1.5">
<template x-for="[tag, count] in sortedTags" :key="tag">
<button
@click="toggleTag(tag)"
class="tag-chip px-2 py-1 text-xs rounded-full"
:style="selectedTags.includes(tag)
? 'background-color: var(--accent-primary); color: white; font-weight: 600;'
: 'background-color: var(--bg-tertiary); color: var(--text-primary);'"
:title="t('tags.filter_by', {tag: tag, count: count})"
>
<span x-text="tag"></span>
<span class="opacity-75" x-text="` (${count})`"></span>
</button>
</template>
</div>
</template>
<template x-if="Object.keys(allTags).length === 0">
<div class="text-center py-6">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>
<p class="text-sm mb-2" style="color: var(--text-tertiary);" x-text="t('tags.no_tags')"></p>
<p class="text-xs" style="color: var(--text-tertiary);" x-text="t('tags.hint')"></p>
</div>
</template>
</div>
<!-- Filtered Notes (shown immediately after tags when selected) -->
<template x-if="selectedTags.length > 0">
<div class="border-t" style="border-color: var(--border-primary);">
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">
<span x-text="t('sidebar.filtered_notes')"></span> (<span x-text="searchResults.length"></span>)
</div>
<div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type)"
class="hover-accent px-3 py-1.5 text-sm cursor-pointer"
:class="{'active': currentNote === note.path}"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
>
<span class="truncate block" x-text="note.name" :title="note.name"></span>
</div>
</template>
<div x-show="searchResults.length === 0" class="px-3 py-4 text-center text-xs" style="color: var(--text-tertiary);" x-text="t('tags.no_matches')">
</div>
</div>
</div>
</template>
</div>
</div>
<!-- END TAGS PANEL -->
<!-- ==================== OUTLINE PANEL ==================== -->
<div x-show="activePanel === 'outline'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Outline Header -->
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('outline.title')"></span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="outline.length"></span>
</div>
<!-- Scrollable content: Outline items -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- Outline items -->
<div class="py-2">
<template x-if="outline.length > 0">
<div>
<template x-for="(heading, index) in outline" :key="index">
<button
@click="scrollToHeading(heading)"
class="hover-accent w-full text-left px-3 py-1.5 text-sm"
:style="`padding-left: ${(heading.level - 1) * 12 + 12}px; color: var(--text-primary);`"
>
<span
class="block truncate"
:style="heading.level === 1 ? 'font-weight: 600;' : (heading.level === 2 ? 'font-weight: 500;' : '')"
x-text="heading.text"
:title="heading.text"
></span>
</button>
</template>
</div>
</template>
<template x-if="outline.length === 0">
<div class="text-center py-6 px-3">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" 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="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
<p class="text-sm mb-2" style="color: var(--text-tertiary);" x-text="t('outline.no_headings')"></p>
<p class="text-xs" style="color: var(--text-tertiary);" x-text="t('outline.hint')"></p>
</div>
</template>
</div>
</div>
</div>
<!-- END OUTLINE PANEL -->
<!-- ==================== BACKLINKS PANEL ==================== -->
<div x-show="activePanel === 'backlinks'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Backlinks Header -->
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('backlinks.title')"></span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="backlinks.length"></span>
</div>
<!-- Scrollable content: Backlinks items -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<div class="py-2">
<!-- When note is open and has backlinks -->
<template x-if="currentNote && backlinks.length > 0">
<div>
<template x-for="(bl, index) in backlinks" :key="index">
<button
@click="navigateToBacklink(bl.path)"
class="hover-accent w-full text-left px-3 py-2 text-sm border-b"
style="color: var(--text-primary); border-color: var(--border-primary);"
>
<!-- Note name -->
<div class="flex items-center gap-2 mb-1">
<svg class="w-4 h-4 flex-shrink-0" style="color: var(--accent-primary);" 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>
<span class="font-medium truncate" x-text="bl.name"></span>
</div>
<!-- References/Context snippets -->
<template x-for="(ref, refIdx) in bl.references.slice(0, 2)" :key="refIdx">
<div class="ml-6 text-xs truncate" style="color: var(--text-tertiary);">
<span class="opacity-60" x-text="'L' + ref.line_number + ':'"></span>
<span x-text="ref.context"></span>
</div>
</template>
<template x-if="bl.references.length > 2">
<div class="ml-6 text-xs" style="color: var(--text-tertiary);">
<span x-text="'+' + (bl.references.length - 2) + ' ' + t('backlinks.more_refs')"></span>
</div>
</template>
</button>
</template>
</div>
</template>
<!-- When note is open but no backlinks -->
<template x-if="currentNote && backlinks.length === 0">
<div class="text-center py-6 px-3">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" 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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
<p class="text-sm mb-2" style="color: var(--text-tertiary);" x-text="t('backlinks.no_backlinks')"></p>
<p class="text-xs" style="color: var(--text-tertiary);" x-text="t('backlinks.hint')"></p>
</div>
</template>
<!-- When no note is open -->
<template x-if="!currentNote">
<div class="text-center py-6 px-3">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" 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>
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('backlinks.select_note')"></p>
</div>
</template>
</div>
</div>
</div>
<!-- END BACKLINKS PANEL -->
<!-- ==================== SHARED NOTES PANEL ==================== -->
<div x-show="activePanel === 'shared'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('share.panel_title')"></span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="_sharedNotePathsList.length"></span>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<div class="py-2">
<template x-if="_sharedNotePathsList.length > 0">
<div class="px-2">
<template x-for="item in getSharedPanelItems()" :key="item.path">
<div
@click="openItem(item.path, 'note')"
class="hover-accent px-2 py-1.5 text-sm cursor-pointer mb-1 rounded"
:style="currentNote === item.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
>
<div class="flex items-start gap-2 mb-0.5">
<svg class="w-4 h-4 flex-shrink-0 mt-0.5" style="color: var(--accent-primary); opacity: 0.9;" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
<div class="min-w-0 flex-1">
<div class="font-medium truncate" x-text="item.name" :title="item.name"></div>
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-text="item.folder" :title="item.folder"></div>
</div>
</div>
</div>
</template>
</div>
</template>
<template x-if="_sharedNotePathsList.length === 0">
<div class="text-center py-6 px-3">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('share.no_shared')"></p>
</div>
</template>
</div>
</div>
</div>
<!-- END SHARED NOTES PANEL -->
<!-- ==================== SETTINGS PANEL ==================== -->
<template x-if="activePanel === 'settings'">
<div class="flex flex-col h-full">
<!-- Settings Header -->
<div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);" x-text="t('sidebar.settings_title')"></span>
</div>
<!-- Scrollable content -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<div class="p-3">
<!-- Theme -->
<div class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('theme.title')">Theme</label>
<select
x-model="currentTheme"
@change="setTheme(currentTheme)"
class="w-full px-2 py-2 text-sm rounded focus:outline-none focus:ring-2 focus:ring-opacity-50"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;"
>
<template x-for="theme in availableThemes" :key="theme.id">
<option :value="theme.id" :selected="theme.id === currentTheme" x-text="theme.name"></option>
</template>
</select>
</div>
<!-- Language -->
<div class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('language.title')">Language</label>
<select
x-model="currentLocale"
@change="changeLocale(currentLocale)"
class="w-full px-2 py-2 text-sm rounded focus:outline-none focus:ring-2 focus:ring-opacity-50"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;"
>
<template x-for="loc in availableLocales" :key="loc.code">
<option :value="loc.code" :selected="loc.code === currentLocale" x-text="loc.flag + ' ' + loc.name"></option>
</template>
</select>
</div>
<!-- Syntax Highlighting Toggle -->
<div class="mb-4">
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);" x-text="t('syntax_highlight.title')"></span>
<div
@click="toggleSyntaxHighlight()"
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
:style="syntaxHighlightEnabled ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
>
<div
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
:style="'background-color: var(--bg-primary);' + (syntaxHighlightEnabled ? ' transform: translateX(20px);' : '')"
></div>
</div>
</label>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('syntax_highlight.description')"></p>
</div>
<!-- Readable Line Length Toggle -->
<div class="mb-4">
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);" x-text="t('settings.readable_line_length')"></span>
<div
@click="toggleReadableLineLength()"
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
:style="readableLineLength ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
>
<div
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
:style="'background-color: var(--bg-primary);' + (readableLineLength ? ' transform: translateX(20px);' : '')"
></div>
</div>
</label>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.readable_line_length_desc')"></p>
</div>
<!-- Hide Underscore Folders Toggle -->
<div class="mb-4">
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);" x-text="t('settings.hide_underscore_folders')"></span>
<div
@click="toggleHideUnderscoreFolders()"
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
:style="hideUnderscoreFolders ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
>
<div
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
:style="'background-color: var(--bg-primary);' + (hideUnderscoreFolders ? ' transform: translateX(20px);' : '')"
></div>
</div>
</label>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.hide_underscore_folders_desc')"></p>
</div>
<!-- Tab inserts tab -->
<div class="mb-4">
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);" x-text="t('settings.tab_inserts_tab')"></span>
<div
@click="toggleTabInsertsTab()"
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
:style="tabInsertsTab ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
>
<div
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
:style="'background-color: var(--bg-primary);' + (tabInsertsTab ? ' transform: translateX(20px);' : '')"
></div>
</div>
</label>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.tab_inserts_tab_desc')"></p>
</div>
<div class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.new_button_action')"></label>
<select
x-model="newButtonAction"
@change="localStorage.setItem('newButtonAction', newButtonAction)"
class="w-full px-3 py-2 rounded border text-sm"
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
>
<option value="chooser" x-text="t('settings.new_button_option_chooser')"></option>
<option value="note" x-text="t('settings.new_button_option_note')"></option>
<option value="folder" x-text="t('settings.new_button_option_folder')"></option>
<option value="template" x-text="t('settings.new_button_option_template')"></option>
<option value="drawing" x-text="t('settings.new_button_option_drawing')"></option>
</select>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.new_button_action_desc')"></p>
</div>
<div class="mb-4">
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);" x-text="t('settings.auto_fill_note_title')"></span>
<div
@click="autoFillNoteTitle = !autoFillNoteTitle; localStorage.setItem('autoFillNoteTitle', autoFillNoteTitle)"
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
:style="autoFillNoteTitle ? 'background-color: var(--accent-primary)' : 'background-color: var(--bg-tertiary)'"
>
<div
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full transition-transform"
:style="'background-color: var(--bg-primary);' + (autoFillNoteTitle ? ' transform: translateX(20px);' : '')"
></div>
</div>
</label>
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('settings.auto_fill_note_title_desc')"></p>
</div>
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
<div x-show="authEnabled" class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.account')"></label>
<a
href="/logout"
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='var(--bg-primary)'"
x-text="'🔒 ' + t('settings.logout')"
>
</a>
</div>
</div>
<!-- Footer Info (inside scroll area) -->
<div class="p-3 mt-4 border-t text-center" style="border-color: var(--border-primary);">
<div class="text-xs" style="color: var(--text-tertiary);">
<span x-text="notes.length"></span> <span x-text="notes.length === 1 ? t('homepage.note_singular') : t('homepage.note_plural')"></span>
<span class="mx-1">·</span>
<span x-text="'v' + appVersion"></span>
</div>
<a
href="https://www.notediscovery.com"
target="_blank"
rel="noopener noreferrer"
class="text-xs hover:underline"
style="color: var(--accent-primary);"
>
notediscovery.com
</a>
<!-- Ko-fi support link (hidden if ALREADY_DONATED=true) -->
<template x-if="!alreadyDonated">
<div class="mt-3 flex flex-col gap-2 items-center">
<a
href="https://ko-fi.com/gamosoft"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-full transition-all hover:scale-105"
style="background-color: #ff5e5b; color: white;"
>
<span></span>
<span x-text="t('support.enjoying_demo')"></span>
</a>
<!-- PikaPods deploy button -->
<a
href="https://www.pikapods.com/pods?run=notediscovery"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center transition-all hover:scale-105"
>
<img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods" style="height: 28px;">
</a>
<!-- Link to hide these buttons -->
<a
href="https://github.com/gamosoft/NoteDiscovery/blob/main/documentation/ENVIRONMENT_VARIABLES.md#support"
target="_blank"
rel="noopener noreferrer"
class="text-[10px] opacity-50 hover:opacity-100 transition-opacity"
style="color: var(--text-tertiary);"
>
hide these buttons
</a>
</div>
</template>
<!-- Thank you message (shown when ALREADY_DONATED=true) -->
<template x-if="alreadyDonated">
<div class="mt-3 text-[10px] opacity-50 hover:opacity-100 transition-opacity text-center" style="color: var(--text-tertiary);">
<span x-text="t('support.thank_you')"></span>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- END SETTINGS PANEL -->
</div>
<!-- END Sidebar Panel -->
</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'"
x-show="!sidebarPanelCollapsed"
></div>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden relative">
<!-- Graph View Overlay (works from homepage or editor) -->
<div
x-show="showGraph"
id="graph-overlay"
class="absolute inset-0"
style="background-color: var(--bg-primary); z-index: 50;"
>
<!-- Graph renders here via vis-network -->
<div x-show="!graphLoaded" class="flex items-center justify-center h-full">
<div class="text-center" style="color: var(--text-secondary);">
<svg class="animate-spin h-8 w-8 mx-auto mb-3" style="color: var(--accent-primary);" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p>Loading graph...</p>
</div>
</div>
<!-- Close button -->
<button
@click="showGraph = false"
class="absolute top-4 left-4 px-3 py-2 text-sm font-medium rounded-lg transition-colors z-10"
style="background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
>
← Back
</button>
</div>
<template x-if="!currentNote && !currentMedia">
<!-- Notes Homepage -->
<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="t('sidebar.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 -->
<template x-if="isAppEmpty">
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
<svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" 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="t('homepage.no_notes_title')"></h2>
<p class="mb-6" style="color: var(--text-secondary);" x-text="t('homepage.no_notes_desc')"></p>
<button
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
class="px-6 py-3 text-sm font-medium text-white rounded-lg transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
<span x-text="t('sidebar.new_button')"></span>
</button>
</div>
</template>
<!-- Grid View - Show when app has content -->
<template x-if="!isAppEmpty">
<div class="max-w-7xl mx-auto p-6 md:p-8">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary);" x-text="appName"></h1>
<p class="text-lg" style="color: var(--text-secondary);" x-text="t('app.tagline')"></p>
<!-- Demo Mode Warning Banner -->
<div x-show="demoMode" class="mt-4 flex items-center px-4 py-2.5 text-sm rounded-lg" style="background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%); color: white; font-weight: 500;">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span><strong x-text="t('demo.title') + ':'"></strong> <span x-text="t('demo.warning')"></span></span>
</div>
</div>
<!-- Breadcrumb Navigation -->
<div class="mb-4 flex items-center gap-2 text-sm" style="color: var(--text-secondary);">
<template x-if="Array.isArray(homepageBreadcrumb())">
<template x-for="(crumb, index) in homepageBreadcrumb()" :key="index">
<div class="flex items-center gap-2">
<span
x-show="index > 0"
class="text-xs"
style="color: var(--text-tertiary);"
>/</span>
<span
@click="goToHomepageFolder(crumb.path)"
class="cursor-pointer hover:underline"
:style="crumb.path === selectedHomepageFolder ? 'color: var(--accent-primary); font-weight: 600;' : 'color: var(--text-secondary);'"
x-text="crumb.name"
></span>
</div>
</template>
</template>
</div>
<!-- Stats Bar - Only show when there are notes or folders -->
<template x-if="homepageNotes().length > 0 || homepageFolders().length > 0">
<div class="mb-6 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);">
<div class="flex flex-wrap items-center gap-4 text-sm" style="color: var(--text-secondary);">
<span class="font-semibold" style="color: var(--text-primary);">
<span x-text="homepageNotes().length"></span>
<span x-text="homepageNotes().length === 1 ? t('homepage.note_singular') : t('homepage.note_plural')"></span>
</span>
<span x-show="homepageFolders().length > 0">
<span x-text="homepageFolders().length"></span>
<span x-text="homepageFolders().length === 1 ? t('homepage.folder_singular') : t('homepage.folder_plural')"></span>
</span>
<button
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
class="ml-auto px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
<span x-text="t('sidebar.new_button')"></span>
</button>
</div>
</div>
</template>
<!-- Empty State (for folder view) -->
<template x-if="homepageNotes().length === 0 && homepageFolders().length === 0">
<div class="flex flex-col items-center justify-center py-16 text-center">
<svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" 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="selectedHomepageFolder ? t('homepage.folder_empty') : t('homepage.no_notes_title')"></h2>
<p class="mb-6" style="color: var(--text-secondary);" x-text="selectedHomepageFolder ? t('homepage.get_started') : t('homepage.no_notes_desc')"></p>
<button
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
class="px-6 py-3 text-sm font-medium text-white rounded-lg transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
<span x-text="t('sidebar.new_button')"></span>
</button>
</div>
</template>
<!-- Folders and Notes Grid -->
<template x-if="homepageNotes().length > 0 || homepageFolders().length > 0">
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 sm:gap-4">
<!-- Folders First -->
<template x-for="folder in homepageFolders()" :key="folder.path">
<div
@click="goToHomepageFolder(folder.path)"
class="homepage-card p-2 sm:p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
style="background-color: var(--bg-secondary); border-color: var(--accent-primary); display: flex; flex-direction: column; border-width: 2px; position: relative;"
onmouseover="this.style.borderColor='var(--accent-hover)'; this.style.transform='translateY(-2px)'; this.querySelector('.card-delete-btn').style.opacity='1';"
onmouseout="this.style.borderColor='var(--accent-primary)'; this.style.transform='translateY(0)'; this.querySelector('.card-delete-btn').style.opacity='0';"
>
<!-- Delete Button -->
<button
@click.stop="deleteFolder(folder.path, folder.name)"
class="card-delete-btn hidden sm:block"
style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
:title="t('common.delete')"
>
<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>
<!-- Folder Icon -->
<div class="mb-1 sm:mb-2 flex items-center gap-1 sm:gap-2">
<svg class="w-5 h-5 sm:w-6 sm:h-6 flex-shrink-0" style="color: var(--accent-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
<h3 class="font-semibold text-sm sm:text-base" style="color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" x-text="folder.name"></h3>
</div>
<!-- Folder Metadata -->
<div class="flex items-center justify-between text-xs mt-auto pt-1 sm:pt-2" style="color: var(--text-tertiary);">
<span x-text="folder.noteCount + ' ' + (folder.noteCount === 1 ? t('homepage.note_singular') : t('homepage.note_plural'))"></span>
<svg class="w-4 h-4 hidden sm:block" style="color: var(--accent-primary);" 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>
</div>
</div>
</template>
<!-- Notes -->
<template x-for="note in homepageNotes().slice(0, HOMEPAGE_MAX_NOTES)" :key="note.path">
<div
@click="openItem(note.path, note.type)"
class="homepage-card p-2 sm:p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
style="background-color: var(--bg-secondary); border-color: var(--border-primary); display: flex; flex-direction: column; position: relative;"
onmouseover="this.style.borderColor='var(--accent-primary)'; this.style.transform='translateY(-2px)'; this.querySelector('.card-delete-btn').style.opacity='1';"
onmouseout="this.style.borderColor='var(--border-primary)'; this.style.transform='translateY(0)'; this.querySelector('.card-delete-btn').style.opacity='0';"
>
<!-- Delete Button -->
<button
@click.stop="note.type !== 'note' ? deleteMedia(note.path) : deleteNote(note.path, note.name)"
class="card-delete-btn hidden sm:block"
style="position: absolute; top: 8px; right: 8px; opacity: 0; transition: opacity 0.2s; color: var(--error); padding: 4px; border-radius: 4px; background-color: var(--bg-secondary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
:title="note.type === 'note' ? t('toolbar.delete_note') : (note.type === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image'))"
>
<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>
<!-- Note Title -->
<h3 class="font-semibold text-sm sm:text-base mb-1 sm:mb-2" style="color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" x-text="note.name"></h3>
<!-- Metadata -->
<div class="flex items-center justify-between text-xs mt-auto pt-1 sm:pt-2" style="color: var(--text-tertiary);">
<span x-text="formatDate(note.modified)"></span>
<span class="hidden sm:inline" x-text="formatSize(note.size)"></span>
</div>
</div>
</template>
</div>
</template>
<!-- Show More Indicator -->
<div x-show="homepageNotes().length > HOMEPAGE_MAX_NOTES" class="mt-6 text-center">
<p class="text-sm" style="color: var(--text-secondary);">
Showing <span x-text="HOMEPAGE_MAX_NOTES"></span> of <span x-text="homepageNotes().length"></span> notes in this folder.
</p>
</div>
</div>
</template>
</div>
</template>
<template x-if="currentNote || currentMedia">
<!-- Editor Area -->
<div class="flex-1 flex flex-col overflow-hidden" :class="{'zen-editor-container': zenMode}" style="background-color: var(--bg-primary);">
<!-- Toolbar -->
<div class="px-4 py-3 flex items-center justify-between mobile-toolbar zen-hide" style="background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-primary);">
<div class="flex items-center space-x-2">
<!-- 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="t('sidebar.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>
<input
type="text"
:value="currentNote ? currentNoteName : (currentMedia ? currentMedia.split('/').pop() : '')"
@input="if (currentNote) currentNoteName = $event.target.value"
@blur="if (currentNote) renameNote()"
:readonly="!currentNote"
class="text-xl font-semibold border-none focus:outline-none focus:ring-2 rounded px-3 py-1.5"
:style="'background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); min-width: 150px; max-width: 300px;' + (!currentNote ? ' cursor: default; opacity: 0.9;' : '')"
>
<!-- Undo Button (note history or drawing strokes when viewing a drawing) -->
<button
@click="currentMediaType === 'drawing' ? drawingUndo() : undo()"
:disabled="currentMediaType === 'drawing' ? drawingOps.length === 0 : undoHistory.length <= 1"
class="p-2 rounded-lg"
:style="(currentMediaType === 'drawing' ? drawingOps.length === 0 : 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="t('toolbar.undo')"
>
<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="currentMediaType === 'drawing' ? drawingRedo() : redo()"
:disabled="currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : redoHistory.length === 0"
class="p-2 rounded-lg"
:style="(currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : 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="t('toolbar.redo')"
>
<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="currentMedia ? deleteMedia(currentMedia) : deleteCurrentNote()"
class="p-2 rounded-lg"
style="color: var(--error);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
:title="currentMedia ? (currentMediaType === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image')) : t('toolbar.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);" x-text="t('common.saving')"></span>
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);" x-text="t('common.saved')"></span>
<span x-show="!isSaving && !lastSaved && lastEditedText" class="text-xs hidden md:inline" style="color: var(--text-tertiary);" x-text="t('editor.edited', {time: lastEditedText})"></span>
</div>
<!-- Demo Mode Warning Banner -->
<div x-show="demoMode" class="flex items-center px-3 py-1.5 text-xs rounded-lg" style="background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%); color: white; font-weight: 500;">
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span><strong x-text="t('demo.title') + ':'"></strong> <span x-text="t('demo.warning')"></span></span>
</div>
<div class="flex items-center space-x-2" x-show="currentNote">
<!-- View Toggle (only for notes) -->
<div class="flex rounded-lg p-1" style="background-color: var(--bg-tertiary);">
<button
@click="viewMode = 'edit'"
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
:style="viewMode === 'edit' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
@mouseenter="if (viewMode !== 'edit') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'edit') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_edit')"
>
</button>
<button
@click="viewMode = 'split'"
class="mobile-hide-split px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
:style="viewMode === 'split' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
@mouseenter="if (viewMode !== 'split') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'split') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_split')"
>
</button>
<button
@click="viewMode = 'preview'"
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
:style="viewMode === 'preview' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
@mouseenter="if (viewMode !== 'preview') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'preview') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_preview')"
>
</button>
</div>
<!-- Note Actions Toolbar -->
<div
x-show="currentNote"
class="flex items-center rounded overflow-hidden"
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
>
<button
@click="toggleFavorite()"
class="p-2 transition"
:title="isFavorite(currentNote) ? t('toolbar.remove_favorite') : t('toolbar.add_favorite')"
:style="'color: ' + (isFavorite(currentNote) ? 'var(--warning, #eab308)' : 'var(--text-secondary)') + ';'"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<!-- Filled star (favorited) -->
<svg x-show="isFavorite(currentNote)" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
</svg>
<!-- Empty star (not favorited) -->
<svg x-show="!isFavorite(currentNote)" x-cloak 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="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path>
</svg>
</button>
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
<button
@click="exportToHTML()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('toolbar.export_html')"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
</button>
<button
@click="printPreview()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('toolbar.print_preview')"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
</button>
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
<button
@click="copyNoteLink()"
class="p-2 transition hover:bg-opacity-80"
:title="linkCopied ? t('common.copied') : t('toolbar.copy_link')"
:style="'color: ' + (linkCopied ? 'var(--success, #22c55e)' : 'var(--text-secondary)') + ';'"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<!-- Link icon (default) -->
<svg x-show="!linkCopied" 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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
<!-- Checkmark icon (after copy) -->
<svg x-show="linkCopied" x-cloak 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="M5 13l4 4L19 7"></path>
</svg>
</button>
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
<!-- Share button -->
<button
@click="openShareModal()"
class="p-2 transition hover:bg-opacity-80"
:title="t('share.button_tooltip')"
style="color: var(--text-secondary);"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<!-- Share/Globe icon -->
<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="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
</button>
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
<button
@click="toggleZenMode()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('zen_mode.tooltip')"
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="$el.style.backgroundColor = 'transparent'"
>
<!-- Expand/Focus icon -->
<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="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Drawing tools (below main toolbar; undo/redo use icons above) -->
<div
x-show="currentMedia && currentMediaType === 'drawing'"
class="px-4 py-2 flex flex-wrap gap-2 items-center border-b flex-shrink-0 zen-hide"
style="background-color: var(--bg-secondary); border-color: var(--border-primary);"
x-cloak
>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'freehand' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'freehand'"
:title="t('drawing.tool_freehand')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'line' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'line'"
:title="t('drawing.tool_line')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 20L20 4" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'rect' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'rect'"
:title="t('drawing.tool_rect')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a2 2 0 012-2h12a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V5z" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'ellipse' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'ellipse'"
:title="t('drawing.tool_ellipse')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'eraser' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'eraser'"
:title="t('drawing.tool_eraser')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21M22 21H7M5 11l9 9" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'eyedropper' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'eyedropper'"
:title="t('drawing.tool_eyedropper')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 11.25 1.425 1.425c.671.671.671 1.757 0 2.428l-1.06 1.06c-.671.671-1.757.671-2.428 0l-1.425-1.425m6-6 1.425-1.425c.671-.671 1.757-.671 2.428 0l1.06 1.06c.671.671.671 1.757 0 2.428L21 15.75m-6-6L8.25 3.75a2.121 2.121 0 0 0-3 3L12 15.75l3-3m-3-3-6 6" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'fill' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'fill'"
:title="t('drawing.tool_fill')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3l9 9-7.5 7.5a2.121 2.121 0 01-3 0L3 16.5a2.121 2.121 0 010-3L9 3z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.5h15" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 14.5c0 1-1.5 3-1.5 3s-1.5-2-1.5-3a1.5 1.5 0 113 0z" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:style="drawingTool === 'text' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
@click="drawingTool = 'text'"
:title="t('drawing.tool_text')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 4h14M12 4v16M8 20h8" />
</svg>
</button>
<button
type="button"
class="p-2 rounded-lg border transition-colors"
:class="!drawingClearEnabled() ? 'opacity-40 cursor-not-allowed' : ''"
:style="!drawingClearEnabled()
? 'border-color: var(--border-primary); color: var(--text-secondary);'
: 'border-color: var(--border-primary); color: var(--text-secondary); background-color: var(--bg-tertiary);'"
:disabled="!drawingClearEnabled()"
@click="drawingClear()"
:title="t('drawing.clear_hint')"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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" />
</svg>
</button>
<label
class="flex h-9 items-center gap-2 rounded-lg border px-2 shrink-0 cursor-pointer"
style="border-color: var(--border-primary); background-color: var(--bg-tertiary);"
:title="t('drawing.color')"
>
<span class="text-xs select-none" style="color: var(--text-secondary);" x-text="t('drawing.color')"></span>
<input
type="color"
x-model="drawingColor"
class="h-7 w-8 shrink-0 cursor-pointer rounded border p-0"
style="border-color: var(--border-primary);"
:aria-label="t('drawing.color')"
/>
</label>
<div
class="flex h-9 min-w-0 shrink items-center gap-2 rounded-lg border px-2"
style="border-color: var(--border-primary); background-color: var(--bg-tertiary);"
:title="t('drawing.width')"
>
<span class="text-xs whitespace-nowrap select-none shrink-0" style="color: var(--text-secondary);" x-text="t('drawing.width')"></span>
<input
type="range"
min="1"
max="32"
x-model.number="drawingLineWidth"
class="w-24 md:w-36 align-middle min-w-0 flex-1"
style="accent-color: var(--accent-primary);"
:aria-label="t('drawing.width')"
:aria-valuemin="1"
:aria-valuemax="32"
:aria-valuenow="drawingLineWidth"
/>
<span class="text-xs tabular-nums shrink-0 w-5 text-right" style="color: var(--text-secondary);" x-text="drawingLineWidth"></span>
</div>
</div>
<!-- Editor/Preview/Graph -->
<div class="flex-1 flex relative" style="min-height: 0;">
<!-- Editor -->
<div
x-show="!currentMedia && (viewMode === 'edit' || viewMode === 'split')"
class="flex flex-col"
style="min-height: 0;"
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
>
<!-- Markdown Formatting Toolbar -->
<div class="flex items-center gap-1 px-2 py-1.5 border-b flex-shrink-0" style="background-color: var(--bg-secondary); border-color: var(--border-primary);">
<button @click="formatText('bold')" class="format-btn" :title="t('format.bold') + ' (Ctrl+B)'">
<span class="font-bold">B</span>
</button>
<button @click="formatText('italic')" class="format-btn" :title="t('format.italic') + ' (Ctrl+I)'">
<span class="italic">I</span>
</button>
<button @click="formatText('strikethrough')" class="format-btn" :title="t('format.strikethrough')">
<span class="line-through">S</span>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('heading')" class="format-btn" :title="t('format.heading')">
<span class="font-bold text-xs">H</span>
</button>
<button @click="formatText('link')" class="format-btn" :title="t('format.link') + ' (Ctrl+K)'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
</button>
<button @click="formatText('image')" class="format-btn" :title="t('format.image')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('code')" class="format-btn" :title="t('format.code')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
</button>
<button @click="formatText('codeblock')" class="format-btn" :title="t('format.codeblock')">
<span class="text-xs font-mono">{}</span>
</button>
<button @click="formatText('quote')" class="format-btn" :title="t('format.quote')">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/></svg>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('bullet')" class="format-btn" :title="t('format.bullet_list')">
<svg class="w-3.5 h-3.5" 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>
<button @click="formatText('numbered')" class="format-btn" :title="t('format.numbered_list')">
<span class="text-xs font-mono">1.</span>
</button>
<button @click="formatText('checkbox')" class="format-btn" :title="t('format.checkbox')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</button>
<button @click="formatText('table')" class="format-btn" :title="t('format.table') + ' (Ctrl+Alt+T)'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
</button>
</div>
<div
class="editor-wrapper"
:class="{'syntax-highlight': syntaxHighlightEnabled}"
>
<div
id="syntax-overlay"
class="syntax-overlay"
x-show="syntaxHighlightEnabled"
x-html="syntaxHighlightEnabled ? highlightMarkdown(noteContent) : ''"
></div>
<textarea
id="note-editor"
x-model="noteContent"
@input="autoSave(); updateSyntaxHighlight()"
@scroll="syncOverlayScroll()"
@keydown.enter="handleEditorEnterKey($event)"
@keydown.tab="handleTabKey($event)"
@drop="onEditorDrop($event)"
@dragover.prevent="onEditorDragOver($event)"
@dragenter="onEditorDragEnter($event)"
@dragleave="onEditorDragLeave($event)"
@paste="handlePaste($event)"
:class="'flex-1 w-full resize-none focus:outline-none editor-textarea' + (draggedItem && dropTarget === 'editor' ? ' link-drop-target' : '')"
:style="'background-color: var(--bg-primary); color: var(--text-primary);' + (draggedItem && dropTarget === 'editor' ? ' outline: 2px dashed var(--accent-primary); outline-offset: -2px; box-shadow: 0 0 20px var(--accent-light);' : '')"
:placeholder="draggedItem && dropTarget === 'editor' ? t('editor.drop_hint') : t('editor.placeholder')"
></textarea>
</div>
</div>
<!-- Split view resize handle -->
<div
x-show="!currentMedia && viewMode === 'split'"
@mousedown="startSplitResize($event)"
class="split-resize-handle"
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>
<!-- Preview -->
<div
x-show="(viewMode === 'preview' || viewMode === 'split') && !currentMedia"
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%;'"
>
<!-- 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 flex-1 min-w-0" 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, 'readable-width': readableLineLength }"
x-html="renderedMarkdown"
@click="handleInternalLink($event)"
></div>
</div>
<!-- Media Viewer (images, audio, video, PDF) -->
<template x-if="currentMedia">
<div
class="flex-1 flex overflow-auto min-h-0"
:class="currentMediaType === 'drawing' ? 'items-stretch justify-stretch' : 'items-center justify-center'"
style="background-color: var(--bg-primary);"
>
<!-- Image -->
<template x-if="currentMediaType === 'image'">
<img
:src="`/api/media/${currentMedia}`"
:alt="currentMedia.split('/').pop()"
style="max-width: 100%; max-height: 100%; object-fit: contain; padding: 2rem;"
/>
</template>
<!-- Audio -->
<template x-if="currentMediaType === 'audio'">
<div class="flex flex-col items-center gap-4 p-8">
<div class="text-6xl">🎵</div>
<div class="text-lg font-medium" style="color: var(--text-primary);" x-text="currentMedia.split('/').pop()"></div>
<audio
controls
:src="`/api/media/${currentMedia}`"
style="width: 100%; max-width: 500px;"
>
Your browser does not support the audio element.
</audio>
</div>
</template>
<!-- Video -->
<template x-if="currentMediaType === 'video'">
<video
controls
:src="`/api/media/${currentMedia}`"
style="max-width: 100%; max-height: 100%; padding: 1rem;"
>
Your browser does not support the video element.
</video>
</template>
<!-- PDF -->
<template x-if="currentMediaType === 'document'">
<iframe
:src="`/api/media/${currentMedia}`"
style="width: 100%; height: 100%; border: none;"
:title="currentMedia.split('/').pop()"
></iframe>
</template>
<!-- Drawing (PNG named drawing-*.png); tools row is in header area above -->
<template x-if="currentMediaType === 'drawing'">
<div
class="flex flex-col flex-1 w-full min-w-0 min-h-0 self-stretch"
:key="currentMedia"
>
<div
x-ref="drawingCanvasWrap"
class="flex-1 w-full min-h-0 min-w-0 flex items-center justify-center relative"
style="min-height: 0; background-color: var(--bg-secondary);"
>
<canvas
x-ref="drawingCanvas"
class="drawing-surface block"
:style="drawingTool === 'eyedropper'
? 'touch-action: none; cursor: copy; background: #fff;'
: drawingTool === 'fill'
? 'touch-action: none; cursor: cell; background: #fff;'
: drawingTool === 'text'
? 'touch-action: none; cursor: text; background: #fff;'
: 'touch-action: none; cursor: crosshair; background: #fff;'"
@pointerdown.prevent="drawingPointerDown($event)"
@pointermove.prevent="drawingPointerMove($event)"
@pointerup.prevent="drawingPointerUp($event)"
@pointercancel.prevent="drawingPointerUp($event)"
></canvas>
<!-- Text-tool floating editor — only visible while placing text.
The contenteditable's typed glyphs are kept INVISIBLE
(color: transparent) on purpose: HTML and canvas text-rendering
land glyphs on subtly different pixels even with identical font /
size / line-height, so showing both would always result in a tiny
jump on commit. Instead we render the typed text live on the
canvas (via drawingDraft), so what the user sees while typing IS
the canvas output — pixel-identical to the rasterized op on Enter.
Only the caret (caret-color) and the dashed outline remain
visible to indicate where the text will land. -->
<div
id="drawing-text-input"
x-show="drawingTextActive"
contenteditable="plaintext-only"
spellcheck="false"
@keydown.enter.prevent="drawingTextCommit()"
@keydown.escape.prevent="drawingTextCancel()"
@blur="drawingTextCommit()"
@input="drawingTextOnInput($event)"
:style="`position: absolute;
left: ${drawingTextCssX}px;
top: ${drawingTextCssY}px;
font-size: ${Math.max(12, drawingTextDocFontSize * ((($refs.drawingCanvas && $refs.drawingCanvas.clientWidth) || drawingDocW) / drawingDocW))}px;
line-height: 1;
font-family: sans-serif;
color: transparent;
caret-color: ${drawingColor};
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
margin: 0;
outline: 1px dashed ${drawingColor};
outline-offset: -1px;
box-shadow: none;
z-index: 5;
min-width: 4ch;
white-space: pre;`"
x-cloak
></div>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- Stats Status Bar (only shows if plugin enabled) -->
<div x-show="statsPluginEnabled && currentNote && noteStats"
class="border-t zen-hide"
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>
<!-- Sentences -->
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Sentences</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.sentences.toLocaleString() : '0'"></div>
</div>
<!-- Lists -->
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Lists</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.list_items.toLocaleString() : '0'"></div>
</div>
<!-- Tables -->
<div>
<div class="text-xs" style="color: var(--text-tertiary);">Tables</div>
<div class="font-semibold" style="color: var(--text-primary);" x-text="noteStats ? noteStats.tables.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>
<!-- 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 x-text="t('sidebar.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 x-text="t('sidebar.new_folder')"></span>
</button>
<button
@click.stop="openTemplateModal()"
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 x-text="t('sidebar.new_from_template')"></span>
</button>
<button
@click="createNewDrawing()"
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 x-text="t('sidebar.new_drawing')"></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);" x-text="t('templates.create_from_template')"></h2>
<!-- Template Selection -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('templates.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="" x-text="'-- ' + t('templates.choose_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);" x-text="t('notes.prompt_name')">
</label>
<input
id="template-note-name-input"
x-model="newTemplateNoteName"
type="text"
:placeholder="t('notes.input_placeholder')"
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 x-text="t('templates.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;">{{year}}</code>,
<code style="font-family: monospace;">{{month}}</code>,
<code style="font-family: monospace;">{{day}}</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);" x-text="t('templates.no_templates')">
</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)'"
x-text="t('templates.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)'"
x-text="t('common.cancel')"
>
</button>
</div>
</div>
</div>
<!-- New note / folder name (replaces window.prompt) -->
<div x-show="showCreateNameModal"
@click.self="cancelCreateNameModal()"
@keydown.escape.window="showCreateNameModal && cancelCreateNameModal()"
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" style="color: var(--text-primary);" :class="createNameModalHelpText().trim() ? 'mb-2' : 'mb-4'" x-text="createNameModalTitle()"></h2>
<p x-show="createNameModalHelpText().trim()" class="text-sm mb-4 whitespace-pre-line" style="color: var(--text-secondary);" x-text="createNameModalHelpText()"></p>
<div class="mb-4">
<label for="create-name-modal-input" class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="createNameModalLabel()"></label>
<input
id="create-name-modal-input"
type="text"
x-model="createNameModalInput"
:placeholder="createNameModalKind === 'folder' ? t('folders.input_placeholder') : t('notes.input_placeholder')"
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="submitCreateNameModal()"
autocomplete="off"
/>
</div>
<div class="flex gap-2">
<button
type="button"
@click="submitCreateNameModal()"
class="flex-1 px-4 py-2 rounded font-medium text-white transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
x-text="t('common.create')"
></button>
<button
type="button"
@click="cancelCreateNameModal()"
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)'"
x-text="t('common.cancel')"
></button>
</div>
</div>
</div>
<!-- Rename folder (replaces window.prompt) -->
<div x-show="showRenameFolderModal"
@click.self="cancelRenameFolderModal()"
@keydown.escape.window="showRenameFolderModal && cancelRenameFolderModal()"
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-2" style="color: var(--text-primary);" x-text="t('folders.rename_modal_title')"></h2>
<p class="text-sm mb-4" style="color: var(--text-secondary);" x-text="t('folders.rename_modal_description', { name: renameFolderOldName })"></p>
<div class="mb-4">
<label for="rename-folder-modal-input" class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('folders.rename_modal_label')"></label>
<input
id="rename-folder-modal-input"
type="text"
x-model="renameFolderInput"
:placeholder="renameFolderOldName"
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="submitRenameFolderModal()"
autocomplete="off"
/>
</div>
<div class="flex gap-2">
<button
type="button"
@click="submitRenameFolderModal()"
class="flex-1 px-4 py-2 rounded font-medium text-white transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
x-text="t('common.rename')"
></button>
<button
type="button"
@click="cancelRenameFolderModal()"
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)'"
x-text="t('common.cancel')"
></button>
</div>
</div>
</div>
<!-- Confirm dialog (replaces window.confirm) -->
<div x-show="showConfirmModal"
@click.self="confirmModalCancel()"
@keydown.escape.window="showConfirmModal && confirmModalCancel()"
@keydown.enter.window="if (showConfirmModal) { $event.preventDefault(); confirmModalConfirm(); }"
class="fixed inset-0 flex items-center justify-center p-4"
style="background-color: rgba(0, 0, 0, 0.5); z-index: 1250;"
x-cloak>
<div class="rounded-lg shadow-xl p-6 max-w-lg w-full mx-auto max-h-[90vh] overflow-y-auto"
style="background-color: var(--bg-secondary); color: var(--text-primary);"
@click.stop
role="alertdialog"
aria-modal="true"
:aria-labelledby="'confirm-modal-title'"
:aria-describedby="'confirm-modal-desc'">
<h2 id="confirm-modal-title" class="text-xl font-bold mb-3" style="color: var(--text-primary);" x-text="confirmModalTitle"></h2>
<p id="confirm-modal-desc" class="text-sm mb-6 whitespace-pre-line leading-relaxed" style="color: var(--text-secondary);" x-text="confirmModalMessage"></p>
<div class="flex flex-col-reverse sm:flex-row gap-2 sm:justify-end">
<button
type="button"
@click="confirmModalCancel()"
class="w-full sm:w-auto px-4 py-2.5 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)'"
x-text="confirmModalCancelLabel"
></button>
<button
type="button"
@click="confirmModalConfirm()"
class="w-full sm:w-auto px-4 py-2.5 rounded font-medium text-white transition-colors"
:style="confirmModalDanger ? 'background-color: var(--error);' : 'background-color: var(--accent-primary);'"
@mouseenter="$el.style.backgroundColor = confirmModalDanger ? '#dc2626' : 'var(--accent-hover)'"
@mouseleave="$el.style.backgroundColor = confirmModalDanger ? 'var(--error)' : 'var(--accent-primary)'"
x-text="confirmModalConfirmLabel"
></button>
</div>
</div>
</div>
<!-- Quick Switcher Modal (Ctrl+K) -->
<div x-show="showQuickSwitcher"
@click.self="closeQuickSwitcher()"
@keydown.escape.window="showQuickSwitcher && closeQuickSwitcher()"
x-effect="if (showQuickSwitcher) { quickSwitcherResults = filterQuickSwitcher(quickSwitcherQuery); quickSwitcherIndex = 0; }"
class="fixed inset-0 flex items-start justify-center pt-24"
style="background-color: rgba(0, 0, 0, 0.5); z-index: 1200;"
x-cloak>
<div class="rounded-lg shadow-2xl w-full max-w-lg mx-4 overflow-hidden"
style="background-color: var(--bg-primary); border: 1px solid var(--border-primary);"
@click.stop>
<!-- Search Input -->
<div class="p-3 border-b" style="border-color: var(--border-primary);">
<input id="quickSwitcherInput"
type="text"
x-model="quickSwitcherQuery"
@keydown="handleQuickSwitcherKeydown($event)"
:placeholder="t('quick_switcher.placeholder')"
class="w-full px-3 py-2 rounded text-base outline-none"
style="background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary);"
autocomplete="off">
</div>
<!-- Results List -->
<div id="quickSwitcherResults" class="max-h-80 overflow-y-auto">
<template x-if="quickSwitcherResults.length === 0">
<div class="p-4 text-center text-sm" style="color: var(--text-secondary);">
<span x-text="t('quick_switcher.no_results')"></span>
</div>
</template>
<template x-for="(note, index) in quickSwitcherResults" :key="note.path">
<div @click="selectQuickSwitcherNote(note)"
data-quick-switcher-item
:class="index === quickSwitcherIndex ? 'quick-switcher-selected' : ''"
class="px-4 py-3 cursor-pointer flex items-center gap-3 transition-colors"
:style="index === quickSwitcherIndex ? 'background-color: var(--bg-tertiary);' : ''"
@mouseenter="quickSwitcherIndex = index">
<!-- Note icon -->
<svg class="w-4 h-4 flex-shrink-0" style="color: var(--text-secondary);" 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>
<div class="flex-1 min-w-0">
<div class="font-medium truncate" style="color: var(--text-primary);" x-text="note.name" :title="note.name"></div>
<div class="text-xs truncate" style="color: var(--text-secondary);" x-show="note.folder" x-text="note.folder" :title="note.folder"></div>
</div>
</div>
</template>
<!-- Recent notes hint (only when no query typed) -->
<div x-show="!quickSwitcherQuery.trim() && quickSwitcherResults.length > 0"
class="px-4 py-2 text-xs text-center italic"
style="color: var(--text-secondary);">
<span x-text="t('quick_switcher.recent_hint')"></span>
</div>
</div>
<!-- Footer hint -->
<div class="px-4 py-2 text-xs border-t flex gap-4" style="border-color: var(--border-primary); color: var(--text-secondary); background-color: var(--bg-secondary);">
<span>↑↓ <span x-text="t('quick_switcher.navigate')"></span></span>
<span><span x-text="t('quick_switcher.open')"></span></span>
<span>esc <span x-text="t('quick_switcher.close')"></span></span>
</div>
</div>
</div>
<!-- Share Modal -->
<div x-show="showShareModal"
@click.self="closeShareModal()"
@keydown.escape.window="showShareModal && closeShareModal()"
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 flex items-center gap-2" style="color: var(--text-primary);">
<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="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
<span x-text="t('share.modal_title')"></span>
</h2>
<!-- Loading state -->
<div x-show="shareLoading" class="text-center py-4">
<span x-text="t('common.loading')"></span>
</div>
<!-- Not shared state -->
<div x-show="!shareLoading && !shareInfo?.shared">
<p class="mb-4 text-sm" style="color: var(--text-secondary);" x-text="t('share.not_shared_description')"></p>
<button
@click="createShareLink()"
class="w-full px-4 py-2 rounded font-medium text-white transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
x-text="t('share.create_link')"
>
</button>
</div>
<!-- Shared state -->
<div x-show="!shareLoading && shareInfo?.shared">
<p class="mb-2 text-sm" style="color: var(--text-secondary);" x-text="t('share.shared_description')"></p>
<!-- Share URL -->
<div class="flex gap-2 mb-4">
<input
type="text"
:value="shareInfo?.url || ''"
readonly
class="flex-1 px-3 py-2 rounded border text-sm"
style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-primary);"
@click="$el.select()"
/>
<button
@click="copyShareLink()"
class="px-3 py-2 rounded font-medium text-white transition-colors flex items-center gap-1"
:style="shareLinkCopied ? 'background-color: var(--success, #22c55e);' : 'background-color: var(--accent-primary);'"
onmouseover="if(!this.classList.contains('copied')) this.style.backgroundColor='var(--accent-hover)'"
onmouseout="if(!this.classList.contains('copied')) this.style.backgroundColor='var(--accent-primary)'"
>
<svg x-show="!shareLinkCopied" 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="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
</svg>
<svg x-show="shareLinkCopied" x-cloak 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="M5 13l4 4L19 7"></path>
</svg>
<span x-text="shareLinkCopied ? t('common.copied') : t('share.copy')"></span>
</button>
</div>
<!-- QR Code -->
<div class="mb-4 flex flex-col items-center">
<button
@click="showShareQR = !showShareQR"
class="text-sm flex items-center gap-1 mb-2 transition-colors"
style="color: var(--text-secondary);"
onmouseover="this.style.color='var(--text-primary)'"
onmouseout="this.style.color='var(--text-secondary)'"
>
<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="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path>
</svg>
<span x-text="showShareQR ? t('share.hide_qr') : t('share.show_qr')"></span>
<svg class="w-3 h-3 transition-transform" :class="showShareQR ? 'rotate-180' : ''" 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>
</button>
<div x-show="showShareQR"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95"
class="p-3 rounded-lg"
style="background-color: white;">
<img :src="generateQRCode(shareInfo?.url)" alt="QR Code" class="w-32 h-32" />
</div>
</div>
<!-- Revoke sharing -->
<button
@click="revokeShareLink()"
class="w-full px-4 py-2 rounded font-medium transition-colors"
style="background-color: transparent; color: var(--error); border: 1px solid var(--error);"
onmouseover="this.style.backgroundColor='rgba(239, 68, 68, 0.1)'"
onmouseout="this.style.backgroundColor='transparent'"
x-text="t('share.stop_sharing')"
>
</button>
</div>
<!-- Close button -->
<div class="mt-4 pt-4 border-t" style="border-color: var(--border-primary);">
<button
@click="closeShareModal()"
class="w-full 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)'"
x-text="t('common.close')"
>
</button>
</div>
</div>
</div>
<!-- Mobile Bottom Tabs -->
<nav class="mobile-bottom-tabs zen-hide" style="display: none;" role="navigation" aria-label="Main navigation">
<button
class="mobile-bottom-tab"
:class="{'active': activePanel === 'files' && (mobileSidebarOpen || (!currentNote && !currentMedia && !showGraph))}"
@click="mobileFilesTabClick()"
:aria-label="t('sidebar.files')"
>
<!-- Show home icon when viewing content, folder icon when on homepage -->
<svg x-show="currentNote || currentMedia || showGraph" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
<svg x-show="!currentNote && !currentMedia && !showGraph" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
<span x-text="(currentNote || currentMedia || showGraph) ? t('homepage.title') : t('sidebar.files')"></span>
</button>
<button
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'search' && mobileSidebarOpen}"
@click="activePanel = 'search'; mobileSidebarOpen = true;"
:aria-label="t('sidebar.search')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<span x-text="t('sidebar.search')"></span>
<!-- Search results badge -->
<span
x-show="searchQuery.trim() && searchResults.length > 0"
x-cloak
class="absolute top-1 right-1/4 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<button
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'tags' && mobileSidebarOpen}"
@click="activePanel = 'tags'; mobileSidebarOpen = true;"
:aria-label="t('tags.title')"
>
<svg fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>
<span x-text="t('tags.title')"></span>
<!-- Filtered notes badge (shows result count when tags selected) -->
<span
x-show="selectedTags.length > 0"
x-cloak
class="absolute top-1 right-1/4 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<button
class="mobile-bottom-tab"
:class="{'active': showGraph}"
@click="showGraph = true; initGraph(); mobileSidebarOpen = false;"
:aria-label="t('graph.title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
<span x-text="t('graph.title')"></span>
</button>
<button
class="mobile-bottom-tab"
:class="{'active': activePanel === 'settings' && mobileSidebarOpen}"
@click="activePanel = 'settings'; mobileSidebarOpen = true;"
:aria-label="t('sidebar.settings')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span x-text="t('sidebar.settings')"></span>
</button>
</nav>
<script src="/static/editor-markdown-continue.js"></script>
<script src="/static/app.js"></script>
<!-- PWA Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration.scope);
})
.catch((error) => {
console.log('SW registration failed:', error);
});
});
}
</script>
</body>
</html>