Merge pull request #68 from gamosoft/features/ui-revamp

Features/UI revamp
This commit is contained in:
Gamosoft 2025-12-08 10:32:21 +01:00 committed by GitHub
commit cae68cfc3b
3 changed files with 579 additions and 271 deletions

View File

@ -36,6 +36,7 @@ function noteApp() {
appName: 'NoteDiscovery', appName: 'NoteDiscovery',
appTagline: 'Your Self-Hosted Knowledge Base', appTagline: 'Your Self-Hosted Knowledge Base',
appVersion: '0.0.0', appVersion: '0.0.0',
authEnabled: false,
notes: [], notes: [],
currentNote: '', currentNote: '',
currentNoteName: '', currentNoteName: '',
@ -60,6 +61,9 @@ function noteApp() {
currentTheme: 'light', currentTheme: 'light',
availableThemes: [], availableThemes: [],
// Icon rail / panel state
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
// Folder state // Folder state
folderTree: [], folderTree: [],
allFolders: [], allFolders: [],
@ -462,6 +466,7 @@ function noteApp() {
this.appName = config.name; this.appName = config.name;
this.appTagline = config.tagline; this.appTagline = config.tagline;
this.appVersion = config.version || '0.0.0'; this.appVersion = config.version || '0.0.0';
this.authEnabled = config.authentication?.enabled || false;
} catch (error) { } catch (error) {
console.error('Failed to load config:', error); console.error('Failed to load config:', error);
} }

View File

@ -519,6 +519,150 @@
opacity: 0.4; opacity: 0.4;
} }
/* Icon Rail (VS Code-style navigation) */
.icon-rail {
width: 44px;
min-width: 44px;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 0;
background-color: var(--bg-tertiary);
border-right: 1px solid var(--border-primary);
}
.icon-rail-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin-bottom: 4px;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
color: var(--text-tertiary);
background: transparent;
border: none;
}
.icon-rail-btn:hover {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.icon-rail-btn.active {
background-color: var(--accent-light);
color: var(--accent-primary);
}
.icon-rail-btn svg {
width: 20px;
height: 20px;
}
.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;
}
.sidebar-panel-header {
padding: 12px;
border-bottom: 1px solid var(--border-primary);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-tertiary);
}
.sidebar-panel-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
/* Mobile bottom tabs */
@media (max-width: 768px) {
.icon-rail {
display: none;
}
.mobile-bottom-tabs {
display: flex !important;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
z-index: 1001;
padding-bottom: env(safe-area-inset-bottom);
}
.mobile-bottom-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: var(--text-tertiary);
background: none;
border: none;
cursor: pointer;
transition: color 0.15s ease, background-color 0.15s ease;
min-width: 44px;
min-height: 44px;
}
.mobile-bottom-tab:active {
background-color: var(--bg-hover);
}
.mobile-bottom-tab.active {
color: var(--accent-primary);
}
.mobile-bottom-tab svg {
width: 22px;
height: 22px;
}
.mobile-bottom-tab span {
font-size: 10px;
}
}
@media (min-width: 769px) {
.mobile-bottom-tabs {
display: none !important;
}
}
/* Scrollbar styling */ /* Scrollbar styling */
.custom-scrollbar::-webkit-scrollbar, .custom-scrollbar::-webkit-scrollbar,
.editor-textarea::-webkit-scrollbar, .editor-textarea::-webkit-scrollbar,
@ -817,252 +961,132 @@
<!-- Main Container --> <!-- Main Container -->
<div class="flex h-screen overflow-hidden"> <div class="flex h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar with Icon Rail -->
<div <div
class="flex flex-col mobile-sidebar" class="flex mobile-sidebar"
:class="{'mobile-sidebar-open': mobileSidebarOpen}" :class="{'mobile-sidebar-open': mobileSidebarOpen}"
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); border-right: 1px solid var(--border-primary); min-width: 200px; max-width: 600px;'" :style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
> >
<!-- Header --> <!-- Icon Rail -->
<div class="flex-shrink-0 p-4 border-b" style="border-color: var(--border-primary);"> <div class="icon-rail">
<button <!-- Logo -->
class="flex items-center gap-2 mb-3 hover:opacity-80 transition" <img
src="/static/logo.svg"
alt="NoteDiscovery"
class="icon-rail-logo"
@click="goHome()" @click="goHome()"
aria-label="Go to homepage" title="Go to homepage"
style="background: none; border: none; outline: none; cursor: pointer;"
> >
<img src="/static/logo.svg" alt="NoteDiscovery Logo" class="w-8 h-8">
<h1 class="text-xl font-bold" style="color: var(--text-primary);" x-text="appName"></h1> <!-- Files -->
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'files'}"
@click="activePanel = 'files'"
title="Files"
aria-label="Files panel"
>
<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> </button>
<!-- Logout Button (only show if auth is enabled) -->
<div x-data="{ authEnabled: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => authEnabled = d.authentication?.enabled || false)"> <!-- Search -->
<a x-show="authEnabled" <button
href="/logout" class="icon-rail-btn relative"
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors" :class="{'active': activePanel === 'search'}"
style="background-color: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-primary);" @click="activePanel = 'search'"
onmouseover="this.style.backgroundColor='var(--bg-hover)'; this.style.color='var(--text-primary)'" title="Search"
onmouseout="this.style.backgroundColor='var(--bg-secondary)'; this.style.color='var(--text-secondary)'"> aria-label="Search panel"
<span>🔒</span> >
<span>Logout</span> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
</a>
</div>
</div>
<!-- Search -->
<div class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
<div class="relative">
<input
type="text"
x-model="searchQuery"
@input="searchNotes()"
placeholder="Search..."
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);"
>
<!-- Search Icon -->
<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> <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> </svg>
<!-- Clear Button --> <!-- Search results badge -->
<button <span
x-show="searchQuery.length > 0" x-show="searchQuery.trim() && searchResults.length > 0"
@click="searchQuery = ''; searchResults = []; currentSearchHighlight = ''; clearSearchHighlights();" x-cloak
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" class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="color: var(--text-tertiary);" style="background-color: var(--accent-primary); color: white;"
title="Clear search" x-text="searchResults.length > 9 ? '9+' : searchResults.length"
> ></span>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> </button>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Match Navigation (shown when there are matches in current note) --> <!-- Tags -->
<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-secondary); color: var(--text-secondary);"
>
<span x-text="`Match ${currentMatchIndex + 1} of ${totalMatches}`"></span>
<div class="flex gap-1">
<button
@click="previousMatch()"
class="px-2 py-1 rounded hover:bg-opacity-80 transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary);"
title="Previous match (Shift+F3)"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button
@click="nextMatch()"
class="px-2 py-1 rounded hover:bg-opacity-80 transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary);"
title="Next match (F3)"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Tags Panel -->
<div class="flex-shrink-0 border-b" style="border-color: var(--border-primary);">
<!-- Tags Header -->
<div
@click="tagsExpanded = !tagsExpanded"
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-opacity-10 transition-colors"
style="background-color: var(--bg-secondary);"
>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" 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>
<span class="text-sm font-medium" style="color: var(--text-primary);">Tags</span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="Object.keys(allTags).length"></span>
</div>
<svg
class="w-4 h-4 transform transition-transform"
:class="{'rotate-180': tagsExpanded}"
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="M19 9l-7 7-7-7"></path>
</svg>
</div>
<!-- Tags List -->
<div x-show="tagsExpanded" class="px-3 py-2 max-h-48 overflow-y-auto custom-scrollbar">
<!-- Clear Filter Button -->
<div x-show="selectedTags.length > 0" class="mb-2">
<button
@click="clearTagFilters()"
class="w-full px-2 py-1 text-xs rounded flex items-center justify-center gap-1"
style="background-color: var(--accent-primary); color: white;"
title="Clear tag filters"
>
<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 (<span x-text="selectedTags.length"></span>)
</button>
</div>
<!-- Tag Chips -->
<div class="flex flex-wrap gap-1.5">
<template x-for="[tag, count] in sortedTags" :key="tag">
<button
@click="toggleTag(tag)"
class="px-2 py-1 text-xs rounded-full transition-all hover:brightness-110"
:style="selectedTags.includes(tag)
? 'background-color: var(--accent-primary); color: white; font-weight: 600;'
: 'background-color: var(--bg-tertiary); color: var(--text-primary);'"
:title="`Filter by ${tag} (${count} notes)`"
>
<span x-text="tag"></span>
<span class="opacity-75" x-text="` (${count})`"></span>
</button>
</template>
</div>
<!-- Empty State -->
<div x-show="Object.keys(allTags).length === 0" class="text-xs text-center py-2" style="color: var(--text-tertiary);">
No tags found. Add tags to notes using YAML frontmatter:
</div>
</div>
</div>
<!-- New Item Dropdown -->
<div class="flex-shrink-0 px-2 py-1.5 border-b" style="border-color: var(--border-primary);">
<button <button
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)" class="icon-rail-btn relative"
class="w-full px-3 py-1.5 text-xs font-medium text-white rounded focus:outline-none focus:ring-1 flex items-center justify-center gap-1.5" :class="{'active': activePanel === 'tags'}"
style="background-color: var(--accent-primary);" @click="activePanel = 'tags'"
onmouseover="this.style.backgroundColor='var(--accent-hover)'" title="Tags"
onmouseout="this.style.backgroundColor='var(--accent-primary)'" aria-label="Tags panel"
> >
<span>+ New</span> <svg fill="currentColor" viewBox="0 0 20 20">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg>
<!-- Filtered notes badge (shows result count when tags selected) -->
<span
x-show="selectedTags.length > 0"
x-cloak
class="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<div class="icon-rail-spacer"></div>
<!-- Graph -->
<button
class="icon-rail-btn"
:class="{'active': showGraph}"
@click="showGraph = true; initGraph()"
title="Graph View"
aria-label="Graph view"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
</button>
<!-- Settings -->
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'settings'}"
@click="activePanel = 'settings'"
title="Settings"
aria-label="Settings panel"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg> </svg>
</button> </button>
</div> </div>
<!-- Notes List --> <!-- Sidebar Panel -->
<div class="flex-1 overflow-y-auto custom-scrollbar"> <div class="sidebar-panel" style="border-right: 1px solid var(--border-primary);">
<template x-if="searchResults.length > 0">
<div class="p-2">
<div class="text-xs px-2 py-1 flex items-center justify-between" style="color: var(--text-tertiary);">
<span x-text="searchQuery.trim() ? 'Search Results' : 'Filtered Notes'"></span>
<span x-text="`${searchResults.length} note${searchResults.length === 1 ? '' : 's'}`"></span>
</div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type, searchQuery)"
class="px-2 py-1.5 text-sm cursor-pointer rounded"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
:title="note.path"
>
<div class="font-medium truncate" x-text="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')"></div>
</div>
</template>
</div>
</template>
<!-- No Results Message (when filters active but no matches) --> <!-- ==================== FILES PANEL ==================== -->
<template x-if="searchResults.length === 0 && (selectedTags.length > 0 || searchQuery.trim())"> <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">
<div class="p-6 text-center"> <!-- Header with + New -->
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div class="flex-shrink-0 px-2 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<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> <span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Files</span>
</svg> <button
<h3 class="text-lg font-medium mb-2" style="color: var(--text-primary);">No matches found</h3> @click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
<p class="text-sm mb-4" style="color: var(--text-secondary);"> class="px-2 py-1 text-xs font-medium text-white rounded focus:outline-none flex items-center gap-1"
<template x-if="searchQuery.trim() && selectedTags.length > 0"> style="background-color: var(--accent-primary);"
<span>No notes match both your search and selected tags</span> onmouseover="this.style.backgroundColor='var(--accent-hover)'"
</template> onmouseout="this.style.backgroundColor='var(--accent-primary)'"
<template x-if="searchQuery.trim() && selectedTags.length === 0"> >
<span>No notes contain "<span x-text="searchQuery"></span>"</span> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</template> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
<template x-if="!searchQuery.trim() && selectedTags.length > 0"> </svg>
<span>No notes have the selected tag<span x-text="selectedTags.length > 1 ? 's' : ''"></span></span> New
</template> </button>
</p>
<div class="flex gap-2 justify-center">
<button
x-show="searchQuery.trim()"
@click="searchQuery = ''; searchNotes();"
class="px-3 py-1.5 text-sm rounded transition-colors"
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)'"
>
Clear search
</button>
<button
x-show="selectedTags.length > 0"
@click="clearTagFilters()"
class="px-3 py-1.5 text-sm rounded transition-colors"
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)'"
>
Clear tags
</button>
</div>
</div> </div>
</template>
<!-- Folder Tree -->
<!-- Folder Tree (when no filters active) --> <div class="flex-1 overflow-y-auto custom-scrollbar p-2">
<template x-if="searchResults.length === 0 && selectedTags.length === 0 && !searchQuery.trim()">
<div class="p-2">
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);"> <div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<span>Folders & Notes</span> <span>Folders & Notes</span>
@ -1147,52 +1171,250 @@
</div> </div>
</template> </template>
</div> </div>
</template>
</div>
<!-- Footer -->
<div class="flex-shrink-0 p-3 border-t" style="border-color: var(--border-primary);">
<!-- Theme Selector and Graph Button -->
<div class="flex gap-2 mb-2">
<select
x-model="currentTheme"
@change="setTheme(currentTheme)"
class="flex-1 px-2 py-1.5 text-xs 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;"
title="Select theme"
>
<template x-for="theme in availableThemes" :key="theme.id">
<option :value="theme.id" x-text="theme.name"></option>
</template>
</select>
<button
@click="showGraph = true; initGraph()"
class="px-2 py-1.5 text-xs rounded transition-colors"
:style="showGraph ? 'background-color: var(--accent-primary); color: white;' : 'background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);'"
title="View note connections graph"
>
🔗
</button>
</div> </div>
<!-- END FILES PANEL -->
<!-- Stats --> <!-- ==================== SEARCH PANEL ==================== -->
<div class="text-xs text-center" style="color: var(--text-tertiary);"> <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">
<span x-text="notes.length"></span> notes <!-- Search Header -->
<span class="mx-1">·</span> <div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
<span x-text="'v' + appVersion"></span> <span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Search</span>
<span class="mx-1">·</span> </div>
<a
href="https://www.notediscovery.com" <!-- Search Input -->
target="_blank" <div class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
rel="noopener noreferrer" <div class="relative">
class="hover:underline" <input
style="color: var(--accent-primary);" type="text"
title="Visit NoteDiscovery website" x-model="searchQuery"
> @input="searchNotes()"
Website placeholder="Search notes..."
</a> 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="Clear search"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Match Navigation -->
<div
x-show="totalMatches > 0"
class="flex items-center justify-between mt-1.5 px-2 py-0.5 rounded text-xs"
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
>
<span x-text="`Match ${currentMatchIndex + 1} of ${totalMatches}`"></span>
<div class="flex gap-1">
<button @click="previousMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" title="Previous (Shift+F3)">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
</button>
<button @click="nextMatch()" class="px-2 py-1 rounded hover:bg-opacity-80" style="background-color: var(--bg-primary); color: var(--text-primary);" title="Next (F3)">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
</div>
</div>
<!-- Search Results -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<template x-if="searchQuery.trim() && searchResults.length > 0">
<div class="p-2">
<div class="text-xs px-2 py-1 mb-1" style="color: var(--text-tertiary);">
<span x-text="`${searchResults.length} result${searchResults.length === 1 ? '' : 's'}`"></span>
</div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type, searchQuery)"
class="px-2 py-1.5 text-sm cursor-pointer rounded mb-1"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
>
<div class="font-medium truncate" x-text="note.name"></div>
<div class="text-xs truncate" style="color: var(--text-tertiary);" x-html="(note.matches && note.matches.length > 0) ? note.matches[0].context : (note.folder || 'Root')"></div>
</div>
</template>
</div>
</template>
<template x-if="searchQuery.trim() && searchResults.length === 0">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="text-sm" style="color: var(--text-tertiary);">No notes contain "<span x-text="searchQuery"></span>"</p>
</div>
</template>
<template x-if="!searchQuery.trim()">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<p class="text-sm" style="color: var(--text-tertiary);">Type to search your notes</p>
</div>
</template>
</div>
</div> </div>
<!-- END SEARCH PANEL -->
<!-- ==================== TAGS PANEL ==================== -->
<div x-show="activePanel === 'tags'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
<!-- Tags Header -->
<div class="flex-shrink-0 px-3 py-2 border-b flex items-center justify-between" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Tags</span>
<span class="text-xs px-1.5 py-0.5 rounded" style="background-color: var(--bg-tertiary); color: var(--text-tertiary);" x-text="Object.keys(allTags).length"></span>
</div>
<!-- Active Filters -->
<div x-show="selectedTags.length > 0" class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
<button
@click="clearTagFilters()"
class="w-full px-2 py-1.5 text-xs rounded flex items-center justify-center gap-1"
style="background-color: var(--accent-primary); color: white;"
title="Clear tag filters"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Clear Filters (<span x-text="selectedTags.length"></span>)
</button>
</div>
<!-- Scrollable content: Tags + Filtered Results -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- Tag Chips -->
<div class="p-3">
<template x-if="Object.keys(allTags).length > 0">
<div class="flex flex-wrap gap-1.5">
<template x-for="[tag, count] in sortedTags" :key="tag">
<button
@click="toggleTag(tag)"
class="px-2 py-1 text-xs rounded-full transition-all hover:brightness-110"
:style="selectedTags.includes(tag)
? 'background-color: var(--accent-primary); color: white; font-weight: 600;'
: 'background-color: var(--bg-tertiary); color: var(--text-primary);'"
:title="`Filter by ${tag} (${count} notes)`"
>
<span x-text="tag"></span>
<span class="opacity-75" x-text="` (${count})`"></span>
</button>
</template>
</div>
</template>
<template x-if="Object.keys(allTags).length === 0">
<div class="text-center py-6">
<svg class="w-12 h-12 mx-auto mb-3 opacity-40" style="color: var(--text-tertiary);" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>
<p class="text-sm mb-2" style="color: var(--text-tertiary);">No tags found</p>
<p class="text-xs" style="color: var(--text-tertiary);">Add tags using YAML frontmatter</p>
</div>
</template>
</div>
<!-- Filtered Notes (shown immediately after tags when selected) -->
<template x-if="selectedTags.length > 0">
<div class="border-t" style="border-color: var(--border-primary);">
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">
Filtered Notes (<span x-text="searchResults.length"></span>)
</div>
<div>
<template x-for="note in searchResults" :key="note.path">
<div
@click="openItem(note.path, note.type)"
class="px-3 py-1.5 text-sm cursor-pointer"
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
onmouseout="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='transparent'"
>
<span class="truncate block" x-text="note.name"></span>
</div>
</template>
<div x-show="searchResults.length === 0" class="px-3 py-4 text-center text-xs" style="color: var(--text-tertiary);">
No notes match selected tags
</div>
</div>
</div>
</template>
</div>
</div>
<!-- END TAGS PANEL -->
<!-- ==================== SETTINGS PANEL ==================== -->
<template x-if="activePanel === 'settings'">
<div class="flex flex-col h-full">
<!-- Settings Header -->
<div class="flex-shrink-0 px-3 py-2 border-b" style="border-color: var(--border-primary);">
<span class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-tertiary);">Settings</span>
</div>
<!-- Scrollable content -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<div class="p-3">
<!-- Theme -->
<div class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);">Theme</label>
<select
x-model="currentTheme"
@change="setTheme(currentTheme)"
class="w-full px-2 py-2 text-sm rounded focus:outline-none focus:ring-2 focus:ring-opacity-50"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;"
>
<template x-for="theme in availableThemes" :key="theme.id">
<option :value="theme.id" x-text="theme.name"></option>
</template>
</select>
</div>
<!-- Logout (if auth enabled) - uses authEnabled from main app state -->
<div x-show="authEnabled" class="mb-4">
<label class="block text-xs font-medium mb-2" style="color: var(--text-secondary);">Account</label>
<a
href="/logout"
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);"
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
onmouseout="this.style.backgroundColor='var(--bg-primary)'"
>
🔒 Logout
</a>
</div>
</div>
<!-- Footer Info (inside scroll area) -->
<div class="p-3 mt-4 border-t text-center" style="border-color: var(--border-primary);">
<div class="text-xs" style="color: var(--text-tertiary);">
<span x-text="notes.length"></span> notes
<span class="mx-1">·</span>
<span x-text="'v' + appVersion"></span>
</div>
<a
href="https://www.notediscovery.com"
target="_blank"
rel="noopener noreferrer"
class="text-xs hover:underline"
style="color: var(--accent-primary);"
>
notediscovery.com
</a>
</div>
</div>
</div>
</template>
<!-- END SETTINGS PANEL -->
</div> </div>
<!-- END Sidebar Panel -->
</div> </div>
<!-- Resize Handle --> <!-- Resize Handle -->
@ -2023,6 +2245,82 @@
</div> </div>
</div> </div>
<!-- Mobile Bottom Tabs -->
<nav class="mobile-bottom-tabs" style="display: none;" role="navigation" aria-label="Main navigation">
<button
class="mobile-bottom-tab"
:class="{'active': activePanel === 'files'}"
@click="activePanel = 'files'; mobileSidebarOpen = true;"
aria-label="Files"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
<span>Files</span>
</button>
<button
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'search'}"
@click="activePanel = 'search'; mobileSidebarOpen = true;"
aria-label="Search"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span>Search</span>
<!-- Search results badge -->
<span
x-show="searchQuery.trim() && searchResults.length > 0"
x-cloak
class="absolute top-1 right-1/4 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<button
class="mobile-bottom-tab relative"
:class="{'active': activePanel === 'tags'}"
@click="activePanel = 'tags'; mobileSidebarOpen = true;"
aria-label="Tags"
>
<svg fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>
<span>Tags</span>
<!-- Filtered notes badge (shows result count when tags selected) -->
<span
x-show="selectedTags.length > 0"
x-cloak
class="absolute top-1 right-1/4 w-4 h-4 text-[9px] font-bold rounded-full flex items-center justify-center"
style="background-color: var(--accent-primary); color: white;"
x-text="searchResults.length > 9 ? '9+' : searchResults.length"
></span>
</button>
<button
class="mobile-bottom-tab"
:class="{'active': showGraph}"
@click="showGraph = true; initGraph(); mobileSidebarOpen = false;"
aria-label="Graph"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
<span>Graph</span>
</button>
<button
class="mobile-bottom-tab"
:class="{'active': activePanel === 'settings'}"
@click="activePanel = 'settings'; mobileSidebarOpen = true;"
aria-label="Settings"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Settings</span>
</button>
</nav>
<script src="/static/app.js"></script> <script src="/static/app.js"></script>
</body> </body>
</html> </html>

View File

@ -146,3 +146,8 @@ git push origin "v$Version"
Write-Host "`nRelease $Version completed successfully!" -ForegroundColor Green Write-Host "`nRelease $Version completed successfully!" -ForegroundColor Green
Write-Host "Tag: v$Version" -ForegroundColor Cyan Write-Host "Tag: v$Version" -ForegroundColor Cyan
# Open GitHub Actions page to monitor build status
$actionsUrl = "https://github.com/gamosoft/NoteDiscovery/actions"
Write-Host "`nOpening GitHub Actions to monitor build status..." -ForegroundColor Yellow
Start-Process $actionsUrl