Files
clawd/kanban/notes.html
Echo ea4101710f Improve Activity panel + task tracking rules
- Activity loads from tasks.json dynamically
- Added task tracking workflow in AGENTS.md
- Notes UI improvements
- Renamed recipe with date prefix
2026-01-30 20:58:30 +00:00

941 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo · Notes</title>
<link rel="stylesheet" href="common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="swipe-nav.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.main {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-6);
flex-wrap: wrap;
gap: var(--space-4);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
}
.search-bar {
width: 300px;
}
/* Date sections (accordion) */
.date-section {
margin-bottom: var(--space-3);
}
.date-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: var(--radius-md);
cursor: pointer;
user-select: none;
transition: all var(--transition-fast);
}
[data-theme="light"] .date-header {
border-color: rgba(0, 0, 0, 0.15);
}
.date-header:hover {
filter: brightness(1.1);
}
/* Section colors */
[data-section="today"] .date-header {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.15));
border-left: 3px solid #3b82f6;
}
[data-section="yesterday"] .date-header {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(124, 58, 237, 0.15));
border-left: 3px solid #8b5cf6;
}
[data-section="thisWeek"] .date-header {
background: linear-gradient(135deg, rgba(20, 184, 166, 0.25), rgba(13, 148, 136, 0.15));
border-left: 3px solid #14b8a6;
}
[data-section="older"] .date-header {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.25), rgba(234, 88, 12, 0.15));
border-left: 3px solid #f97316;
}
.date-header-left {
display: flex;
align-items: center;
gap: var(--space-3);
}
.date-header-left svg {
width: 18px;
height: 18px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.date-section.collapsed .date-header-left svg {
transform: rotate(-90deg);
}
.date-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
}
.date-sublabel {
font-size: var(--text-xs);
color: var(--text-muted);
margin-left: var(--space-2);
}
.date-section.collapsed .date-content {
display: none;
}
.date-content {
padding: var(--space-4) 0;
}
/* Notes grid inside sections */
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-4);
}
.note-card {
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: var(--radius-lg);
padding: var(--space-4);
cursor: pointer;
transition: all var(--transition-base);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-theme="light"] .note-card {
border-color: rgba(0, 0, 0, 0.15);
}
.note-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.note-title {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
line-height: 1.4;
}
.note-tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
/* Empty section */
.empty-section {
padding: var(--space-4);
text-align: center;
color: var(--text-muted);
font-size: var(--text-sm);
}
/* Note viewer overlay */
.note-viewer {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 200;
overflow: auto;
}
.note-viewer.active {
display: block;
}
.note-viewer-content {
max-width: 800px;
margin: var(--space-5) auto;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
min-height: calc(100vh - var(--space-10));
}
.note-viewer-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background: var(--bg-base);
z-index: 10;
}
.note-viewer-header h2 {
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
}
.note-viewer-body {
padding: var(--space-6);
}
/* Markdown */
.markdown-body {
color: var(--text-secondary);
line-height: 1.7;
}
.markdown-body h1 {
font-size: 1.8rem;
color: var(--text-primary);
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--border);
}
.markdown-body h2 {
font-size: 1.3rem;
color: var(--accent);
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
.markdown-body h3 {
font-size: 1.1rem;
color: var(--text-primary);
margin-top: var(--space-5);
margin-bottom: var(--space-2);
}
.markdown-body p { margin-bottom: var(--space-4); }
.markdown-body ul, .markdown-body ol {
margin-bottom: var(--space-4);
padding-left: var(--space-6);
}
.markdown-body li { margin-bottom: var(--space-2); }
.markdown-body a {
color: var(--accent);
text-decoration: none;
}
.markdown-body a:hover { text-decoration: underline; }
.markdown-body code {
background: var(--bg-surface);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9em;
}
.markdown-body pre {
background: var(--bg-surface);
padding: var(--space-4);
border-radius: var(--radius-md);
overflow-x: auto;
margin-bottom: var(--space-4);
}
.markdown-body pre code {
background: none;
padding: 0;
}
.markdown-body blockquote {
border-left: 3px solid var(--accent);
padding-left: var(--space-4);
color: var(--text-muted);
margin-bottom: var(--space-4);
}
.markdown-body strong { color: var(--text-primary); }
/* Tag filter pills */
.tag-filter {
margin-bottom: var(--space-5);
}
.tag-filter-header {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
user-select: none;
margin-bottom: var(--space-2);
}
.tag-filter-header svg {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.tag-filter.collapsed .tag-filter-header svg {
transform: rotate(-90deg);
}
.tag-pills-more {
display: none;
margin-top: var(--space-2);
}
.tag-pills-more.expanded {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
/* More tags toggle button - same style as pills */
.tag-pill.more-toggle {
background: rgba(100, 116, 139, 0.2);
border-color: rgba(100, 116, 139, 0.4);
color: var(--text-muted);
}
.tag-pill.more-toggle:hover {
background: rgba(100, 116, 139, 0.3);
}
.tag-pill.more-toggle.expanded {
background: rgba(100, 116, 139, 0.4);
}
/* Dimmed pills - tags not in visible notes */
.tag-pill.dimmed {
opacity: 0.35;
}
.tag-pill.dimmed:hover {
opacity: 0.6;
}
.filter-count {
font-size: var(--text-xs);
color: var(--text-muted);
margin-left: var(--space-1);
}
.tag-filter-label {
font-size: var(--text-xs);
color: var(--text-muted);
margin-bottom: var(--space-2);
display: block;
}
.tag-pills {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--radius-full);
font-size: var(--text-sm);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
user-select: none;
}
[data-theme="light"] .tag-pill {
border-color: rgba(0, 0, 0, 0.15);
}
.tag-pill:hover {
background: var(--bg-surface-hover);
border-color: var(--accent);
}
.tag-pill.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
/* Category pills (📁) - teal */
.tag-pill.category {
background: rgba(20, 184, 166, 0.2);
border-color: rgba(20, 184, 166, 0.5);
color: #14b8a6;
}
.tag-pill.category:hover {
background: rgba(20, 184, 166, 0.3);
}
.tag-pill.category.active {
background: #14b8a6;
border-color: #14b8a6;
color: white;
}
/* Domain pills (@) - purple */
.tag-pill.domain {
background: rgba(139, 92, 246, 0.2);
border-color: rgba(139, 92, 246, 0.5);
color: #8b5cf6;
}
.tag-pill.domain:hover {
background: rgba(139, 92, 246, 0.3);
}
.tag-pill.domain.active {
background: #8b5cf6;
border-color: #8b5cf6;
color: white;
}
/* Light mode adjustments */
[data-theme="light"] .tag-pill.category {
color: #0d9488;
}
[data-theme="light"] .tag-pill.domain {
color: #7c3aed;
}
.tag-pill-count {
font-size: var(--text-xs);
opacity: 0.7;
}
.clear-filters {
font-size: var(--text-xs);
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
padding: var(--space-2);
margin-left: var(--space-2);
}
.clear-filters:hover {
color: var(--accent);
}
/* No results */
.no-results {
text-align: center;
padding: var(--space-10);
color: var(--text-muted);
}
.no-results svg {
width: 48px;
height: 48px;
margin-bottom: var(--space-4);
opacity: 0.5;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: stretch;
}
.search-bar { width: 100%; }
.notes-grid {
grid-template-columns: 1fr;
}
.note-viewer-content {
margin: 0;
border-radius: 0;
min-height: 100vh;
}
}
</style>
</head>
<body>
<header class="header">
<a href="index.html" class="logo">
<i data-lucide="circle-dot"></i>
Echo
</a>
<nav class="nav">
<a href="index.html" class="nav-item">
<i data-lucide="layout-list"></i>
<span>Tasks</span>
</a>
<a href="notes.html" class="nav-item active">
<i data-lucide="file-text"></i>
<span>Notes</span>
</a>
<a href="files.html" class="nav-item">
<i data-lucide="folder"></i>
<span>Files</span>
</a>
<a href="grup-sprijin.html" class="nav-item">
<i data-lucide="heart-handshake"></i>
<span>Grup</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
<i data-lucide="sun" id="themeIcon"></i>
</button>
</nav>
</header>
<main class="main">
<div class="page-header">
<h1 class="page-title">Notes</h1>
<div class="search-bar">
<input type="text" class="input" id="searchInput" placeholder="Caută în notițe..." oninput="filterNotes()">
</div>
</div>
<div class="tag-filter">
<div class="tag-pills" id="mainPills"></div>
<div class="tag-pills-more" id="tagPills"></div>
</div>
<div id="notesContainer">
<div class="no-results">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</main>
<!-- Note viewer overlay -->
<div class="note-viewer" id="noteViewer" onclick="if(event.target === this) closeNote()">
<div class="note-viewer-content">
<div class="note-viewer-header">
<h2 id="viewerTitle">Titlu</h2>
<button class="btn btn-ghost" onclick="closeNote()">
<i data-lucide="x"></i>
</button>
</div>
<div class="note-viewer-body">
<div id="viewerContent" class="markdown-body"></div>
</div>
</div>
</div>
<script>
// Theme
function initTheme() {
const saved = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
updateThemeIcon(saved);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (icon) {
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
lucide.createIcons();
}
}
initTheme();
lucide.createIcons();
const notesCache = {};
let notesIndex = [];
let lastFilteredNotes = null;
const notesBasePath = "notes-data/";
const indexPath = notesBasePath + "index.json";
// Load notes index from JSON
async function loadNotesIndex() {
try {
const response = await fetch(indexPath + '?t=' + Date.now());
const data = await response.json();
notesIndex = Array.isArray(data) ? data : (data.notes || []);
console.log(`Loaded ${notesIndex.length} notes from index.json`);
} catch (e) {
console.error('Failed to load notes index:', e);
notesIndex = [];
}
}
let selectedTags = new Set();
// Extract all tags with counts (including domains and categories)
function getAllTags() {
const tagCounts = {};
notesIndex.forEach(note => {
// Category tags (📁youtube, 📁retete)
if (note.category) {
const catTag = '📁' + note.category;
tagCounts[catTag] = (tagCounts[catTag] || 0) + 1;
}
// Domain tags (@work, @health, etc.)
if (note.domains) {
note.domains.forEach(domain => {
const domainTag = '@' + domain;
tagCounts[domainTag] = (tagCounts[domainTag] || 0) + 1;
});
}
// Regular tags
note.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
// Sort: categories first (📁), then domains (@), then by count
return Object.entries(tagCounts)
.sort((a, b) => {
const aIsCat = a[0].startsWith('📁');
const bIsCat = b[0].startsWith('📁');
const aIsDomain = a[0].startsWith('@');
const bIsDomain = b[0].startsWith('@');
if (aIsCat && !bIsCat) return -1;
if (!aIsCat && bIsCat) return 1;
if (aIsDomain && !bIsDomain) return -1;
if (!aIsDomain && bIsDomain) return 1;
return b[1] - a[1];
})
.map(([tag, count]) => ({ tag, count }));
}
// Render tag pills
function renderTagPills(visibleNotes = null) {
const mainContainer = document.getElementById('mainPills');
const moreContainer = document.getElementById('tagPills');
const tags = getAllTags();
// Calculate which tags appear in visible notes
const visibleTags = new Set();
if (visibleNotes && visibleNotes.length > 0) {
visibleNotes.forEach(note => {
note.tags.forEach(t => visibleTags.add(t));
(note.domains || []).forEach(d => visibleTags.add('@' + d));
if (note.category) visibleTags.add('📁' + note.category);
});
}
const hasFilter = visibleNotes !== null;
// Separate: categories + domains vs regular tags
const mainTags = tags.filter(({tag}) => tag.startsWith('📁') || tag.startsWith('@'));
const moreTags = tags.filter(({tag}) => !tag.startsWith('📁') && !tag.startsWith('@'));
// Check if more section is expanded
const isExpanded = moreContainer.classList.contains('expanded');
const activeMoreCount = [...selectedTags].filter(t => !t.startsWith('📁') && !t.startsWith('@')).length;
// Render main pills (categories + domains)
let mainHtml = mainTags.map(({ tag, count }) => {
let pillClass = 'tag-pill';
if (tag.startsWith('📁')) pillClass += ' category';
else if (tag.startsWith('@')) pillClass += ' domain';
if (selectedTags.has(tag)) pillClass += ' active';
// Dim tags not in visible notes (unless it's already selected)
if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed';
return `<span class="${pillClass}" onclick="toggleTag('${tag}')">
${tag} <span class="tag-pill-count">(${count})</span>
</span>`;
}).join('');
// Add "more tags" toggle button inline
if (moreTags.length > 0) {
const visibleMoreCount = moreTags.filter(({tag}) => visibleTags.has(tag)).length;
const moreLabel = activeMoreCount > 0
? `+${moreTags.length} tags (${activeMoreCount} active)`
: (hasFilter && visibleMoreCount > 0 ? `+${visibleMoreCount}/${moreTags.length} tags` : `+${moreTags.length} tags`);
mainHtml += `<span class="tag-pill more-toggle ${isExpanded ? 'expanded' : ''}" onclick="toggleMoreTags()">
${isExpanded ? '' : '+'} ${moreLabel}
</span>`;
}
if (selectedTags.size > 0) {
mainHtml += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`;
}
mainContainer.innerHTML = mainHtml;
// Render more pills (regular tags)
const moreHtml = moreTags.map(({ tag, count }) => {
let pillClass = 'tag-pill';
if (selectedTags.has(tag)) pillClass += ' active';
if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed';
return `<span class="${pillClass}" onclick="toggleTag('${tag}')">
${tag} <span class="tag-pill-count">(${count})</span>
</span>`;
}).join('');
moreContainer.innerHTML = moreHtml;
}
// Toggle tag selection
function toggleTag(tag) {
if (selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
selectedTags.add(tag);
}
renderTagPills();
filterNotes();
}
// Clear all tag filters
function clearTagFilters() {
selectedTags.clear();
renderTagPills();
filterNotes();
}
// Toggle more tags section
function toggleMoreTags() {
const moreContainer = document.getElementById('tagPills');
moreContainer.classList.toggle('expanded');
renderTagPills(lastFilteredNotes);
}
// Filter notes by search and tags
function filterNotes() {
const query = document.getElementById('searchInput').value.toLowerCase().trim();
let filtered = notesIndex;
// Filter by selected tags (AND logic) - includes categories and domains
if (selectedTags.size > 0) {
filtered = filtered.filter(note => {
const allNoteTags = [
...note.tags,
...(note.domains || []).map(d => '@' + d),
note.category ? '📁' + note.category : null
].filter(Boolean);
return [...selectedTags].every(tag => allNoteTags.includes(tag));
});
}
// Filter by search query
if (query) {
filtered = filtered.filter(note => {
const titleMatch = note.title.toLowerCase().includes(query);
const tagsMatch = note.tags.some(t => t.toLowerCase().includes(query));
const contentMatch = (notesCache[note.file] || '').toLowerCase().includes(query);
return titleMatch || tagsMatch || contentMatch;
});
}
renderNotesAccordion(filtered);
// Save filtered notes for tag pills
lastFilteredNotes = filtered;
// Update tag pills to show which tags are in visible notes
renderTagPills(filtered);
}
// Group notes by date category
function groupNotesByDate(notes) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
const groups = {
today: { label: 'Azi', sublabel: formatDate(today), notes: [], expanded: true },
yesterday: { label: 'Ieri', sublabel: formatDate(yesterday), notes: [], expanded: false },
thisWeek: { label: 'Săptămâna aceasta', sublabel: '', notes: [], expanded: false },
older: { label: 'Mai vechi', sublabel: '', notes: [], expanded: false }
};
notes.forEach(note => {
const noteDate = new Date(note.date);
noteDate.setHours(0, 0, 0, 0);
if (noteDate.getTime() === today.getTime()) {
groups.today.notes.push(note);
} else if (noteDate.getTime() === yesterday.getTime()) {
groups.yesterday.notes.push(note);
} else if (noteDate >= weekAgo) {
groups.thisWeek.notes.push(note);
} else {
groups.older.notes.push(note);
}
});
return groups;
}
function formatDate(date) {
return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short', year: 'numeric' });
}
function renderNotesAccordion(notes = notesIndex) {
const container = document.getElementById('notesContainer');
if (notes.length === 0) {
container.innerHTML = `
<div class="no-results">
<i data-lucide="search-x"></i>
<p>Nicio notiță găsită</p>
</div>
`;
lucide.createIcons();
return;
}
const groups = groupNotesByDate(notes);
let html = '';
Object.entries(groups).forEach(([key, group]) => {
if (group.notes.length === 0) return;
const collapsed = group.expanded ? '' : 'collapsed';
html += `
<div class="date-section ${collapsed}" data-section="${key}">
<div class="date-header" onclick="toggleSection('${key}')">
<div class="date-header-left">
<i data-lucide="chevron-down"></i>
<span class="date-label">${group.label}</span>
<span class="date-sublabel">${group.sublabel}</span>
</div>
<span class="badge">${group.notes.length}</span>
</div>
<div class="date-content">
<div class="notes-grid">
${group.notes.map(note => renderNoteCard(note)).join('')}
</div>
</div>
</div>
`;
});
container.innerHTML = html;
lucide.createIcons();
}
function renderNoteCard(note) {
return `
<div class="note-card" onclick="openNote('${note.file}')">
<div class="note-title">${note.title}</div>
<div class="note-tags">
${note.tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
}
function toggleSection(sectionKey) {
const section = document.querySelector(`[data-section="${sectionKey}"]`);
section.classList.toggle('collapsed');
}
async function openNote(file) {
const note = notesIndex.find(n => n.file === file);
if (!note) return;
document.getElementById('viewerTitle').textContent = note.title;
document.getElementById('viewerContent').innerHTML = '<p style="color: var(--text-muted)">Se încarcă...</p>';
document.getElementById('noteViewer').classList.add('active');
document.body.style.overflow = 'hidden';
// Update URL
const noteId = file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
history.replaceState(null, '', '#' + noteId);
try {
let content = notesCache[file];
if (!content) {
const response = await fetch(notesBasePath + file);
content = await response.text();
notesCache[file] = content;
}
document.getElementById('viewerContent').innerHTML = marked.parse(content);
} catch (error) {
document.getElementById('viewerContent').innerHTML = `<p style="color: var(--error)">Eroare: ${error.message}</p>`;
}
}
function closeNote() {
document.getElementById('noteViewer').classList.remove('active');
document.body.style.overflow = '';
history.replaceState(null, '', window.location.pathname);
}
async function preloadNotes() {
for (const note of notesIndex) {
try {
const response = await fetch(notesBasePath + note.file);
notesCache[note.file] = await response.text();
} catch (e) {
notesCache[note.file] = '';
}
}
}
// searchNotes replaced by filterNotes above
// Handle hash for deep links
function checkHash() {
if (window.location.hash) {
const id = window.location.hash.slice(1);
const note = notesIndex.find(n => {
const noteId = n.file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
return noteId === id;
});
if (note) {
openNote(note.file);
}
}
}
// ESC to close
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeNote();
});
// Init - load index first, then render
async function init() {
await loadNotesIndex();
renderTagPills();
renderNotesAccordion();
preloadNotes();
checkHash();
}
init();
</script>
</body>
</html>