Match taskboard to Nexus dark board

This commit is contained in:
Iris
2026-06-26 15:37:16 +00:00
parent 2d906b9e1e
commit 1de4169303
4 changed files with 275 additions and 180 deletions
+33 -19
View File
@@ -130,10 +130,13 @@ function boardView() {
</div> </div>
<div class="actions"> <div class="actions">
<span class="status-pill"><span class="dot ${connected ? "ok" : ""}"></span>OpenClaw ${connected ? "verbunden" : "pruefen"}</span> <span class="status-pill"><span class="dot ${connected ? "ok" : ""}"></span>OpenClaw ${connected ? "verbunden" : "pruefen"}</span>
<button class="primary" id="new-task">+ Aufgabe</button>
</div> </div>
</header> </header>
<section class="board"> <form class="quick-create" id="quick-create">
<input name="title" autocomplete="off" maxlength="180" placeholder="New task title">
<button type="submit">Create task</button>
</form>
<section class="kanban">
${states.map((name) => column(name)).join("")} ${states.map((name) => column(name)).join("")}
</section> </section>
`; `;
@@ -142,8 +145,8 @@ function boardView() {
function column(name) { function column(name) {
const tasks = state.tasks.filter((task) => task.state === name); const tasks = state.tasks.filter((task) => task.state === name);
return ` return `
<section class="column" data-state="${name}" aria-label="${name}"> <section class="kanban-column" data-state="${name}" aria-label="${name}">
<h2><span>${name}</span><span class="count">${tasks.length}</span></h2> <header><span>${name}</span><b>${tasks.length}</b></header>
<div class="dropzone" data-state="${name}"> <div class="dropzone" data-state="${name}">
${tasks.map(taskCard).join("") || `<p class="empty-state">Keine Aufgaben</p>`} ${tasks.map(taskCard).join("") || `<p class="empty-state">Keine Aufgaben</p>`}
</div> </div>
@@ -152,24 +155,22 @@ function column(name) {
} }
function taskCard(task) { function taskCard(task) {
const index = states.indexOf(task.state);
return ` return `
<article class="task priority-${escapeHtml(task.priority).toLowerCase()}" draggable="true" data-task-id="${task.id}" tabindex="0" role="button" aria-label="Aufgabe ${escapeHtml(task.title)} oeffnen"> <article class="task-card priority-${escapeHtml(task.priority).toLowerCase()}" draggable="true" data-task-id="${task.id}" tabindex="0" role="button" aria-label="Aufgabe ${escapeHtml(task.title)} oeffnen">
<div class="task-card-head"> <div class="task-card-head">
<span class="badge">${escapeHtml(task.priority)}</span> <span class="priority ${escapeHtml(task.priority).toLowerCase()}">${escapeHtml(task.priority)}</span>
<span class="meta">${escapeHtml(task.assignee || "luna")}</span> <button class="task-edit-btn" data-edit="${task.id}" title="Bearbeiten" aria-label="Aufgabe bearbeiten">✎</button>
</div> </div>
<div> <div>
<h3>${escapeHtml(task.title)}</h3> <h3>${escapeHtml(task.title)}</h3>
${task.detail ? `<p>${escapeHtml(task.detail)}</p>` : ""} ${task.detail ? `<p>${escapeHtml(task.detail)}</p>` : ""}
</div> </div>
<select class="task-status-select" data-status-task="${task.id}" aria-label="Status fuer ${escapeHtml(task.title)}">
${states.map((x) => `<option value="${x}" ${x === task.state ? "selected" : ""}>${x}</option>`).join("")}
</select>
<div class="task-footer"> <div class="task-footer">
<time class="meta" datetime="${escapeHtml(task.updatedAt)}">${formatDate(task.updatedAt)}</time> <time class="meta" datetime="${escapeHtml(task.updatedAt)}">${formatDate(task.updatedAt)}</time>
<div class="task-controls"> <span class="meta">${escapeHtml(task.assignee || "luna")}</span>
<button class="icon-button" data-move="${task.id}" data-state="${states[Math.max(0, index - 1)]}" title="Zurueck"></button>
<button class="icon-button" data-edit="${task.id}" title="Bearbeiten">✎</button>
<button class="icon-button" data-move="${task.id}" data-state="${states[Math.min(states.length - 1, index + 1)]}" title="Weiter"></button>
</div>
</div> </div>
</article> </article>
`; `;
@@ -232,18 +233,31 @@ function bindBoard() {
dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`; dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`;
dialog.showModal(); dialog.showModal();
}; };
document.querySelector("#new-task").addEventListener("click", () => { document.querySelector("#quick-create").addEventListener("submit", async (event) => {
resetDialog(); event.preventDefault();
dialog.showModal(); const formData = new FormData(event.currentTarget);
const title = String(formData.get("title") || "").trim();
if (!title) return;
await api("/api/tasks", {
method: "POST",
body: JSON.stringify({
title,
state: "Backlog",
priority: "Normal",
assignee: state.settings?.defaultAssignee || "luna"
})
});
await loadTasks();
renderApp();
}); });
document.querySelector("#cancel-task").addEventListener("click", () => dialog.close()); document.querySelector("#cancel-task").addEventListener("click", () => dialog.close());
dialog.addEventListener("click", (event) => { dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close(); if (event.target === dialog) dialog.close();
}); });
document.querySelectorAll("[data-move]").forEach((button) => { document.querySelectorAll("[data-status-task]").forEach((select) => {
button.addEventListener("click", async (event) => { select.addEventListener("change", async (event) => {
event.stopPropagation(); event.stopPropagation();
await api(`/api/tasks/${button.dataset.move}`, { method: "PATCH", body: JSON.stringify({ state: button.dataset.state }) }); await api(`/api/tasks/${select.dataset.statusTask}`, { method: "PATCH", body: JSON.stringify({ state: select.value }) });
await loadTasks(); await loadTasks();
renderApp(); renderApp();
}); });
+2 -2
View File
@@ -4,10 +4,10 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maxi Mission Control</title> <title>Maxi Mission Control</title>
<link rel="stylesheet" href="/styles.css"> <link rel="stylesheet" href="/styles.css?v=20260626-nexus-board">
</head> </head>
<body> <body>
<main id="app"></main> <main id="app"></main>
<script type="module" src="/app.js"></script> <script type="module" src="/app.js?v=20260626-nexus-board"></script>
</body> </body>
</html> </html>
+239 -158
View File
@@ -1,18 +1,20 @@
:root { :root {
color-scheme: light; color-scheme: dark;
--bg: #f3f5f8; --bg: #080a0f;
--panel: #ffffff; --panel: #10131a;
--panel-soft: #eef2f7; --panel-soft: #0d1016;
--text: #18212f; --card: #12151c;
--muted: #667085; --text: #e8eaf0;
--line: #d9e0ea; --muted: #7e8799;
--accent: #3268d8; --line: #202530;
--accent-strong: #224fa8; --line-strong: #292f3b;
--accent-soft: #e8efff; --accent: #8b7cf6;
--warn: #b7791f; --accent-strong: #b8adff;
--danger: #b42335; --accent-soft: rgba(139, 124, 246, 0.12);
--ok: #14845f; --warn: #e5b05e;
--shadow: 0 12px 30px rgba(24, 33, 47, 0.09); --danger: #ec7b82;
--ok: #51d49a;
--shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
} }
* { * {
@@ -21,10 +23,11 @@
body { body {
margin: 0; margin: 0;
min-width: 320px;
min-height: 100vh; min-height: 100vh;
background: var(--bg); background: radial-gradient(circle at 70% -20%, #17152b 0, transparent 34%), var(--bg);
color: var(--text); color: var(--text);
font: 15px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font: 12px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -33,7 +36,7 @@ button, input, textarea, select {
} }
button { button {
border: 0; color: inherit;
cursor: pointer; cursor: pointer;
} }
@@ -46,155 +49,176 @@ button {
.login { .login {
width: min(420px, 100%); width: min(420px, 100%);
background: var(--panel); padding: 32px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 12px;
background: linear-gradient(145deg, rgba(18, 21, 29, 0.98), rgba(10, 12, 18, 0.98));
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 28px;
} }
.login h1, .view-title h1 { .login h1, .view-title h1 {
margin: 0; margin: 0;
font-size: 28px; font-size: 26px;
line-height: 1.1; line-height: 1.1;
} }
.login p, .view-title p { .login p, .view-title p {
margin: 8px 0 0; margin: 8px 0 0;
color: var(--muted); color: var(--muted);
font-size: 11px;
} }
.field { .field {
display: grid; display: grid;
gap: 6px; gap: 7px;
margin-top: 16px; margin-top: 16px;
} }
label { label {
color: var(--muted); color: #aab1bf;
font-size: 13px; font-size: 10px;
font-weight: 650; font-weight: 650;
} }
input, textarea, select { input, textarea, select {
width: 100%; width: 100%;
min-height: 40px; min-height: 38px;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
color: var(--text);
padding: 9px 10px; padding: 9px 10px;
border: 1px solid var(--line-strong);
border-radius: 7px;
outline: none; outline: none;
color: #e7e9ef;
background: #0c0f14;
} }
textarea { textarea {
resize: vertical; resize: vertical;
min-height: 84px; min-height: 92px;
} }
input:focus, textarea:focus, select:focus { input:focus, textarea:focus, select:focus {
border-color: var(--accent); border-color: #6559b8;
box-shadow: 0 0 0 3px rgba(37, 111, 91, 0.14); box-shadow: 0 0 0 3px rgba(139, 124, 246, 0.1);
} }
.primary, .ghost, .danger { .primary, .ghost, .danger {
min-height: 38px; min-height: 38px;
border-radius: 6px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 7px;
font-weight: 700; font-weight: 700;
} }
.primary { .primary {
background: var(--accent); border: 1px solid #443d7c;
color: #fff; background: #211d39;
color: #c1b8ff;
} }
.primary:hover { .primary:hover {
background: var(--accent-strong); background: #29234a;
} }
.ghost { .ghost {
border: 1px solid var(--line); border: 1px solid var(--line);
background: #fff; background: var(--panel);
color: var(--text); color: #a5adba;
} }
.danger { .danger {
background: #fff1f1; border: 1px solid rgba(236, 123, 130, 0.3);
color: var(--danger); background: rgba(193, 70, 79, 0.12);
border: 1px solid #edc9c9; color: #efa2a7;
} }
.app-shell { .app-shell {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
grid-template-columns: 248px minmax(0, 1fr); grid-template-columns: 224px minmax(0, 1fr);
} }
.sidebar { .sidebar {
border-right: 1px solid var(--line); position: sticky;
background: #fff; top: 0;
padding: 20px 16px; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
padding: 22px 14px 14px;
border-right: 1px solid #1a1e27;
background: rgba(9, 11, 16, 0.94);
backdrop-filter: blur(18px);
} }
.brand { .brand {
display: grid; display: grid;
gap: 4px; gap: 3px;
padding: 0 8px 5px;
} }
.brand strong { .brand strong {
font-size: 18px; font-size: 13px;
letter-spacing: 0.14em;
text-transform: uppercase;
} }
.brand span, .user small, .meta { .brand span, .user small, .meta {
color: var(--muted); color: var(--muted);
font-size: 13px; font-size: 10px;
} }
.nav { .nav {
display: grid; display: flex;
gap: 8px; flex-direction: column;
gap: 3px;
} }
.nav button { .nav button {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 9px; gap: 10px;
width: 100%; width: 100%;
min-height: 40px; min-height: 34px;
border-radius: 6px; padding: 9px 10px;
border: 0;
border-radius: 7px;
background: transparent; background: transparent;
color: var(--text); color: #8991a1;
padding: 8px 10px;
text-align: left; text-align: left;
} }
.nav button:hover, .nav button.active {
background: var(--accent-soft);
color: #ececf5;
}
.nav button.active { .nav button.active {
background: var(--panel-soft); box-shadow: inset 2px 0 var(--accent);
color: var(--accent-strong);
font-weight: 750;
} }
.user { .user {
margin-top: auto; margin-top: auto;
display: grid; display: grid;
gap: 12px; gap: 12px;
padding-top: 10px;
border-top: 1px solid #1b1f28;
} }
.content { .content {
min-width: 0; min-width: 0;
padding: 24px; padding: 16px;
} }
.topbar { .topbar {
min-height: 62px;
display: flex; display: flex;
gap: 16px; gap: 16px;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: center;
margin-bottom: 20px; margin: -16px -16px 16px;
padding: 0 30px;
border-bottom: 1px solid #191d25;
background: rgba(8, 10, 15, 0.68);
backdrop-filter: blur(16px);
} }
.actions { .actions {
@@ -207,104 +231,118 @@ input:focus, textarea:focus, select:focus {
.status-pill { .status-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 7px;
min-height: 32px; min-height: 30px;
padding: 6px 10px; padding: 6px 10px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 999px; border-radius: 8px;
background: #fff; background: var(--panel);
color: var(--muted); color: #8c95a5;
font-size: 10px;
white-space: nowrap; white-space: nowrap;
} }
.dot { .dot {
width: 9px; width: 7px;
height: 9px; height: 7px;
border-radius: 50%; border-radius: 50%;
background: var(--warn); background: var(--warn);
} }
.dot.ok { .dot.ok {
background: var(--ok); background: var(--ok);
box-shadow: 0 0 7px rgba(81, 212, 154, 0.4);
} }
.board { .quick-create {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr)); grid-template-columns: minmax(180px, 360px) max-content;
gap: 8px;
align-items: stretch;
margin-bottom: 12px;
}
.quick-create input {
font-size: 10px;
}
.quick-create button {
min-width: 100px;
padding: 0 14px;
border: 1px solid #443d7c;
border-radius: 8px;
background: #211d39;
color: #c1b8ff;
font-size: 9px;
}
.kanban {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px; gap: 12px;
align-items: start; align-items: start;
} }
.column { .kanban-column {
min-width: 0; min-width: 0;
background: #edf1f6; min-height: 380px;
padding: 12px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 9px;
padding: 10px; background: rgba(14, 17, 23, 0.8);
min-height: 520px;
} }
.column h2 { .kanban-column > header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; padding: 5px 3px 14px;
margin: 0 0 10px; color: #aeb4c0;
font-size: 14px; font-size: 10px;
text-transform: uppercase;
color: var(--muted);
} }
.count { .kanban-column > header b {
display: inline-grid; padding: 1px 6px;
place-items: center; border-radius: 8px;
min-width: 24px; background: #242936;
height: 24px;
border-radius: 999px;
background: #fff;
color: var(--text);
} }
.dropzone { .dropzone {
min-height: 456px; min-height: 320px;
border-radius: 8px; border-radius: 8px;
transition: background 0.16s ease, box-shadow 0.16s ease; transition: background 0.16s ease, box-shadow 0.16s ease;
} }
.dropzone.drag-over { .dropzone.drag-over {
background: rgba(50, 104, 216, 0.08); background: rgba(139, 124, 246, 0.08);
box-shadow: inset 0 0 0 2px rgba(50, 104, 216, 0.28); box-shadow: inset 0 0 0 2px rgba(139, 124, 246, 0.25);
} }
.empty-state { .empty-state {
margin: 0; margin: 0;
padding: 32px 6px; padding: 35px 0;
text-align: center; text-align: center;
color: var(--muted); color: #596171;
font-size: 13px; font-size: 9px;
} }
.task { .task-card {
display: grid; margin-bottom: 9px;
gap: 10px; padding: 14px;
background: #fff; border: 1px solid #252a35;
border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
padding: 12px; background: var(--card);
margin-bottom: 10px;
box-shadow: 0 4px 14px rgba(23, 32, 28, 0.04);
cursor: grab; cursor: grab;
transition: border-color 0.14s ease, box-shadow 0.14s ease, transform 0.14s ease; transition: border-color 0.14s ease, transform 0.14s ease;
} }
.task:hover, .task-card:hover,
.task:focus-visible { .task-card:focus-visible {
border-color: rgba(50, 104, 216, 0.45); border-color: #343947;
box-shadow: 0 12px 24px rgba(24, 33, 47, 0.1);
outline: none; outline: none;
transform: translateY(-1px); transform: translateY(-1px);
} }
.task.dragging { .task-card.dragging {
opacity: 0.55; opacity: 0.55;
cursor: grabbing; cursor: grabbing;
} }
@@ -316,68 +354,99 @@ input:focus, textarea:focus, select:focus {
justify-content: space-between; justify-content: space-between;
} }
.task h3 { .task-card h3 {
margin: 0; margin: 10px 0 12px;
font-size: 15px; color: #eef0f5;
font-size: 11px;
line-height: 1.4;
overflow-wrap: anywhere;
} }
.task p { .task-card p {
margin: 0; margin: -4px 0 12px;
color: var(--muted); color: var(--muted);
font-size: 13px; font-size: 10px;
line-height: 1.45;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.task-card select {
width: 100%;
min-height: 32px;
margin-bottom: 12px;
padding: 7px 8px;
border: 1px solid #292f3b;
border-radius: 7px;
color: #c8ccd5;
background: #0c0f14;
font-size: 9px;
cursor: pointer;
}
.task-footer { .task-footer {
display: flex; display: flex;
gap: 6px; gap: 5px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: var(--muted);
font-size: 8px;
}
.priority {
color: #9b91e6;
font-size: 8px;
text-transform: uppercase;
}
.priority.urgent, .priority.high {
color: var(--warn);
}
.priority.low {
color: var(--ok);
}
.task-edit-btn {
width: 24px;
height: 24px;
display: grid;
place-items: center;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: #687181;
opacity: 0;
}
.task-card:hover .task-edit-btn,
.task-card:focus-within .task-edit-btn {
opacity: 1;
}
.task-edit-btn:hover {
border-color: #343947;
color: #c1b8ff;
background: #18152a;
} }
.badge { .badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-height: 24px; min-height: 22px;
padding: 3px 8px; padding: 3px 8px;
border-radius: 999px; border-radius: 10px;
background: var(--accent-soft); background: var(--accent-soft);
color: var(--accent-strong); color: var(--accent-strong);
font-size: 12px; font-size: 9px;
font-weight: 750; font-weight: 750;
} }
.priority-urgent .badge,
.priority-high .badge {
background: #fff3d8;
color: #8a4f00;
}
.priority-low .badge {
background: #e9f8f0;
color: #0d6b4d;
}
.task-controls {
display: flex;
gap: 4px;
}
.icon-button {
width: 30px;
height: 30px;
border-radius: 6px;
border: 1px solid var(--line);
background: #fff;
color: var(--text);
}
.panel { .panel {
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
max-width: 760px; max-width: 760px;
padding: 18px;
border: 1px solid var(--line);
border-radius: 9px;
background: linear-gradient(145deg, rgba(18, 21, 29, 0.96), rgba(12, 15, 21, 0.96));
} }
.panel + .panel { .panel + .panel {
@@ -398,14 +467,22 @@ input:focus, textarea:focus, select:focus {
dialog { dialog {
width: min(560px, calc(100vw - 32px)); width: min(560px, calc(100vw - 32px));
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 22px; padding: 22px;
border: 1px solid var(--line);
border-radius: 10px;
background: linear-gradient(145deg, rgba(18, 21, 29, 0.98), rgba(10, 12, 18, 0.98));
color: var(--text);
box-shadow: var(--shadow);
} }
dialog::backdrop { dialog::backdrop {
background: rgba(18, 26, 23, 0.35); background: rgba(0, 0, 0, 0.68);
backdrop-filter: blur(3px);
}
dialog h2 {
margin: 0;
font-size: 18px;
} }
.dialog-actions { .dialog-actions {
@@ -417,8 +494,9 @@ dialog::backdrop {
.dialog-meta { .dialog-meta {
min-height: 20px; min-height: 20px;
margin-top: 4px;
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 10px;
} }
@media (max-width: 980px) { @media (max-width: 980px) {
@@ -428,18 +506,19 @@ dialog::backdrop {
.sidebar { .sidebar {
position: sticky; position: sticky;
top: 0;
z-index: 2; z-index: 2;
height: auto;
border-right: 0; border-right: 0;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
padding: 12px; padding: 12px;
} }
.nav, .user { .nav, .user {
grid-template-columns: repeat(3, minmax(0, 1fr)); display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.board { .kanban {
grid-template-columns: repeat(2, minmax(220px, 1fr)); grid-template-columns: repeat(2, minmax(220px, 1fr));
} }
} }
@@ -451,17 +530,19 @@ dialog::backdrop {
.topbar { .topbar {
display: grid; display: grid;
margin: -16px -16px 16px;
padding: 16px;
} }
.actions { .actions {
justify-content: flex-start; justify-content: flex-start;
} }
.board, .grid { .quick-create, .kanban, .grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.column { .kanban-column {
min-height: 260px; min-height: 260px;
} }
} }
+1 -1
View File
@@ -317,7 +317,7 @@ async function serveStatic(req, res) {
const file = await readFile(filePath); const file = await readFile(filePath);
const ext = path.extname(filePath); const ext = path.extname(filePath);
const type = ext === ".html" ? "text/html; charset=utf-8" : ext === ".css" ? "text/css; charset=utf-8" : ext === ".js" ? "text/javascript; charset=utf-8" : "application/octet-stream"; const type = ext === ".html" ? "text/html; charset=utf-8" : ext === ".css" ? "text/css; charset=utf-8" : ext === ".js" ? "text/javascript; charset=utf-8" : "application/octet-stream";
send(res, 200, file, { "content-type": type, "cache-control": ext === ".html" ? "no-store" : "public, max-age=3600" }); send(res, 200, file, { "content-type": type, "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate" });
} catch { } catch {
const file = await readFile(path.join(publicDir, "index.html")); const file = await readFile(path.join(publicDir, "index.html"));
send(res, 200, file, { "content-type": "text/html; charset=utf-8" }); send(res, 200, file, { "content-type": "text/html; charset=utf-8" });