NoteDiscovery/frontend/index.html

4028 lines
238 KiB
HTML
Raw Normal View History

2025-11-05 16:48:41 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NoteDiscovery</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon.svg">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon.svg">
<link rel="apple-touch-icon" href="/static/logo.svg">
2026-01-01 14:26:03 +00:00
<!-- 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>
2025-11-05 16:48:41 +00:00
<!-- 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>
2025-11-05 16:48:41 +00:00
<!-- Marked.js for Markdown parsing (v12.0.2) -->
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
2025-11-05 16:48:41 +00:00
2026-03-03 08:27:01 +00:00
<!-- DOMPurify for HTML sanitization (XSS prevention) -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
2025-11-11 13:51:55 +00:00
<!-- MathJax for LaTeX math rendering -->
<script>
MathJax = {
tex: {
inlineMath: [['\\(', '\\)'], ['$', '$']],
displayMath: [['\\[', '\\]'], ['$$', '$$']],
2025-11-11 13:51:55 +00:00
processEscapes: true,
processEnvironments: true
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
},
startup: {
pageReady: () => {
return MathJax.startup.defaultPageReady();
}
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
2025-11-11 13:51:55 +00:00
<!-- Highlight.js for code syntax highlighting (v11.9.0) -->
2025-11-05 16:48:41 +00:00
<link rel="stylesheet" id="highlight-theme" href="">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/csharp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/shell.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/http.min.js"></script>
2025-12-06 12:06:23 +00:00
<!-- Mermaid.js for diagram rendering -->
2025-11-17 16:29:00 +00:00
<script type="module">
2025-12-06 12:06:23 +00:00
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.esm.min.mjs';
2025-11-17 16:37:29 +00:00
// Note: Mermaid is initialized dynamically by app.js renderMermaid() based on current theme
2025-11-17 16:29:00 +00:00
window.mermaid = mermaid;
</script>
2025-12-04 17:05:41 +00:00
<!-- 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>
2026-02-09 17:55:55 +00:00
<!-- QR Code generator for share links -->
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
2025-12-04 17:05:41 +00:00
2025-11-05 16:48:41 +00:00
<!-- Theme styles will be loaded dynamically -->
<link rel="stylesheet" id="theme-stylesheet" href="">
<!-- Custom styles -->
<style>
[x-cloak] { display: none !important; }
/* Theme-aware styles using CSS variables */
body {
background-color: var(--bg-primary, #ffffff);
color: var(--text-primary, #111827);
}
2025-11-20 13:10:17 +00:00
/* Truncate utility for long text */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
2025-11-05 16:48:41 +00:00
.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;
}
2026-01-26 09:22:00 +00:00
/* Readable line length - constrain text width for better readability */
.markdown-preview.readable-width {
max-width: 750px;
margin-left: auto;
margin-right: auto;
}
2025-11-05 16:48:41 +00:00
/* 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);
}
2025-12-03 15:00:25 +00:00
/* Wikilinks - Obsidian style internal links */
.markdown-preview a[data-wikilink] {
color: var(--link-internal, var(--accent-primary));
text-decoration: none;
2025-11-05 16:48:41 +00:00
font-weight: 500;
2025-12-03 15:00:25 +00:00
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;
2025-11-05 16:48:41 +00:00
}
2025-12-03 15:00:25 +00:00
/* Broken wikilinks - note doesn't exist (Obsidian shows these dimmed) */
.markdown-preview a[data-wikilink].wikilink-broken {
color: var(--link-broken, var(--text-tertiary));
2025-11-05 16:48:41 +00:00
opacity: 0.7;
}
2025-12-03 15:00:25 +00:00
.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;
}
2025-12-03 15:00:25 +00:00
2025-12-04 09:07:07 +00:00
/* 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;
}
2025-12-04 17:05:41 +00:00
/* 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%;
}
2025-12-03 15:00:25 +00:00
/* 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);
}
2025-11-05 16:48:41 +00:00
/* 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;
}
2026-06-01 13:50:27 +00:00
.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; }
2025-11-05 16:48:41 +00:00
.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;
}
2026-01-18 15:00:01 +00:00
/* 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) */
2026-01-18 15:00:01 +00:00
.markdown-preview .media-pdf {
border: 1px solid var(--border-primary);
border-radius: 0.5rem;
overflow: hidden;
background: var(--bg-secondary);
2026-01-18 15:00:01 +00:00
}
.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);
2026-01-18 15:00:01 +00:00
}
2025-11-05 16:48:41 +00:00
/* Task lists */
.markdown-preview input[type="checkbox"] {
margin-right: 0.5rem;
}
2025-11-11 13:51:55 +00:00
/* Math/LaTeX styling */
.markdown-preview .MathJax {
color: var(--text-primary);
overflow-x: auto;
overflow-y: hidden;
}
/* Display math (block) */
.markdown-preview mjx-container[display="true"] {
margin: 1.5rem 0;
overflow-x: auto;
}
/* Inline math */
.markdown-preview mjx-container[display="false"] {
margin: 0 0.2rem;
}
2025-11-17 16:29:00 +00:00
/* Mermaid diagram styling - minimal, let Mermaid handle its own rendering */
.markdown-preview .mermaid-rendered svg {
max-width: 100%;
height: auto;
}
2025-11-05 16:48:41 +00:00
/* Editor styling */
2025-12-13 11:24:43 +00:00
/* 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;
2025-11-05 16:48:41 +00:00
tab-size: 2;
2025-12-13 11:24:43 +00:00
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 {
2025-11-05 16:48:41 +00:00
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
2025-12-13 11:24:43 +00:00
resize: none;
2026-03-10 09:47:51 +00:00
overflow-y: scroll; /* Always show scrollbar for consistent text width */
overflow-x: auto;
2025-11-05 16:48:41 +00:00
}
/* Syntax Highlighting Overlay */
.editor-wrapper {
position: relative;
flex: 1;
display: flex;
min-height: 0;
}
.editor-wrapper .editor-textarea {
position: relative;
z-index: 1;
2025-12-13 11:24:43 +00:00
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;
2026-03-10 09:47:51 +00:00
overflow-y: scroll; /* Match textarea scrollbar space */
2026-03-10 10:36:05 +00:00
overflow-x: auto; /* Match textarea for horizontal scroll sync */
color: var(--text-primary);
background-color: var(--bg-primary);
z-index: 0;
2025-12-14 11:19:49 +00:00
}
2026-03-11 15:26:16 +00:00
/* Invisible scrollbar that takes EXACT same space as textarea */
2025-12-14 11:19:49 +00:00
.syntax-overlay::-webkit-scrollbar {
2026-03-10 09:47:51 +00:00
width: 12px;
2026-03-11 15:26:16 +00:00
height: 12px;
background: transparent;
}
2026-03-11 15:26:16 +00:00
.syntax-overlay::-webkit-scrollbar-track {
background: var(--bg-tertiary); /* MUST match textarea track */
}
.syntax-overlay::-webkit-scrollbar-thumb {
background: transparent;
2026-03-11 15:26:16 +00:00
border-radius: 6px;
border: 2px solid transparent; /* MUST match textarea's 2px border */
}
2026-03-11 15:26:16 +00:00
/* Firefox - MUST match textarea's scrollbar-width: thin */
.syntax-overlay {
scrollbar-width: thin;
scrollbar-color: transparent var(--bg-tertiary);
}
2025-12-12 16:18:14 +00:00
/* 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);
}
2025-12-13 11:24:43 +00:00
.zen-mode-active .zen-editor-container .editor-textarea,
2025-12-12 16:18:14 +00:00
.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; }
}
2026-05-07 09:15:36 +00:00
/* Syntax highlight colors (color-only — see issue #212) */
2026-05-06 16:51:01 +00:00
.syntax-overlay .md-heading { color: var(--accent-primary); }
.syntax-overlay .md-bold { color: var(--syntax-bold, #fbbf24); }
2026-05-07 09:15:36 +00:00
.syntax-overlay .md-italic { color: var(--syntax-italic, #a78bfa); }
2026-05-06 16:51:01 +00:00
.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)); }
2026-05-06 13:34:16 +00:00
.syntax-overlay .md-link-url { color: var(--text-tertiary); }
2026-05-06 16:51:01 +00:00
.syntax-overlay .md-list { color: var(--accent-primary); }
.syntax-overlay .md-blockquote{ color: var(--text-tertiary); }
2026-05-06 13:34:16 +00:00
.syntax-overlay .md-hr { color: var(--border-primary); }
2026-05-06 16:51:01 +00:00
.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); }
2025-11-05 16:48:41 +00:00
/* Link drop zone cursor */
.editor-textarea.link-drop-target {
cursor: copy !important;
}
2025-12-05 13:31:52 +00:00
/* Folder tree styling - compact Obsidian-like */
2025-11-05 16:48:41 +00:00
.folder-item {
user-select: none;
2026-02-19 14:23:46 +00:00
border-radius: 6px;
border-left: 3px solid transparent;
margin-left: 2px;
2025-11-05 16:48:41 +00:00
}
.folder-item:hover {
background-color: var(--bg-hover);
2026-02-19 14:23:46 +00:00
border-left-color: var(--accent-primary);
2025-11-05 16:48:41 +00:00
}
2025-12-09 11:08:12 +00:00
.folder-item.drag-over {
background-color: var(--accent-light) !important;
border: 2px dashed var(--accent-primary) !important;
}
2025-11-05 16:48:41 +00:00
.folder-expanded {
font-weight: 600;
}
.note-item {
2026-02-19 14:23:46 +00:00
border-radius: 6px;
border-left: 3px solid transparent;
margin-left: 2px;
2025-12-05 13:31:52 +00:00
}
.note-item:hover {
background-color: var(--bg-hover);
2026-02-19 14:23:46 +00:00
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);
}
2026-02-19 14:23:46 +00:00
/* 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);
2025-11-18 09:03:47 +00:00
}
/* 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;
2026-02-19 14:23:46 +00:00
transform: scale(0.98);
2025-11-18 09:03:47 +00:00
}
/* Smooth transitions for drag states */
.folder-item,
.note-item {
2026-02-19 14:23:46 +00:00
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
2025-11-18 09:03:47 +00:00
}
/* Better drop zone visibility */
.folder-item:has(+ .folder-contents) {
position: relative;
2025-11-05 16:48:41 +00:00
}
.hover-buttons {
transition: opacity 0.2s;
}
.folder-item:hover .hover-buttons {
opacity: 1 !important;
pointer-events: auto !important;
}
/* Note delete button on hover */
.note-item:hover .note-delete-btn {
opacity: 1 !important;
}
/* Folder hierarchy lines (Obsidian-style) */
.folder-contents {
position: relative;
padding-left: 1rem;
margin-left: 0.5rem;
}
.folder-contents::before {
content: '';
position: absolute;
left: 2px;
2025-11-05 16:48:41 +00:00
top: 0;
bottom: 0;
width: 1px;
background-color: var(--text-tertiary);
2025-11-05 16:48:41 +00:00
opacity: 0.4;
}
2025-12-06 17:06:07 +00:00
/* 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;
}
2025-12-18 13:16:04 +00:00
/* 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);
}
2025-12-06 17:06:07 +00:00
.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;
2026-05-29 10:30:23 +00:00
transition: flex-basis 180ms ease, width 180ms ease, opacity 120ms ease;
2025-12-06 17:06:07 +00:00
}
2026-05-29 10:30:23 +00:00
/* 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;
}
2025-12-06 17:06:07 +00:00
.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;
2025-12-06 17:40:28 +00:00
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);
2025-12-06 17:06:07 +00:00
}
.mobile-bottom-tab.active {
color: var(--accent-primary);
}
.mobile-bottom-tab svg {
width: 22px;
height: 22px;
}
.mobile-bottom-tab span {
font-size: 10px;
}
2026-01-19 10:01:19 +00:00
/* Zen mode mobile adjustments - use 95% width instead of fixed padding */
.zen-mode-active .zen-editor-container {
padding: 1rem 2.5%;
width: 100%;
}
2025-12-06 17:06:07 +00:00
}
@media (min-width: 769px) {
.mobile-bottom-tabs {
display: none !important;
}
}
2025-11-05 16:48:41 +00:00
/* Scrollbar styling */
.custom-scrollbar::-webkit-scrollbar,
.editor-textarea::-webkit-scrollbar,
.markdown-preview::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.custom-scrollbar::-webkit-scrollbar-track,
.editor-textarea::-webkit-scrollbar-track,
.markdown-preview::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
.custom-scrollbar::-webkit-scrollbar-thumb,
.editor-textarea::-webkit-scrollbar-thumb,
.markdown-preview::-webkit-scrollbar-thumb {
background: var(--border-secondary);
border-radius: 6px;
border: 2px solid var(--bg-tertiary);
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover,
.editor-textarea::-webkit-scrollbar-thumb:hover,
.markdown-preview::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Firefox scrollbar */
.custom-scrollbar,
.editor-textarea,
.markdown-preview {
scrollbar-width: thin;
scrollbar-color: var(--border-secondary) var(--bg-tertiary);
}
/* Resize handle */
.resize-handle {
position: relative;
user-select: none;
flex-shrink: 0;
}
.resize-handle:hover {
background-color: var(--accent-primary) !important;
}
.resize-handle:active {
background-color: var(--accent-hover) !important;
}
/* Split view resize handle */
.split-resize-handle {
position: relative;
user-select: none;
flex-shrink: 0;
}
.split-resize-handle:hover {
background-color: var(--accent-primary) !important;
}
.split-resize-handle:active {
background-color: var(--accent-hover) !important;
}
2025-11-05 16:48:41 +00:00
/* Enhanced Shell/Bash Syntax Highlighting */
.markdown-preview pre code.language-shell .hljs-meta,
.markdown-preview pre code.language-bash .hljs-meta,
.markdown-preview pre code.language-sh .hljs-meta {
color: #7c3aed !important; /* Purple for prompts like $ # */
font-weight: 600;
}
.markdown-preview pre code.language-shell .hljs-built_in,
.markdown-preview pre code.language-bash .hljs-built_in,
.markdown-preview pre code.language-sh .hljs-built_in {
color: #10b981 !important; /* Green for built-in commands */
font-weight: 500;
}
.markdown-preview pre code.language-shell .hljs-string,
.markdown-preview pre code.language-bash .hljs-string,
.markdown-preview pre code.language-sh .hljs-string {
color: #f59e0b !important; /* Orange for strings */
}
.markdown-preview pre code.language-shell .hljs-variable,
.markdown-preview pre code.language-bash .hljs-variable,
.markdown-preview pre code.language-sh .hljs-variable {
color: #06b6d4 !important; /* Cyan for variables */
font-weight: 500;
}
.markdown-preview pre code.language-shell .hljs-comment,
.markdown-preview pre code.language-bash .hljs-comment,
.markdown-preview pre code.language-sh .hljs-comment {
color: #6b7280 !important; /* Gray for comments */
font-style: italic;
}
.markdown-preview pre code.language-shell .hljs-keyword,
.markdown-preview pre code.language-bash .hljs-keyword,
.markdown-preview pre code.language-sh .hljs-keyword {
color: #ec4899 !important; /* Pink for keywords like if, then, else */
font-weight: 600;
}
.markdown-preview pre code.language-shell .hljs-number,
.markdown-preview pre code.language-bash .hljs-number,
.markdown-preview pre code.language-sh .hljs-number {
color: #8b5cf6 !important; /* Purple for numbers */
}
.markdown-preview pre code.language-shell .hljs-literal,
.markdown-preview pre code.language-bash .hljs-literal,
.markdown-preview pre code.language-sh .hljs-literal {
color: #3b82f6 !important; /* Blue for literals like true, false */
font-weight: 500;
}
/* Enhanced PowerShell Syntax Highlighting */
.markdown-preview pre code.language-powershell .hljs-built_in,
.markdown-preview pre code.language-ps1 .hljs-built_in,
.markdown-preview pre code.language-ps .hljs-built_in {
color: #10b981 !important; /* Green for cmdlets like Get-Process, Set-Location */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-literal,
.markdown-preview pre code.language-ps1 .hljs-literal,
.markdown-preview pre code.language-ps .hljs-literal {
color: #3b82f6 !important; /* Blue for $true, $false, $null */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-variable,
.markdown-preview pre code.language-ps1 .hljs-variable,
.markdown-preview pre code.language-ps .hljs-variable {
color: #06b6d4 !important; /* Cyan for $variables */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-string,
.markdown-preview pre code.language-ps1 .hljs-string,
.markdown-preview pre code.language-ps .hljs-string {
color: #f59e0b !important; /* Orange for strings */
}
.markdown-preview pre code.language-powershell .hljs-keyword,
.markdown-preview pre code.language-ps1 .hljs-keyword,
.markdown-preview pre code.language-ps .hljs-keyword {
color: #ec4899 !important; /* Pink for keywords like if, else, foreach */
font-weight: 600;
}
.markdown-preview pre code.language-powershell .hljs-params,
.markdown-preview pre code.language-ps1 .hljs-params,
.markdown-preview pre code.language-ps .hljs-params {
color: #8b5cf6 !important; /* Purple for -Parameters */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-type,
.markdown-preview pre code.language-ps1 .hljs-type,
.markdown-preview pre code.language-ps .hljs-type {
color: #14b8a6 !important; /* Teal for [Types] */
font-weight: 500;
}
.markdown-preview pre code.language-powershell .hljs-comment,
.markdown-preview pre code.language-ps1 .hljs-comment,
.markdown-preview pre code.language-ps .hljs-comment {
color: #6b7280 !important; /* Gray for comments */
font-style: italic;
}
.markdown-preview pre code.language-powershell .hljs-number,
.markdown-preview pre code.language-ps1 .hljs-number,
.markdown-preview pre code.language-ps .hljs-number {
color: #8b5cf6 !important; /* Purple for numbers */
}
.markdown-preview pre code.language-powershell .hljs-operator,
.markdown-preview pre code.language-ps1 .hljs-operator,
.markdown-preview pre code.language-ps .hljs-operator {
color: #f472b6 !important; /* Pink for operators */
}
2025-11-13 14:09:26 +00:00
/* Mobile Responsive Styles */
2025-11-26 10:40:20 +00:00
/* Z-index hierarchy (for reference):
- Mobile sidebar: 1000
- Mobile overlay: 999
- Template modal: 1100 (must be above sidebar)
- Dropdown menu: 50 (below everything)
*/
2025-11-13 14:09:26 +00:00
@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 */
2025-11-13 14:09:26 +00:00
width: 80% !important;
max-width: 300px !important;
z-index: 1000;
transition: left 0.3s ease;
}
/* Show sidebar when open */
.mobile-sidebar-open {
left: 0;
}
2026-05-29 10:30:23 +00:00
/* 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;
}
2025-11-13 14:09:26 +00:00
/* 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));
}
2025-11-13 14:09:26 +00:00
/* 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 */
2025-11-13 14:09:26 +00:00
.note-item,
.folder-item {
padding: 0.5rem 0.5rem !important;
min-height: 36px;
2025-11-13 14:09:26 +00:00
}
}
/* 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;
}
}
2025-11-05 16:48:41 +00:00
</style>
</head>
2025-12-12 16:18:14 +00:00
<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')"
2025-12-12 16:18:14 +00:00
>
<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>
2025-11-05 16:48:41 +00:00
<!-- 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>
2025-11-13 14:09:26 +00:00
<!-- Mobile Overlay -->
<div
@click="mobileSidebarOpen = false"
:class="{'mobile-overlay': true, 'mobile-overlay-visible': mobileSidebarOpen}"
x-show="mobileSidebarOpen"
></div>
2025-11-05 16:48:41 +00:00
<!-- Main Container -->
<div class="flex h-screen overflow-hidden">
2025-12-06 17:06:07 +00:00
<!-- Sidebar with Icon Rail -->
2025-11-13 14:09:26 +00:00
<div
2025-12-12 16:18:14 +00:00
class="flex mobile-sidebar zen-hide"
2025-11-13 14:09:26 +00:00
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
2026-05-29 10:30:23 +00:00
: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;'"
2025-11-13 14:09:26 +00:00
>
2025-12-06 17:06:07 +00:00
<!-- Icon Rail -->
<div class="icon-rail">
<!-- Logo -->
<img
src="/static/logo.svg"
alt="NoteDiscovery"
class="icon-rail-logo"
2025-11-24 09:04:20 +00:00
@click="goHome()"
:title="t('sidebar.go_to_homepage')"
>
2025-12-06 17:06:07 +00:00
<!-- Files -->
<button
2025-12-06 17:06:07 +00:00
class="icon-rail-btn"
:class="{'active': activePanel === 'files'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('files')"
:title="t('sidebar.files')"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.files')"
>
2025-12-06 17:06:07 +00:00
<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>
2025-12-06 17:06:07 +00:00
<!-- Search -->
<button
2025-12-06 17:06:07 +00:00
class="icon-rail-btn relative"
:class="{'active': activePanel === 'search'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('search')"
:title="t('sidebar.search')"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.search')"
2025-12-06 17:06:07 +00:00
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
2025-11-05 16:48:41 +00:00
<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>
2025-12-06 17:06:07 +00:00
<!-- Search results badge -->
<span
x-show="searchQuery.trim() && searchResults.length > 0"
2025-12-06 17:40:28 +00:00
x-cloak
2025-12-06 17:06:07 +00:00
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>
2025-11-05 16:48:41 +00:00
2025-12-06 17:06:07 +00:00
<!-- Tags -->
<button
2025-12-06 17:06:07 +00:00
class="icon-rail-btn relative"
:class="{'active': activePanel === 'tags'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('tags')"
:title="t('tags.title')"
2025-12-17 09:15:08 +00:00
:aria-label="t('tags.title')"
2025-11-05 16:48:41 +00:00
>
2025-12-06 17:06:07 +00:00
<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>
2025-12-07 09:41:11 +00:00
<!-- Filtered notes badge (shows result count when tags selected) -->
2025-12-06 17:06:07 +00:00
<span
x-show="selectedTags.length > 0"
2025-12-06 17:40:28 +00:00
x-cloak
2025-12-06 17:06:07 +00:00
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;"
2025-12-07 09:41:11 +00:00
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
2025-12-06 17:06:07 +00:00
></span>
</button>
2025-12-06 17:06:07 +00:00
2025-12-16 15:55:05 +00:00
<!-- Outline (TOC) -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'outline'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('outline')"
2025-12-16 15:55:05 +00:00
:title="t('outline.title')"
2025-12-17 09:15:08 +00:00
:aria-label="t('outline.title')"
2025-12-16 15:55:05 +00:00
>
<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>
2026-03-24 11:02:34 +00:00
<!-- Backlinks -->
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'backlinks'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('backlinks')"
2026-03-24 11:02:34 +00:00
:title="t('backlinks.title')"
:aria-label="t('backlinks.title')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
2026-03-24 11:20:51 +00:00
<!-- 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>
2026-03-24 11:02:34 +00:00
</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'}"
2026-05-29 10:30:23 +00:00
@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>
2025-12-16 15:55:05 +00:00
2025-12-06 17:06:07 +00:00
<div class="icon-rail-spacer"></div>
2026-05-29 10:30:23 +00:00
<!-- 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>
2025-12-06 17:06:07 +00:00
<!-- Graph -->
<button
2025-12-06 17:06:07 +00:00
class="icon-rail-btn"
:class="{'active': showGraph}"
@click="showGraph = true; initGraph()"
:title="t('graph.title')"
2025-12-17 09:15:08 +00:00
:aria-label="t('graph.title')"
2025-11-24 16:11:22 +00:00
>
2025-12-06 17:06:07 +00:00
<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>
2025-12-06 17:06:07 +00:00
<!-- Settings -->
<button
2025-12-06 17:06:07 +00:00
class="icon-rail-btn"
:class="{'active': activePanel === 'settings'}"
2026-05-29 10:30:23 +00:00
@click="openSidebarPanel('settings')"
:title="t('sidebar.settings')"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.settings')"
2025-11-05 16:48:41 +00:00
>
2025-12-06 17:06:07 +00:00
<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>
2025-11-17 13:20:57 +00:00
</svg>
</button>
</div>
2025-12-06 17:06:07 +00:00
<!-- Sidebar Panel -->
2026-05-29 10:30:23 +00:00
<div
class="sidebar-panel"
:class="{'sidebar-panel-collapsed': sidebarPanelCollapsed}"
style="border-right: 1px solid var(--border-primary);"
>
2025-11-05 16:48:41 +00:00
2025-12-06 17:06:07 +00:00
<!-- ==================== 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)"
2025-12-06 17:06:07 +00:00
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)'"
>
2025-12-06 17:06:07 +00:00
<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>
2025-12-06 17:06:07 +00:00
New
</button>
</div>
2025-12-06 17:06:07 +00:00
<!-- 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)"
2026-02-19 14:23:46 +00:00
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);'"
2025-12-15 11:18:14 +00:00
: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>
2026-01-18 17:04:32 +00:00
<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>
2025-11-05 16:48:41 +00:00
<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>
2026-02-19 14:02:50 +00:00
<div class="flex gap-0.5">
2026-04-06 14:00:00 +00:00
<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
2025-11-05 16:48:41 +00:00
@click="expandAllFolders()"
2026-02-19 14:02:50 +00:00
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.expand_all')"
2026-02-19 14:02:50 +00:00
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
>📂</button>
2025-11-05 16:48:41 +00:00
<button
@click="collapseAllFolders()"
2026-02-19 14:02:50 +00:00
class="text-xs px-1.5 py-0.5 rounded transition-all hover:scale-110"
:title="t('sidebar.collapse_all')"
2026-02-19 14:02:50 +00:00
style="opacity: 0.7;"
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.7'"
>📁</button>
2025-11-05 16:48:41 +00:00
</div>
</div>
2025-11-18 09:27:01 +00:00
<!-- Dual-purpose: hint text OR root drop zone when dragging -->
<div
2025-11-18 09:47:23 +00:00
class="text-xs px-2 py-1 mb-2 rounded transition-all font-medium text-center"
:class="{
2026-01-19 08:42:32 +00:00
'border-2 border-dashed bg-accent-light': draggedItem && dragOverFolder === '',
'border-2 border-dashed': draggedItem && dragOverFolder !== '',
'border-2 border-transparent': !draggedItem
2025-11-18 09:47:23 +00:00
}"
:style="{
2026-01-19 08:42:32 +00:00
'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)',
2025-11-18 09:47:23 +00:00
'background-color': dragOverFolder === '' ? 'var(--accent-light)' : ''
}"
2026-01-19 08:42:32 +00:00
@dragover.prevent="if(draggedItem) dragOverFolder = '';"
@dragenter.prevent="if(draggedItem) dragOverFolder = '';"
@dragleave="if(draggedItem) dragOverFolder = null;"
@drop="if(draggedItem) onFolderDrop('')"
2025-11-18 09:47:23 +00:00
style="height: 28px; line-height: 20px; display: flex; align-items: center; justify-content: center;"
2025-11-18 09:27:01 +00:00
>
2026-01-19 08:42:32 +00:00
<span x-show="!draggedItem" x-text="t('sidebar.drag_hint')"></span>
<span x-show="draggedItem" x-text="t('sidebar.root_folder')"></span>
2025-11-05 16:48:41 +00:00
</div>
</div>
2025-11-08 11:23:07 +00:00
<!-- Folders (rendered recursively for unlimited nesting) -->
2026-04-06 14:00:00 +00:00
<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">
2025-11-08 11:28:31 +00:00
<div x-html="renderFolderRecursive(folder, 0, true)"></div>
2025-11-05 16:48:41 +00:00
</template>
2026-04-06 14:00:00 +00:00
<!-- Root notes and images (no folder) -->
2026-01-19 09:14:37 +00:00
<div x-html="renderRootItems()"></div>
2025-11-05 16:48:41 +00:00
<!-- 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);">
2025-12-15 14:45:10 +00:00
<span x-text="t('sidebar.no_notes_yet')"></span><br>
<span x-text="t('sidebar.create_first')"></span>
2025-11-05 16:48:41 +00:00
</div>
</template>
</div>
2025-12-06 17:06:07 +00:00
</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>
2025-12-06 17:06:07 +00:00
</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"
2026-02-20 07:26:52 +00:00
@input="debouncedSearchNotes()"
:placeholder="t('sidebar.search_placeholder')"
2025-12-06 17:06:07 +00:00
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')"
2025-12-06 17:06:07 +00:00
>
<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);"
>
2025-12-17 10:43:01 +00:00
<span x-text="t('search.match_of', { current: currentMatchIndex + 1, total: totalMatches })"></span>
2025-12-06 17:06:07 +00:00
<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')">
2025-12-06 17:06:07 +00:00
<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')">
2025-12-06 17:06:07 +00:00
<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);">
2025-12-17 10:43:01 +00:00
<span x-text="searchResults.length + ' ' + t(searchResults.length === 1 ? 'search.result_singular' : 'search.result_plural')"></span>
2025-12-06 17:06:07 +00:00
</div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type, searchQuery)"
2026-02-19 14:23:46 +00:00
class="hover-accent px-2 py-1.5 text-sm cursor-pointer mb-1"
:class="{'active': currentNote === note.path}"
2025-12-06 17:06:07 +00:00
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
>
2026-01-18 17:04:32 +00:00
<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>
2025-12-06 17:06:07 +00:00
</template>
2026-02-20 07:26:52 +00:00
<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">
2025-12-06 17:06:07 +00:00
<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>
2026-01-22 16:23:41 +00:00
<p class="text-sm" style="color: var(--text-tertiary);" x-text="t('search.no_results', {query: searchQuery})"></p>
2025-12-06 17:06:07 +00:00
</div>
2025-12-04 17:05:41 +00:00
</template>
2025-12-06 17:06:07 +00:00
<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>
2025-12-06 17:06:07 +00:00
</div>
</template>
</div>
2025-12-04 17:05:41 +00:00
</div>
2025-12-06 17:06:07 +00:00
<!-- END SEARCH PANEL -->
2025-12-06 17:06:07 +00:00
<!-- ==================== 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>
2025-12-06 17:06:07 +00:00
<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')"
2025-12-06 17:06:07 +00:00
>
<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>
2025-12-06 17:40:28 +00:00
<!-- 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)"
2026-02-19 14:23:46 +00:00
class="tag-chip px-2 py-1 text-xs rounded-full"
2025-12-06 17:40:28 +00:00
: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})"
2025-12-06 17:40:28 +00:00
>
<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>
2025-12-17 15:05:01 +00:00
<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>
2025-12-06 17:40:28 +00:00
</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>)
2025-12-06 17:40:28 +00:00
</div>
<div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type)"
2026-02-19 14:23:46 +00:00
class="hover-accent px-3 py-1.5 text-sm cursor-pointer"
:class="{'active': currentNote === note.path}"
2025-12-06 17:40:28 +00:00
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
>
2026-01-18 17:04:32 +00:00
<span class="truncate block" x-text="note.name" :title="note.name"></span>
2025-12-06 17:40:28 +00:00
</div>
</template>
2025-12-17 15:30:11 +00:00
<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')">
2025-12-06 17:06:07 +00:00
</div>
</div>
</div>
2025-12-06 17:40:28 +00:00
</template>
</div>
2025-11-05 16:48:41 +00:00
</div>
2025-12-06 17:06:07 +00:00
<!-- END TAGS PANEL -->
2025-12-16 15:55:05 +00:00
<!-- ==================== 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)"
2026-02-19 14:23:46 +00:00
class="hover-accent w-full text-left px-3 py-1.5 text-sm"
2025-12-16 15:55:05 +00:00
: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"
2026-01-18 17:04:32 +00:00
:title="heading.text"
2025-12-16 15:55:05 +00:00
></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 -->
2026-03-24 11:02:34 +00:00
<!-- ==================== 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 -->
2025-12-06 17:06:07 +00:00
<!-- ==================== SETTINGS PANEL ==================== -->
2025-12-06 17:40:28 +00:00
<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>
2025-12-06 17:06:07 +00:00
</div>
2025-12-06 18:00:13 +00:00
<!-- 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>
2025-12-06 18:00:13 +00:00
<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>
2025-12-06 18:00:13 +00:00
</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">
2026-06-04 12:11:50 +00:00
<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()"
2026-06-04 12:11:50 +00:00
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>
2025-12-16 10:27:54 +00:00
<p class="text-xs mt-1" style="color: var(--text-tertiary);" x-text="t('syntax_highlight.description')"></p>
</div>
2026-01-26 09:22:00 +00:00
<!-- Readable Line Length Toggle -->
<div class="mb-4">
2026-06-04 12:11:50 +00:00
<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>
2026-01-26 09:22:00 +00:00
<div
@click="toggleReadableLineLength()"
2026-06-04 12:11:50 +00:00
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
2026-01-26 09:22:00 +00:00
: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">
2026-06-04 12:11:50 +00:00
<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()"
2026-06-04 12:11:50 +00:00
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>
2026-04-06 09:25:21 +00:00
<!-- Tab inserts tab -->
<div class="mb-4">
2026-06-04 12:11:50 +00:00
<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>
2026-04-06 09:25:21 +00:00
<div
@click="toggleTabInsertsTab()"
2026-06-04 12:11:50 +00:00
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
2026-04-06 09:25:21 +00:00
: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>
2026-06-04 11:21:49 +00:00
<!-- + / New button action (issue #217). Lets the user skip the
type-chooser dropdown when they almost always create the same
kind of thing. Persisted in LOCAL_SETTINGS.newButtonAction. -->
<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>
<!-- Auto-fill new-note title with timestamp (issue #217). Only
applies to plain-note creation (folder/template have their
own naming flows). Persisted as autoFillNoteTitle. -->
<div class="mb-4">
2026-06-04 12:11:50 +00:00
<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>
2026-06-04 11:21:49 +00:00
<div
@click="autoFillNoteTitle = !autoFillNoteTitle; localStorage.setItem('autoFillNoteTitle', autoFillNoteTitle)"
2026-06-04 12:11:50 +00:00
class="relative w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0"
2026-06-04 11:21:49 +00:00
: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>
2025-12-06 18:00:13 +00:00
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
<div x-show="authEnabled" class="mb-4">
2025-12-16 10:27:54 +00:00
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);" x-text="t('settings.account')"></label>
2025-12-06 18:00:13 +00:00
<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)'"
2025-12-16 10:27:54 +00:00
x-text="'🔒 ' + t('settings.logout')"
2025-12-06 18:00:13 +00:00
>
</a>
</div>
2025-12-06 17:40:28 +00:00
</div>
2025-12-06 18:00:13 +00:00
<!-- 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);">
2025-12-16 10:27:54 +00:00
<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>
2025-12-06 18:00:13 +00:00
</div>
<a
href="https://www.notediscovery.com"
target="_blank"
rel="noopener noreferrer"
2025-12-06 18:00:13 +00:00
class="text-xs hover:underline"
style="color: var(--accent-primary);"
>
2025-12-06 18:00:13 +00:00
notediscovery.com
2025-12-17 09:32:02 +00:00
</a>
2026-01-22 09:43:47 +00:00
<!-- Ko-fi support link (hidden if ALREADY_DONATED=true) -->
<template x-if="!alreadyDonated">
2026-01-16 11:16:34 +00:00
<div class="mt-3 flex flex-col gap-2 items-center">
2025-12-17 09:32:02 +00:00
<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>
2026-01-16 10:24:09 +00:00
<!-- PikaPods deploy button -->
<a
href="https://www.pikapods.com/pods?run=notediscovery"
target="_blank"
rel="noopener noreferrer"
2026-01-16 11:16:34 +00:00
class="inline-flex items-center transition-all hover:scale-105"
2026-01-16 10:24:09 +00:00
>
2026-01-16 11:16:34 +00:00
<img src="https://www.pikapods.com/static/run-button.svg" alt="Run on PikaPods" style="height: 28px;">
2026-01-16 10:24:09 +00:00
</a>
2026-01-22 10:08:12 +00:00
<!-- 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>
2025-12-17 09:32:02 +00:00
</div>
</template>
2026-01-22 10:27:06 +00:00
<!-- 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>
2025-12-06 17:06:07 +00:00
</div>
2025-12-06 17:40:28 +00:00
</template>
2025-12-06 17:06:07 +00:00
<!-- END SETTINGS PANEL -->
2025-11-05 16:48:41 +00:00
</div>
2025-12-06 17:06:07 +00:00
<!-- END Sidebar Panel -->
2025-11-05 16:48:41 +00:00
</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'"
2026-05-29 10:30:23 +00:00
x-show="!sidebarPanelCollapsed"
2025-11-05 16:48:41 +00:00
></div>
<!-- Main Content Area -->
2025-12-04 17:05:41 +00:00
<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>
2025-11-05 16:48:41 +00:00
2026-01-18 15:00:01 +00:00
<template x-if="!currentNote && !currentMedia">
<!-- Notes Homepage -->
<div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);">
2025-11-26 10:40:20 +00:00
<!-- 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')"
2025-11-26 10:40:20 +00:00
>
<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">
2025-11-27 10:17:06 +00:00
<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">
2025-11-24 09:04:20 +00:00
<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>
2025-12-16 10:27:54 +00:00
<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>
2025-11-24 09:04:20 +00:00
<button
2025-11-27 10:17:06 +00:00
@click="dropdownTargetFolder = selectedHomepageFolder; toggleNewDropdown($event)"
class="px-6 py-3 text-sm font-medium text-white rounded-lg transition-colors"
2025-11-24 09:04:20 +00:00
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>
2025-11-24 09:04:20 +00:00
</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>
2025-12-15 14:45:10 +00:00
<p class="text-lg" style="color: var(--text-secondary);" x-text="t('app.tagline')"></p>
<!-- Demo Mode Warning Banner -->
2026-01-14 08:31:25 +00:00
<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>
2026-01-14 08:31:25 +00:00
<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>
2025-11-24 09:04:20 +00:00
</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>
2025-12-15 14:45:10 +00:00
<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>
2025-12-15 14:45:10 +00:00
<span x-text="homepageFolders().length === 1 ? t('homepage.folder_singular') : t('homepage.folder_plural')"></span>
</span>
<button
2025-11-27 10:17:06 +00:00
@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
2025-11-27 10:17:06 +00:00
@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;"
2025-11-27 10:30:49 +00:00
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';"
>
2025-11-27 10:30:49 +00:00
<!-- Delete Button -->
<button
@click.stop="deleteFolder(folder.path, folder.name)"
class="card-delete-btn hidden sm:block"
2025-11-27 10:30:49 +00:00
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')"
2025-11-27 10:30:49 +00:00
>
<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);">
2025-12-15 14:45:10 +00:00
<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 -->
2025-11-24 09:04:20 +00:00
<template x-for="note in homepageNotes().slice(0, HOMEPAGE_MAX_NOTES)" :key="note.path">
<div
2025-12-04 17:05:41 +00:00
@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;"
2025-11-27 10:30:49 +00:00
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';"
>
2025-11-27 10:30:49 +00:00
<!-- Delete Button -->
<button
2026-01-18 15:00:01 +00:00
@click.stop="note.type !== 'note' ? deleteMedia(note.path) : deleteNote(note.path, note.name)"
class="card-delete-btn hidden sm:block"
2025-11-27 10:30:49 +00:00
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)'"
2026-04-21 13:22:09 +00:00
:title="note.type === 'note' ? t('toolbar.delete_note') : (note.type === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image'))"
2025-11-27 10:30:49 +00:00
>
<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 -->
2025-11-24 09:04:20 +00:00
<div x-show="homepageNotes().length > HOMEPAGE_MAX_NOTES" class="mt-6 text-center">
<p class="text-sm" style="color: var(--text-secondary);">
2025-11-24 09:04:20 +00:00
Showing <span x-text="HOMEPAGE_MAX_NOTES"></span> of <span x-text="homepageNotes().length"></span> notes in this folder.
</p>
</div>
</div>
</template>
2025-11-05 16:48:41 +00:00
</div>
</template>
2026-01-18 15:00:01 +00:00
<template x-if="currentNote || currentMedia">
2025-11-05 16:48:41 +00:00
<!-- Editor Area -->
2025-12-12 16:18:14 +00:00
<div class="flex-1 flex flex-col overflow-hidden" :class="{'zen-editor-container': zenMode}" style="background-color: var(--bg-primary);">
2025-11-05 16:48:41 +00:00
<!-- Toolbar -->
2025-12-12 16:18:14 +00:00
<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);">
2025-11-05 16:48:41 +00:00
<div class="flex items-center space-x-2">
2025-11-13 14:09:26 +00:00
<!-- Mobile Menu Button -->
<button
@click="mobileSidebarOpen = !mobileSidebarOpen"
class="mobile-menu-button p-2 rounded-lg"
style="color: var(--text-primary); display: none;"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
:title="t('sidebar.toggle_sidebar')"
2025-11-13 14:09:26 +00:00
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
2025-11-05 16:48:41 +00:00
<input
type="text"
2026-01-18 15:00:01 +00:00
:value="currentNote ? currentNoteName : (currentMedia ? currentMedia.split('/').pop() : '')"
2025-11-20 13:10:17 +00:00
@input="if (currentNote) currentNoteName = $event.target.value"
@blur="if (currentNote) renameNote()"
:readonly="!currentNote"
2025-11-05 16:48:41 +00:00
class="text-xl font-semibold border-none focus:outline-none focus:ring-2 rounded px-3 py-1.5"
2025-11-20 13:10:17 +00:00
: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;' : '')"
2025-11-05 16:48:41 +00:00
>
2026-04-21 13:22:09 +00:00
<!-- Undo Button (note history or drawing strokes when viewing a drawing) -->
2025-11-05 16:48:41 +00:00
<button
2026-04-21 13:22:09 +00:00
@click="currentMediaType === 'drawing' ? drawingUndo() : undo()"
:disabled="currentMediaType === 'drawing' ? drawingOps.length === 0 : undoHistory.length <= 1"
2025-11-05 16:48:41 +00:00
class="p-2 rounded-lg"
2026-04-21 13:22:09 +00:00
:style="(currentMediaType === 'drawing' ? drawingOps.length === 0 : undoHistory.length <= 1) ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
2025-11-05 16:48:41 +00:00
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
:title="t('toolbar.undo')"
2025-11-05 16:48:41 +00:00
>
<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
2026-04-21 13:22:09 +00:00
@click="currentMediaType === 'drawing' ? drawingRedo() : redo()"
:disabled="currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : redoHistory.length === 0"
2025-11-05 16:48:41 +00:00
class="p-2 rounded-lg"
2026-04-21 13:22:09 +00:00
:style="(currentMediaType === 'drawing' ? drawingRedoStack.length === 0 : redoHistory.length === 0) ? 'color: var(--text-tertiary); opacity: 0.4; cursor: not-allowed;' : 'color: var(--text-secondary);'"
2025-11-05 16:48:41 +00:00
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
:title="t('toolbar.redo')"
2025-11-05 16:48:41 +00:00
>
<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
2026-01-18 15:00:01 +00:00
@click="currentMedia ? deleteMedia(currentMedia) : deleteCurrentNote()"
2025-11-05 16:48:41 +00:00
class="p-2 rounded-lg"
style="color: var(--error);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='transparent'"
2026-04-21 13:22:09 +00:00
:title="currentMedia ? (currentMediaType === 'drawing' ? t('toolbar.delete_drawing') : t('toolbar.delete_image')) : t('toolbar.delete_note')"
2025-11-05 16:48:41 +00:00
>
<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>
2025-11-05 16:48:41 +00:00
</div>
<!-- Demo Mode Warning Banner -->
2026-01-14 08:31:25 +00:00
<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>
2026-01-14 08:31:25 +00:00
<span><strong x-text="t('demo.title') + ':'"></strong> <span x-text="t('demo.warning')"></span></span>
</div>
2025-11-20 13:10:17 +00:00
<div class="flex items-center space-x-2" x-show="currentNote">
<!-- View Toggle (only for notes) -->
2025-11-05 16:48:41 +00:00
<div class="flex rounded-lg p-1" style="background-color: var(--bg-tertiary);">
<button
@click="viewMode = 'edit'"
2025-11-13 14:09:26 +00:00
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'edit' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
2025-12-12 15:44:11 +00:00
@mouseenter="if (viewMode !== 'edit') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'edit') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_edit')"
2025-11-05 16:48:41 +00:00
>
</button>
<button
@click="viewMode = 'split'"
2025-11-13 14:09:26 +00:00
class="mobile-hide-split px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'split' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
2025-12-12 15:44:11 +00:00
@mouseenter="if (viewMode !== 'split') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'split') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_split')"
2025-11-05 16:48:41 +00:00
>
</button>
<button
@click="viewMode = 'preview'"
2025-11-13 14:09:26 +00:00
class="px-2 md:px-3 py-1 text-xs md:text-sm rounded transition"
2025-11-05 16:48:41 +00:00
:style="viewMode === 'preview' ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
2025-12-12 15:44:11 +00:00
@mouseenter="if (viewMode !== 'preview') $el.style.backgroundColor = 'var(--bg-secondary)'"
@mouseleave="if (viewMode !== 'preview') $el.style.backgroundColor = 'transparent'"
x-text="t('editor.mode_preview')"
2025-11-05 16:48:41 +00:00
>
</button>
</div>
2025-11-12 10:16:12 +00:00
2025-12-12 15:42:14 +00:00
<!-- Note Actions Toolbar -->
<div
2025-12-12 15:32:29 +00:00
x-show="currentNote"
2025-12-12 15:42:14 +00:00
class="flex items-center rounded overflow-hidden"
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
2025-12-12 15:32:29 +00:00
>
<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>
2025-12-12 15:42:14 +00:00
<button
@click="exportToHTML()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('toolbar.export_html')"
2025-12-12 15:42:14 +00:00
@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>
2026-04-06 10:58:38 +00:00
</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>
2025-12-12 15:42:14 +00:00
</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')"
2025-12-12 15:42:14 +00:00
: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>
2025-12-12 16:18:14 +00:00
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
2026-01-15 09:07:51 +00:00
<!-- 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>
2025-12-12 16:18:14 +00:00
<button
@click="toggleZenMode()"
class="p-2 transition"
style="color: var(--text-secondary);"
:title="t('zen_mode.tooltip')"
2025-12-12 16:18:14 +00:00
@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>
2025-12-12 15:42:14 +00:00
</div>
2025-11-05 16:48:41 +00:00
</div>
</div>
2026-04-21 13:22:09 +00:00
<!-- 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"
2026-04-21 15:39:22 +00:00
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')"
2026-04-21 13:22:09 +00:00
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
2026-04-21 15:39:22 +00:00
<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" />
2026-04-21 13:22:09 +00:00
</svg>
</button>
2026-05-04 10:33:20 +00:00
<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>
2026-05-07 12:30:55 +00:00
<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>
2026-04-21 15:39:22 +00:00
<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>
2026-04-21 13:22:09 +00:00
</div>
2025-12-04 17:05:41 +00:00
<!-- Editor/Preview/Graph -->
<div class="flex-1 flex relative" style="min-height: 0;">
2025-11-05 16:48:41 +00:00
<!-- Editor -->
<div
2026-01-18 15:00:01 +00:00
x-show="!currentMedia && (viewMode === 'edit' || viewMode === 'split')"
2025-11-05 16:48:41 +00:00
class="flex flex-col"
style="min-height: 0;"
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
2025-11-05 16:48:41 +00:00
>
2025-12-18 13:16:04 +00:00
<!-- 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>
2026-04-06 09:25:21 +00:00
<textarea
id="note-editor"
x-model="noteContent"
@input="autoSave(); updateSyntaxHighlight()"
@scroll="syncOverlayScroll()"
2026-04-12 16:46:35 +00:00
@keydown.enter="handleEditorEnterKey($event)"
2026-04-06 09:25:21 +00:00
@keydown.tab="handleTabKey($event)"
@drop="onEditorDrop($event)"
@dragover.prevent="onEditorDragOver($event)"
@dragenter="onEditorDragEnter($event)"
@dragleave="onEditorDragLeave($event)"
@paste="handlePaste($event)"
2025-12-13 11:24:43 +00:00
: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>
2025-11-05 16:48:41 +00:00
</div>
<!-- Split view resize handle -->
<div
2026-01-18 15:00:01 +00:00
x-show="!currentMedia && viewMode === 'split'"
@mousedown="startSplitResize($event)"
class="split-resize-handle"
2025-11-11 14:50:33 +00:00
style="width: 6px; cursor: col-resize; background: linear-gradient(90deg, transparent 0%, var(--border-secondary) 50%, transparent 100%); transition: all 0.2s; position: relative; z-index: 10; opacity: 0.5;"
@mouseover="$el.style.opacity='1'; $el.style.background='var(--accent-primary)'"
@mouseout="if(!isResizingSplit) { $el.style.opacity='0.5'; $el.style.background='linear-gradient(90deg, transparent 0%, var(--border-secondary) 50%, transparent 100%)' }"
></div>
2025-11-05 16:48:41 +00:00
<!-- Preview -->
<div
2026-01-18 15:00:01 +00:00
x-show="(viewMode === 'preview' || viewMode === 'split') && !currentMedia"
2025-11-05 16:48:41 +00:00
class="overflow-y-auto overflow-x-hidden custom-scrollbar"
style="background-color: var(--bg-primary); min-height: 0;"
:style="viewMode === 'split' ? `width: ${100 - editorWidth}%;` : 'width: 100%;'"
2025-11-05 16:48:41 +00:00
>
2025-12-04 09:07:07 +00:00
<!-- 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);"
>
2026-06-04 12:11:50 +00:00
<span class="text-xs font-medium flex-1 min-w-0" style="color: var(--text-secondary);">
2025-12-04 09:07:07 +00:00
⚙️ 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>
2025-11-05 16:48:41 +00:00
<div
class="p-6 markdown-preview"
2026-01-26 09:22:00 +00:00
:class="{ 'pt-2': getHasMetadata() && currentNote, 'readable-width': readableLineLength }"
2025-11-05 16:48:41 +00:00
x-html="renderedMarkdown"
@click="handleInternalLink($event)"
></div>
</div>
2025-11-20 13:10:17 +00:00
2026-01-18 15:00:01 +00:00
<!-- Media Viewer (images, audio, video, PDF) -->
<template x-if="currentMedia">
2025-11-20 13:10:17 +00:00
<div
2026-04-21 13:22:09 +00:00
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);"
2025-11-20 13:10:17 +00:00
>
2026-01-18 15:00:01 +00:00
<!-- 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>
2026-04-21 13:22:09 +00:00
<!-- 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"
2026-05-07 12:30:55 +00:00
class="flex-1 w-full min-h-0 min-w-0 flex items-center justify-center relative"
2026-04-24 13:29:00 +00:00
style="min-height: 0; background-color: var(--bg-secondary);"
2026-04-21 13:22:09 +00:00
>
<canvas
x-ref="drawingCanvas"
2026-04-24 13:29:00 +00:00
class="drawing-surface block"
2026-04-21 15:39:22 +00:00
:style="drawingTool === 'eyedropper'
? 'touch-action: none; cursor: copy; background: #fff;'
2026-05-04 10:33:20 +00:00
: drawingTool === 'fill'
? 'touch-action: none; cursor: cell; background: #fff;'
2026-05-07 12:30:55 +00:00
: drawingTool === 'text'
? 'touch-action: none; cursor: text; background: #fff;'
: 'touch-action: none; cursor: crosshair; background: #fff;'"
2026-04-21 13:22:09 +00:00
@pointerdown.prevent="drawingPointerDown($event)"
@pointermove.prevent="drawingPointerMove($event)"
@pointerup.prevent="drawingPointerUp($event)"
@pointercancel.prevent="drawingPointerUp($event)"
></canvas>
2026-05-07 12:30:55 +00:00
<!-- 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>
2026-04-21 13:22:09 +00:00
</div>
</div>
</template>
2025-11-20 13:10:17 +00:00
</div>
</template>
2025-11-05 16:48:41 +00:00
</div>
<!-- Stats Status Bar (only shows if plugin enabled) -->
<div x-show="statsPluginEnabled && currentNote && noteStats"
2025-12-12 16:18:14 +00:00
class="border-t zen-hide"
2025-11-05 16:48:41 +00:00
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>
2025-11-05 16:48:41 +00:00
<!-- 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>
2025-11-26 13:38:47 +00:00
<!-- 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>
2025-11-26 13:38:47 +00:00
</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>
2025-11-26 13:38:47 +00:00
</button>
<button
2026-06-04 11:21:49 +00:00
@click.stop="openTemplateModal()"
2025-11-26 13:38:47 +00:00
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>
2025-11-26 13:38:47 +00:00
</button>
2026-04-21 13:22:09 +00:00
<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>
2025-11-26 13:38:47 +00:00
</div>
</div>
2025-11-26 10:28:09 +00:00
<!-- Template Selection Modal -->
<div x-show="showTemplateModal"
@click.self="showTemplateModal = false; selectedTemplate = ''; newTemplateNoteName = ''"
2025-11-26 10:30:33 +00:00
@keydown.escape.window="showTemplateModal && (showTemplateModal = false, selectedTemplate = '', newTemplateNoteName = '')"
2025-11-26 10:40:20 +00:00
class="fixed inset-0 flex items-center justify-center"
style="background-color: rgba(0, 0, 0, 0.5); z-index: 1100;"
2025-11-26 10:28:09 +00:00
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>
2025-12-16 10:27:54 +00:00
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary);" x-text="t('templates.create_from_template')"></h2>
2025-11-26 10:28:09 +00:00
<!-- Template Selection -->
<div class="mb-4">
2025-12-16 10:27:54 +00:00
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('templates.select_template')">
2025-11-26 10:28:09 +00:00
</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);"
>
2025-12-16 10:27:54 +00:00
<option value="" x-text="'-- ' + t('templates.choose_template') + ' --'"></option>
2025-11-26 10:28:09 +00:00
<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">
2026-04-20 10:45:41 +00:00
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);" x-text="t('notes.prompt_name')">
2025-11-26 10:28:09 +00:00
</label>
<input
2026-06-04 11:21:49 +00:00
id="template-note-name-input"
2025-11-26 10:28:09 +00:00
x-model="newTemplateNoteName"
type="text"
2026-04-20 10:45:41 +00:00
:placeholder="t('notes.input_placeholder')"
2025-11-26 10:28:09 +00:00
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);">
2025-12-16 10:27:54 +00:00
<strong x-text="t('templates.available_placeholders') + ':'"></strong><br>
2025-11-26 10:28:09 +00:00
<code style="font-family: monospace;">{{date}}</code>,
<code style="font-family: monospace;">{{time}}</code>,
<code style="font-family: monospace;">{{datetime}}</code>,
2025-12-16 15:13:24 +00:00
<code style="font-family: monospace;">{{year}}</code>,
<code style="font-family: monospace;">{{month}}</code>,
<code style="font-family: monospace;">{{day}}</code>,
2025-11-26 10:28:09 +00:00
<code style="font-family: monospace;">{{title}}</code>,
<code style="font-family: monospace;">{{folder}}</code>
</div>
<!-- Empty state when no templates available -->
2025-12-16 10:27:54 +00:00
<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')">
2025-11-26 10:28:09 +00:00
</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);"
2026-06-04 11:21:49 +00:00
:style="(!selectedTemplate || !newTemplateNoteName.trim() || availableTemplates.length === 0) ? { opacity: '0.5', cursor: 'not-allowed' } : {}"
2025-11-26 10:28:09 +00:00
onmouseover="if(!this.disabled) this.style.backgroundColor='var(--accent-hover)'"
onmouseout="if(!this.disabled) this.style.backgroundColor='var(--accent-primary)'"
2025-12-16 10:27:54 +00:00
x-text="t('templates.create_note')"
2025-11-26 10:28:09 +00:00
>
</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)'"
2025-12-16 10:27:54 +00:00
x-text="t('common.cancel')"
2025-11-26 10:28:09 +00:00
>
</button>
</div>
</div>
</div>
2026-04-17 07:27:36 +00:00
<!-- 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>
2026-04-20 10:45:41 +00:00
<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>
2026-04-17 07:27:36 +00:00
<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"
2026-04-20 10:45:41 +00:00
:placeholder="createNameModalKind === 'folder' ? t('folders.input_placeholder') : t('notes.input_placeholder')"
2026-04-17 07:27:36 +00:00
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>
2026-04-17 07:39:52 +00:00
<!-- Confirm dialog (replaces window.confirm) -->
<div x-show="showConfirmModal"
@click.self="confirmModalCancel()"
@keydown.escape.window="showConfirmModal && confirmModalCancel()"
2026-06-04 11:21:49 +00:00
@keydown.enter.window="if (showConfirmModal) { $event.preventDefault(); confirmModalConfirm(); }"
2026-04-17 07:39:52 +00:00
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>
2026-01-15 15:32:35 +00:00
<!-- 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">
2026-01-18 17:04:32 +00:00
<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>
2026-01-15 15:32:35 +00:00
</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>
2026-01-15 09:07:51 +00:00
<!-- Share Modal -->
<div x-show="showShareModal"
2026-02-09 17:55:55 +00:00
@click.self="closeShareModal()"
@keydown.escape.window="showShareModal && closeShareModal()"
2026-01-15 09:07:51 +00:00
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>
2026-02-09 17:55:55 +00:00
<!-- 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>
2026-01-15 09:07:51 +00:00
<!-- 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
2026-02-09 17:55:55 +00:00
@click="closeShareModal()"
2026-01-15 09:07:51 +00:00
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>
2025-12-06 17:06:07 +00:00
<!-- Mobile Bottom Tabs -->
<nav class="mobile-bottom-tabs zen-hide" style="display: none;" role="navigation" aria-label="Main navigation">
2025-12-06 17:06:07 +00:00
<button
class="mobile-bottom-tab"
2026-01-18 15:00:01 +00:00
:class="{'active': activePanel === 'files' && (mobileSidebarOpen || (!currentNote && !currentMedia && !showGraph))}"
@click="mobileFilesTabClick()"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.files')"
2025-12-06 17:06:07 +00:00
>
<!-- Show home icon when viewing content, folder icon when on homepage -->
2026-01-18 15:00:01 +00:00
<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>
2026-01-18 15:00:01 +00:00
<svg x-show="!currentNote && !currentMedia && !showGraph" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
2025-12-06 17:06:07 +00:00
<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>
2026-01-18 15:00:01 +00:00
<span x-text="(currentNote || currentMedia || showGraph) ? t('homepage.title') : t('sidebar.files')"></span>
2025-12-06 17:06:07 +00:00
</button>
<button
2025-12-07 09:41:11 +00:00
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'search' && mobileSidebarOpen}"
2025-12-06 17:11:00 +00:00
@click="activePanel = 'search'; mobileSidebarOpen = true;"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.search')"
2025-12-06 17:06:07 +00:00
>
2025-12-06 17:40:28 +00:00
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
2025-12-06 17:06:07 +00:00
<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>
2025-12-17 09:15:08 +00:00
<span x-text="t('sidebar.search')"></span>
2025-12-07 09:41:11 +00:00
<!-- 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>
2025-12-06 17:06:07 +00:00
</button>
<button
2025-12-07 09:41:11 +00:00
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'tags' && mobileSidebarOpen}"
2025-12-06 17:11:00 +00:00
@click="activePanel = 'tags'; mobileSidebarOpen = true;"
2025-12-17 09:15:08 +00:00
:aria-label="t('tags.title')"
2025-12-06 17:06:07 +00:00
>
2025-12-06 17:40:28 +00:00
<svg fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
2025-12-06 17:06:07 +00:00
<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>
2025-12-17 09:15:08 +00:00
<span x-text="t('tags.title')"></span>
2025-12-07 09:41:11 +00:00
<!-- 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>
2025-12-06 17:06:07 +00:00
</button>
<button
class="mobile-bottom-tab"
2025-12-06 17:11:00 +00:00
:class="{'active': showGraph}"
@click="showGraph = true; initGraph(); mobileSidebarOpen = false;"
2025-12-17 09:15:08 +00:00
:aria-label="t('graph.title')"
2025-12-06 17:06:07 +00:00
>
2025-12-06 17:40:28 +00:00
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
2025-12-06 17:06:07 +00:00
<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>
2025-12-17 09:15:08 +00:00
<span x-text="t('graph.title')"></span>
2025-12-06 17:06:07 +00:00
</button>
<button
class="mobile-bottom-tab"
:class="{'active': activePanel === 'settings' && mobileSidebarOpen}"
2025-12-06 17:11:00 +00:00
@click="activePanel = 'settings'; mobileSidebarOpen = true;"
2025-12-17 09:15:08 +00:00
:aria-label="t('sidebar.settings')"
2025-12-06 17:06:07 +00:00
>
2025-12-06 17:40:28 +00:00
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
2025-12-06 17:06:07 +00:00
<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>
2025-12-17 09:15:08 +00:00
<span x-text="t('sidebar.settings')"></span>
2025-12-06 17:06:07 +00:00
</button>
2025-12-06 17:40:28 +00:00
</nav>
2025-12-06 17:06:07 +00:00
2026-04-12 16:46:35 +00:00
<script src="/static/editor-markdown-continue.js"></script>
2025-11-05 16:48:41 +00:00
<script src="/static/app.js"></script>
2026-01-01 14:26:03 +00:00
<!-- 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>
2025-11-05 16:48:41 +00:00
</body>
</html>