added rail menu
This commit is contained in:
parent
25db76f494
commit
9f9db95c18
|
|
@ -60,6 +60,9 @@ function noteApp() {
|
|||
currentTheme: 'light',
|
||||
availableThemes: [],
|
||||
|
||||
// Icon rail / panel state
|
||||
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
|
||||
|
||||
// Folder state
|
||||
folderTree: [],
|
||||
allFolders: [],
|
||||
|
|
|
|||
|
|
@ -519,6 +519,148 @@
|
|||
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;
|
||||
}
|
||||
|
||||
.mobile-bottom-tab.active {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.mobile-bottom-tab svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.mobile-bottom-tab span {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Add padding to main content for bottom tabs */
|
||||
.main-content-mobile-padding {
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-bottom-tabs {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.custom-scrollbar::-webkit-scrollbar,
|
||||
.editor-textarea::-webkit-scrollbar,
|
||||
|
|
@ -817,252 +959,125 @@
|
|||
<!-- Main Container -->
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<!-- Sidebar with Icon Rail -->
|
||||
<div
|
||||
class="flex flex-col mobile-sidebar"
|
||||
class="flex mobile-sidebar"
|
||||
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
|
||||
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); border-right: 1px solid var(--border-primary); min-width: 200px; max-width: 600px;'"
|
||||
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 p-4 border-b" style="border-color: var(--border-primary);">
|
||||
<button
|
||||
class="flex items-center gap-2 mb-3 hover:opacity-80 transition"
|
||||
<!-- Icon Rail -->
|
||||
<div class="icon-rail">
|
||||
<!-- Logo -->
|
||||
<img
|
||||
src="/static/logo.svg"
|
||||
alt="NoteDiscovery"
|
||||
class="icon-rail-logo"
|
||||
@click="goHome()"
|
||||
aria-label="Go to homepage"
|
||||
style="background: none; border: none; outline: none; cursor: pointer;"
|
||||
title="Go to homepage"
|
||||
>
|
||||
<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>
|
||||
</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)">
|
||||
<a x-show="authEnabled"
|
||||
href="/logout"
|
||||
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'; this.style.color='var(--text-primary)'"
|
||||
onmouseout="this.style.backgroundColor='var(--bg-secondary)'; this.style.color='var(--text-secondary)'">
|
||||
<span>🔒</span>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</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">
|
||||
<!-- Files -->
|
||||
<button
|
||||
class="icon-rail-btn"
|
||||
:class="{'active': activePanel === 'files'}"
|
||||
@click="activePanel = 'files'"
|
||||
title="Files"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Search -->
|
||||
<button
|
||||
class="icon-rail-btn relative"
|
||||
:class="{'active': activePanel === 'search'}"
|
||||
@click="activePanel = 'search'"
|
||||
title="Search"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<!-- Clear Button -->
|
||||
<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>
|
||||
<!-- Search results badge -->
|
||||
<span
|
||||
x-show="searchQuery.trim() && searchResults.length > 0"
|
||||
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>
|
||||
|
||||
<!-- Match Navigation (shown when there are matches in current note) -->
|
||||
<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);">
|
||||
<!-- Tags -->
|
||||
<button
|
||||
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
|
||||
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"
|
||||
style="background-color: var(--accent-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||
class="icon-rail-btn relative"
|
||||
:class="{'active': activePanel === 'tags'}"
|
||||
@click="activePanel = 'tags'"
|
||||
title="Tags"
|
||||
>
|
||||
<span>+ New</span>
|
||||
<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="M19 9l-7 7-7-7"></path>
|
||||
<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>
|
||||
<!-- Selected tags badge -->
|
||||
<span
|
||||
x-show="selectedTags.length > 0"
|
||||
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="selectedTags.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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Notes List -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<template x-if="searchResults.length > 0">
|
||||
<div class="p-2">
|
||||
<div class="text-xs px-2 py-1 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>
|
||||
<!-- Sidebar Panel -->
|
||||
<div class="sidebar-panel" style="border-right: 1px solid var(--border-primary);">
|
||||
|
||||
<!-- No Results Message (when filters active but no matches) -->
|
||||
<template x-if="searchResults.length === 0 && (selectedTags.length > 0 || searchQuery.trim())">
|
||||
<div class="p-6 text-center">
|
||||
<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">
|
||||
<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>
|
||||
<h3 class="text-lg font-medium mb-2" style="color: var(--text-primary);">No matches found</h3>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary);">
|
||||
<template x-if="searchQuery.trim() && selectedTags.length > 0">
|
||||
<span>No notes match both your search and selected tags</span>
|
||||
</template>
|
||||
<template x-if="searchQuery.trim() && selectedTags.length === 0">
|
||||
<span>No notes contain "<span x-text="searchQuery"></span>"</span>
|
||||
</template>
|
||||
<template x-if="!searchQuery.trim() && selectedTags.length > 0">
|
||||
<span>No notes have the selected tag<span x-text="selectedTags.length > 1 ? 's' : ''"></span></span>
|
||||
</template>
|
||||
</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>
|
||||
<!-- ==================== 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);">Files</span>
|
||||
<button
|
||||
@click="dropdownTargetFolder = ''; toggleNewDropdown($event)"
|
||||
class="px-2 py-1 text-xs font-medium text-white rounded focus:outline-none flex items-center gap-1"
|
||||
style="background-color: var(--accent-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Folder Tree (when no filters active) -->
|
||||
<template x-if="searchResults.length === 0 && selectedTags.length === 0 && !searchQuery.trim()">
|
||||
<div class="p-2">
|
||||
<!-- Folder Tree -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span>Folders & Notes</span>
|
||||
|
|
@ -1147,52 +1162,244 @@
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END FILES PANEL -->
|
||||
|
||||
<!-- 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>
|
||||
<!-- ==================== 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);">Search</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Input -->
|
||||
<div class="flex-shrink-0 px-2 py-2 border-b" style="border-color: var(--border-primary);">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="searchQuery"
|
||||
@input="searchNotes()"
|
||||
placeholder="Search notes..."
|
||||
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>
|
||||
</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>
|
||||
<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>
|
||||
<!-- END SEARCH PANEL -->
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="text-xs text-center" style="color: var(--text-tertiary);">
|
||||
<span x-text="notes.length"></span> notes
|
||||
<span class="mx-1">·</span>
|
||||
<span x-text="'v' + appVersion"></span>
|
||||
<span class="mx-1">·</span>
|
||||
<a
|
||||
href="https://www.notediscovery.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:underline"
|
||||
style="color: var(--accent-primary);"
|
||||
title="Visit NoteDiscovery website"
|
||||
>
|
||||
Website
|
||||
</a>
|
||||
<!-- ==================== 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>
|
||||
|
||||
<!-- Tag List -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar 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 when tags selected) -->
|
||||
<template x-if="selectedTags.length > 0">
|
||||
<div class="flex-shrink-0 border-t" style="border-color: var(--border-primary); max-height: 40%;">
|
||||
<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 class="overflow-y-auto custom-scrollbar" style="max-height: 200px;">
|
||||
<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" 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>
|
||||
<!-- END TAGS PANEL -->
|
||||
|
||||
<!-- ==================== SETTINGS PANEL ==================== -->
|
||||
<div x-show="activePanel === 'settings'" 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">
|
||||
<!-- 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>
|
||||
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar 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) -->
|
||||
<div x-data="{ authEnabled: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => authEnabled = d.authentication?.enabled || false)">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<div class="flex-shrink-0 p-3 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>
|
||||
<!-- END SETTINGS PANEL -->
|
||||
|
||||
</div>
|
||||
<!-- END Sidebar Panel -->
|
||||
</div>
|
||||
|
||||
<!-- Resize Handle -->
|
||||
|
|
@ -2023,6 +2230,61 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Bottom Tabs -->
|
||||
<div class="mobile-bottom-tabs" x-data style="display: none;">
|
||||
<button
|
||||
class="mobile-bottom-tab"
|
||||
:class="{'active': $root.activePanel === 'files'}"
|
||||
@click="$root.activePanel = 'files'; $root.mobileSidebarOpen = true;"
|
||||
>
|
||||
<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>
|
||||
<span>Files</span>
|
||||
</button>
|
||||
<button
|
||||
class="mobile-bottom-tab"
|
||||
:class="{'active': $root.activePanel === 'search'}"
|
||||
@click="$root.activePanel = 'search'; $root.mobileSidebarOpen = true;"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
<button
|
||||
class="mobile-bottom-tab"
|
||||
:class="{'active': $root.activePanel === 'tags'}"
|
||||
@click="$root.activePanel = 'tags'; $root.mobileSidebarOpen = true;"
|
||||
>
|
||||
<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>
|
||||
<span>Tags</span>
|
||||
</button>
|
||||
<button
|
||||
class="mobile-bottom-tab"
|
||||
:class="{'active': $root.showGraph}"
|
||||
@click="$root.showGraph = true; $root.initGraph(); $root.mobileSidebarOpen = false;"
|
||||
>
|
||||
<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>
|
||||
<span>Graph</span>
|
||||
</button>
|
||||
<button
|
||||
class="mobile-bottom-tab"
|
||||
:class="{'active': $root.activePanel === 'settings'}"
|
||||
@click="$root.activePanel = 'settings'; $root.mobileSidebarOpen = true;"
|
||||
>
|
||||
<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>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue