Initial commit - workspace setup

- AGENTS.md, SOUL.md, USER.md, IDENTITY.md
- ANAF monitor (declarații fiscale)
- Kanban board + Notes UI
- Email tools
- Memory system
This commit is contained in:
Echo
2026-01-29 13:11:59 +00:00
commit f9912e0081
52 changed files with 23148 additions and 0 deletions

725
kanban/index.html Normal file
View File

@@ -0,0 +1,725 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo · Tasks</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>
<style>
/* Page-specific styles */
.main {
max-width: 1400px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
margin-bottom: var(--space-6);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.page-subtitle {
font-size: var(--text-sm);
color: var(--text-muted);
}
.search-bar {
max-width: 400px;
margin-bottom: var(--space-6);
}
/* Kanban board */
.board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: var(--space-5);
}
.column {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
min-height: 200px;
}
.column-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.column-header:hover {
filter: brightness(1.1);
}
/* Column header colors */
.column.backlog .column-header {
background: linear-gradient(135deg, rgba(100, 100, 120, 0.3), rgba(80, 80, 100, 0.2));
border-left: 3px solid #6b7280;
}
.column.in-progress .column-header {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.15));
border-left: 3px solid #3b82f6;
}
.column.done .column-header {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.25), rgba(22, 163, 74, 0.15));
border-left: 3px solid #22c55e;
}
.column-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
}
.column-title svg {
width: 16px;
height: 16px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.column.collapsed .column-title svg {
transform: rotate(-90deg);
}
.column-body {
padding: var(--space-3);
}
.column.collapsed .column-body {
display: none;
}
.column.collapsed {
min-height: auto;
}
/* Tasks */
.task {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-3);
margin-bottom: var(--space-2);
cursor: grab;
transition: all var(--transition-fast);
}
.task:hover {
border-color: var(--border-focus);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.task.dragging {
opacity: 0.5;
}
.task-title {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.task-desc {
font-size: var(--text-xs);
color: var(--text-muted);
display: none;
margin-bottom: var(--space-2);
}
.task.expanded .task-desc {
display: block;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-date {
font-size: var(--text-xs);
color: var(--text-muted);
}
.task-priority {
width: 8px;
height: 8px;
border-radius: 50%;
}
.priority-high { background: var(--error); }
.priority-medium { background: var(--warning); }
.priority-low { background: var(--success); }
.task-actions {
display: none;
gap: var(--space-1);
}
.task:hover .task-actions {
display: flex;
}
.task-action {
padding: var(--space-1);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
}
.task-action:hover {
background: var(--bg-surface);
color: var(--text-primary);
}
.task-action svg {
width: 14px;
height: 14px;
}
/* Add task */
.add-task-btn {
width: 100%;
padding: var(--space-3);
background: transparent;
border: 1px dashed var(--border);
border-radius: var(--radius-md);
color: var(--text-muted);
font-size: var(--text-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
transition: all var(--transition-fast);
}
.add-task-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-subtle);
}
.add-task-btn svg {
width: 16px;
height: 16px;
}
.add-form {
display: none;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3);
background: var(--bg-elevated);
border-radius: var(--radius-md);
margin-top: var(--space-2);
}
.add-form.active {
display: flex;
}
.add-form-actions {
display: flex;
gap: var(--space-2);
}
/* Stats */
.stats {
display: flex;
gap: var(--space-6);
padding: var(--space-5);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
margin-top: var(--space-6);
justify-content: center;
}
.stat {
text-align: center;
}
.stat-value {
font-size: var(--text-xl);
font-weight: 700;
color: var(--accent);
}
.stat-label {
font-size: var(--text-xs);
color: var(--text-muted);
}
/* Drag over */
.column.drag-over {
background: var(--accent-subtle);
}
/* Toast */
.toast {
position: fixed;
bottom: var(--space-5);
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: var(--space-3) var(--space-5);
border-radius: var(--radius-md);
font-size: var(--text-sm);
opacity: 0;
transition: all var(--transition-base);
z-index: 1000;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Hidden on search */
.task.hidden { display: none; }
@media (max-width: 768px) {
.board {
grid-template-columns: 1fr;
}
.stats {
flex-wrap: wrap;
}
}
</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 active">
<i data-lucide="layout-list"></i>
<span>Tasks</span>
</a>
<a href="notes.html" class="nav-item">
<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>
<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">Task Board</h1>
<p class="page-subtitle" id="lastUpdated">Se încarcă...</p>
</div>
<div class="search-bar">
<input type="text" class="input" id="searchInput" placeholder="Caută task-uri..." oninput="searchTasks()">
</div>
<div class="board" id="board"></div>
<div class="stats" id="stats"></div>
</main>
<div class="toast" id="toast"></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();
// Init Lucide icons
lucide.createIcons();
let tasksData = null;
let draggedTask = null;
let saveTimeout = null;
const DONE_LIMIT = 5;
let showAllDone = false;
// Collapsed columns state - default: only in-progress expanded
function getCollapsedColumns() {
try {
const saved = localStorage.getItem('collapsedColumns');
if (saved) return JSON.parse(saved);
return ['backlog', 'done'];
} catch { return ['backlog', 'done']; }
}
function setCollapsedColumns(cols) {
localStorage.setItem('collapsedColumns', JSON.stringify(cols));
}
function toggleColumnCollapse(columnId) {
const cols = getCollapsedColumns();
const idx = cols.indexOf(columnId);
if (idx > -1) cols.splice(idx, 1);
else cols.push(columnId);
setCollapsedColumns(cols);
renderBoard();
}
function toggleTaskExpand(e, taskEl) {
if (e.target.hasAttribute('contenteditable') || e.target.tagName === 'BUTTON') return;
taskEl.classList.toggle('expanded');
}
async function loadTasks() {
try {
const response = await fetch('tasks.json?' + Date.now());
tasksData = await response.json();
renderBoard();
} catch (error) {
showToast('Eroare la încărcare', 'error');
}
}
function searchTasks() {
const query = document.getElementById('searchInput').value.toLowerCase().trim();
document.querySelectorAll('.task').forEach(taskEl => {
const title = taskEl.querySelector('.task-title')?.textContent.toLowerCase() || '';
const desc = taskEl.querySelector('.task-desc')?.textContent.toLowerCase() || '';
taskEl.classList.toggle('hidden', query && !title.includes(query) && !desc.includes(query));
});
}
function renderBoard() {
const board = document.getElementById('board');
const stats = document.getElementById('stats');
const lastUpdated = document.getElementById('lastUpdated');
const date = new Date(tasksData.lastUpdated);
lastUpdated.textContent = `Actualizat: ${date.toLocaleString('ro-RO')}`;
board.innerHTML = '';
let totalTasks = 0, doneTasks = 0, inProgress = 0;
const icons = {
'backlog': 'inbox',
'in-progress': 'loader',
'done': 'check-circle'
};
tasksData.columns.forEach(column => {
column.tasks.sort((a, b) => {
const dateA = a.completed || a.created || '';
const dateB = b.completed || b.created || '';
return dateB.localeCompare(dateA);
});
totalTasks += column.tasks.length;
if (column.id === 'done') doneTasks = column.tasks.length;
if (column.id === 'in-progress') inProgress = column.tasks.length;
const isCollapsed = getCollapsedColumns().includes(column.id);
let visibleTasks = column.tasks;
let hasMore = false;
if (column.id === 'done' && !showAllDone && column.tasks.length > DONE_LIMIT) {
visibleTasks = column.tasks.slice(0, DONE_LIMIT);
hasMore = true;
}
const columnEl = document.createElement('div');
columnEl.className = `column ${column.id}${isCollapsed ? ' collapsed' : ''}`;
columnEl.dataset.columnId = column.id;
columnEl.innerHTML = `
<div class="column-header" onclick="toggleColumnCollapse('${column.id}')">
<div class="column-title">
<i data-lucide="chevron-down"></i>
${column.name}
</div>
<span class="badge">${column.tasks.length}</span>
</div>
<div class="column-body">
<div class="tasks-container" data-column-id="${column.id}">
${visibleTasks.map(task => renderTask(task)).join('')}
</div>
${hasMore ? `
<button class="add-task-btn" onclick="event.stopPropagation(); showAllDone=true; renderBoard();">
<i data-lucide="chevron-down"></i>
${column.tasks.length - DONE_LIMIT} mai multe
</button>
` : ''}
${column.id === 'done' && showAllDone && column.tasks.length > DONE_LIMIT ? `
<button class="add-task-btn" onclick="event.stopPropagation(); showAllDone=false; renderBoard();">
<i data-lucide="chevron-up"></i>
Arată mai puține
</button>
` : ''}
<button class="add-task-btn" onclick="event.stopPropagation(); showAddForm('${column.id}')">
<i data-lucide="plus"></i>
Adaugă task
</button>
<div class="add-form" id="form-${column.id}">
<input type="text" class="input" placeholder="Titlu..." id="title-${column.id}">
<textarea class="input" placeholder="Descriere..." id="desc-${column.id}" rows="2"></textarea>
<select class="input" id="priority-${column.id}">
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="low">Low</option>
</select>
<div class="add-form-actions">
<button class="btn btn-secondary" onclick="hideAddForm('${column.id}')">Anulează</button>
<button class="btn btn-primary" onclick="addTask('${column.id}')">Adaugă</button>
</div>
</div>
</div>
`;
board.appendChild(columnEl);
const container = columnEl.querySelector('.tasks-container');
container.addEventListener('dragover', handleDragOver);
container.addEventListener('drop', handleDrop);
container.addEventListener('dragleave', handleDragLeave);
});
document.querySelectorAll('.task').forEach(task => {
task.addEventListener('dragstart', handleDragStart);
task.addEventListener('dragend', handleDragEnd);
});
stats.innerHTML = `
<div class="stat"><div class="stat-value">${totalTasks}</div><div class="stat-label">Total</div></div>
<div class="stat"><div class="stat-value">${inProgress}</div><div class="stat-label">În progres</div></div>
<div class="stat"><div class="stat-value">${doneTasks}</div><div class="stat-label">Finalizate</div></div>
`;
lucide.createIcons();
searchTasks();
}
function renderTask(task) {
const hasDesc = task.description && task.description.trim();
return `
<div class="task" draggable="true" data-task-id="${task.id}" onclick="toggleTaskExpand(event, this)">
<div class="task-title" contenteditable="true" onblur="updateTask('${task.id}', 'title', this.textContent)">${task.title}</div>
${hasDesc ? `<div class="task-desc" contenteditable="true" onblur="updateTask('${task.id}', 'description', this.textContent)">${task.description}</div>` : ''}
<div class="task-footer">
<span class="task-date">${task.completed || task.created}</span>
<div class="task-actions">
<button class="task-action" onclick="event.stopPropagation(); cyclePriority('${task.id}')" title="Prioritate">
<i data-lucide="flag"></i>
</button>
<button class="task-action" onclick="event.stopPropagation(); deleteTask('${task.id}')" title="Șterge">
<i data-lucide="trash-2"></i>
</button>
</div>
<span class="task-priority priority-${task.priority || 'medium'}" title="${task.priority || 'medium'}"></span>
</div>
</div>
`;
}
// Drag & Drop
function handleDragStart(e) {
draggedTask = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.currentTarget.closest('.column').classList.add('drag-over');
}
function handleDragLeave(e) {
e.currentTarget.closest('.column').classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
const targetColumn = e.currentTarget.dataset.columnId;
const taskId = draggedTask.dataset.taskId;
for (const col of tasksData.columns) {
const taskIdx = col.tasks.findIndex(t => t.id === taskId);
if (taskIdx > -1) {
const [task] = col.tasks.splice(taskIdx, 1);
if (targetColumn === 'done' && !task.completed) {
task.completed = new Date().toISOString().split('T')[0];
} else if (targetColumn !== 'done') {
delete task.completed;
}
const targetCol = tasksData.columns.find(c => c.id === targetColumn);
targetCol.tasks.unshift(task);
break;
}
}
renderBoard();
scheduleSave();
}
function showAddForm(columnId) {
document.querySelectorAll('.add-form').forEach(f => f.classList.remove('active'));
document.getElementById(`form-${columnId}`).classList.add('active');
document.getElementById(`title-${columnId}`).focus();
}
function hideAddForm(columnId) {
document.getElementById(`form-${columnId}`).classList.remove('active');
}
function addTask(columnId) {
const title = document.getElementById(`title-${columnId}`).value.trim();
if (!title) return;
const desc = document.getElementById(`desc-${columnId}`).value.trim();
const priority = document.getElementById(`priority-${columnId}`).value;
const newTask = {
id: 'task-' + Date.now(),
title,
description: desc,
priority,
created: new Date().toISOString().split('T')[0]
};
const col = tasksData.columns.find(c => c.id === columnId);
col.tasks.unshift(newTask);
hideAddForm(columnId);
document.getElementById(`title-${columnId}`).value = '';
document.getElementById(`desc-${columnId}`).value = '';
renderBoard();
scheduleSave();
showToast('Task adăugat');
}
function updateTask(taskId, field, value) {
for (const col of tasksData.columns) {
const task = col.tasks.find(t => t.id === taskId);
if (task) {
task[field] = value.trim();
break;
}
}
scheduleSave();
}
function deleteTask(taskId) {
if (!confirm('Ștergi acest task?')) return;
for (const col of tasksData.columns) {
const idx = col.tasks.findIndex(t => t.id === taskId);
if (idx > -1) {
col.tasks.splice(idx, 1);
break;
}
}
renderBoard();
scheduleSave();
showToast('Task șters');
}
function cyclePriority(taskId) {
const priorities = ['low', 'medium', 'high'];
for (const col of tasksData.columns) {
const task = col.tasks.find(t => t.id === taskId);
if (task) {
const current = task.priority || 'medium';
const idx = priorities.indexOf(current);
task.priority = priorities[(idx + 1) % 3];
break;
}
}
renderBoard();
scheduleSave();
}
function scheduleSave() {
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveTasks, 1000);
}
async function saveTasks() {
tasksData.lastUpdated = new Date().toISOString();
try {
const response = await fetch('/echo/api/files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: 'kanban/tasks.json',
content: JSON.stringify(tasksData, null, 2)
})
});
if (!response.ok) throw new Error('Save failed');
} catch (error) {
showToast('Eroare la salvare', 'error');
}
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show status-${type}`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
loadTasks();
</script>
</body>
</html>