Initial slim mission control
This commit is contained in:
+302
@@ -0,0 +1,302 @@
|
||||
const states = ["Backlog", "In Progress", "Review", "Done"];
|
||||
const priorities = ["Low", "Normal", "High", "Urgent"];
|
||||
|
||||
const app = document.querySelector("#app");
|
||||
const state = {
|
||||
user: null,
|
||||
tasks: [],
|
||||
settings: null,
|
||||
openClaw: null,
|
||||
view: "board",
|
||||
error: ""
|
||||
};
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json", ...(options.headers || {}) },
|
||||
...options
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const session = await api("/api/session");
|
||||
state.user = session.user;
|
||||
await Promise.all([loadTasks(), loadSettings()]);
|
||||
renderApp();
|
||||
} catch {
|
||||
renderLogin();
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogin() {
|
||||
app.innerHTML = `
|
||||
<section class="login-shell">
|
||||
<form class="login" id="login-form">
|
||||
<h1>Maxi Mission Control</h1>
|
||||
<p>Schlanke Kommandozentrale fuer Aufgaben und OpenClaw-Status.</p>
|
||||
<label class="field">E-Mail
|
||||
<input name="email" type="email" autocomplete="username" required autofocus>
|
||||
</label>
|
||||
<label class="field">Passwort
|
||||
<input name="password" type="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button class="primary field" type="submit">Anmelden</button>
|
||||
<div class="error" id="login-error" role="alert"></div>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
document.querySelector("#login-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
try {
|
||||
const result = await api("/api/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: form.get("email"), password: form.get("password") })
|
||||
});
|
||||
state.user = result.user;
|
||||
await Promise.all([loadTasks(), loadSettings()]);
|
||||
renderApp();
|
||||
} catch (error) {
|
||||
document.querySelector("#login-error").textContent = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
const result = await api("/api/tasks");
|
||||
state.tasks = result.tasks;
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const result = await api("/api/settings");
|
||||
state.settings = result.settings;
|
||||
state.openClaw = result.openClaw;
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
app.innerHTML = `
|
||||
<section class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<strong>Maxi Mission Control</strong>
|
||||
<span>mc.texisoft.com</span>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<button data-view="board" class="${state.view === "board" ? "active" : ""}" title="Taskboard">▦ Taskboard</button>
|
||||
<button data-view="settings" class="${state.view === "settings" ? "active" : ""}" title="Einstellungen">⚙ Einstellungen</button>
|
||||
</nav>
|
||||
<div class="user">
|
||||
<div><strong>${escapeHtml(state.user?.name || "Maxi")}</strong><br><small>${escapeHtml(state.user?.role || "Admin")}</small></div>
|
||||
<button class="ghost" id="logout">Abmelden</button>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="content">
|
||||
${state.view === "settings" ? settingsView() : boardView()}
|
||||
</section>
|
||||
</section>
|
||||
${taskDialog()}
|
||||
`;
|
||||
|
||||
document.querySelectorAll("[data-view]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
state.view = button.dataset.view;
|
||||
if (state.view === "settings") await loadSettings();
|
||||
renderApp();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("#logout").addEventListener("click", async () => {
|
||||
await api("/api/logout", { method: "POST" });
|
||||
state.user = null;
|
||||
renderLogin();
|
||||
});
|
||||
|
||||
if (state.view === "board") bindBoard();
|
||||
if (state.view === "settings") bindSettings();
|
||||
}
|
||||
|
||||
function boardView() {
|
||||
const connected = state.openClaw?.ok;
|
||||
return `
|
||||
<header class="topbar">
|
||||
<div class="view-title">
|
||||
<h1>Taskboard</h1>
|
||||
<p>${state.tasks.length} Aufgaben · Assignee ${escapeHtml(state.settings?.defaultAssignee || "luna")}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="status-pill"><span class="dot ${connected ? "ok" : ""}"></span>OpenClaw ${connected ? "verbunden" : "pruefen"}</span>
|
||||
<button class="primary" id="new-task">+ Aufgabe</button>
|
||||
</div>
|
||||
</header>
|
||||
<section class="board">
|
||||
${states.map((name) => column(name)).join("")}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function column(name) {
|
||||
const tasks = state.tasks.filter((task) => task.state === name);
|
||||
return `
|
||||
<section class="column" data-state="${name}">
|
||||
<h2>${name}<span class="count">${tasks.length}</span></h2>
|
||||
${tasks.map(taskCard).join("") || `<p class="meta">Keine Aufgaben</p>`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function taskCard(task) {
|
||||
const index = states.indexOf(task.state);
|
||||
return `
|
||||
<article class="task">
|
||||
<div>
|
||||
<h3>${escapeHtml(task.title)}</h3>
|
||||
${task.detail ? `<p>${escapeHtml(task.detail)}</p>` : ""}
|
||||
</div>
|
||||
<div class="task-footer">
|
||||
<span class="badge">${escapeHtml(task.priority)}</span>
|
||||
<div class="task-controls">
|
||||
<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>
|
||||
<span class="meta">${escapeHtml(task.assignee || "luna")}</span>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function taskDialog() {
|
||||
return `
|
||||
<dialog id="task-dialog">
|
||||
<form id="task-form">
|
||||
<h2 id="dialog-title">Aufgabe</h2>
|
||||
<input type="hidden" name="id">
|
||||
<label class="field">Titel
|
||||
<input name="title" required>
|
||||
</label>
|
||||
<label class="field">Details
|
||||
<textarea name="detail"></textarea>
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label class="field">Status
|
||||
<select name="state">${states.map((x) => `<option>${x}</option>`).join("")}</select>
|
||||
</label>
|
||||
<label class="field">Prioritaet
|
||||
<select name="priority">${priorities.map((x) => `<option>${x}</option>`).join("")}</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">Assignee
|
||||
<input name="assignee" value="${escapeHtml(state.settings?.defaultAssignee || "luna")}">
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button class="ghost" type="button" id="cancel-task">Abbrechen</button>
|
||||
<button class="primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindBoard() {
|
||||
const dialog = document.querySelector("#task-dialog");
|
||||
const form = document.querySelector("#task-form");
|
||||
document.querySelector("#new-task").addEventListener("click", () => {
|
||||
form.reset();
|
||||
form.elements.assignee.value = state.settings?.defaultAssignee || "luna";
|
||||
dialog.showModal();
|
||||
});
|
||||
document.querySelector("#cancel-task").addEventListener("click", () => dialog.close());
|
||||
document.querySelectorAll("[data-move]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
await api(`/api/tasks/${button.dataset.move}`, { method: "PATCH", body: JSON.stringify({ state: button.dataset.state }) });
|
||||
await loadTasks();
|
||||
renderApp();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-edit]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const task = state.tasks.find((x) => x.id === button.dataset.edit);
|
||||
form.elements.id.value = task.id;
|
||||
form.elements.title.value = task.title;
|
||||
form.elements.detail.value = task.detail || "";
|
||||
form.elements.state.value = task.state;
|
||||
form.elements.priority.value = task.priority;
|
||||
form.elements.assignee.value = task.assignee || "";
|
||||
dialog.showModal();
|
||||
});
|
||||
});
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
const path = data.id ? `/api/tasks/${data.id}` : "/api/tasks";
|
||||
await api(path, { method: data.id ? "PATCH" : "POST", body: JSON.stringify(data) });
|
||||
dialog.close();
|
||||
await loadTasks();
|
||||
renderApp();
|
||||
});
|
||||
}
|
||||
|
||||
function settingsView() {
|
||||
const openClaw = state.openClaw || {};
|
||||
return `
|
||||
<header class="topbar">
|
||||
<div class="view-title">
|
||||
<h1>Einstellungen</h1>
|
||||
<p>Admin-Konfiguration und Maxi-OpenClaw-Verbindung.</p>
|
||||
</div>
|
||||
<button class="ghost" id="refresh-settings">Aktualisieren</button>
|
||||
</header>
|
||||
<section class="panel">
|
||||
<h2>OpenClaw</h2>
|
||||
<p class="status-pill"><span class="dot ${openClaw.ok ? "ok" : ""}"></span>${openClaw.ok ? "Verbunden" : "Nicht verbunden"}</p>
|
||||
<p class="meta">Gateway: ${escapeHtml(openClaw.baseUrl || state.settings?.openClawBaseUrl || "")}</p>
|
||||
<p class="meta">Health: ${escapeHtml(String(openClaw.healthStatus || openClaw.error || "unbekannt"))} · Tool: ${escapeHtml(String(openClaw.toolResult || "nicht geprueft"))} · ${escapeHtml(String(openClaw.latencyMs || 0))} ms</p>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<h2>Board</h2>
|
||||
<form id="settings-form">
|
||||
<div class="grid">
|
||||
<label class="field">Default-Assignee
|
||||
<input name="defaultAssignee" value="${escapeHtml(state.settings?.defaultAssignee || "luna")}">
|
||||
</label>
|
||||
<label class="field">Zeitzone
|
||||
<input name="timezone" value="${escapeHtml(state.settings?.timezone || "Europe/Berlin")}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindSettings() {
|
||||
document.querySelector("#refresh-settings").addEventListener("click", async () => {
|
||||
await loadSettings();
|
||||
renderApp();
|
||||
});
|
||||
document.querySelector("#settings-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
await api("/api/settings", { method: "PATCH", body: JSON.stringify(data) });
|
||||
await loadSettings();
|
||||
renderApp();
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Maxi Mission Control</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app"></main>
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,407 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7f8;
|
||||
--panel: #ffffff;
|
||||
--panel-soft: #eef3f1;
|
||||
--text: #17201c;
|
||||
--muted: #66736d;
|
||||
--line: #d9e1de;
|
||||
--accent: #256f5b;
|
||||
--accent-strong: #174c3e;
|
||||
--warn: #a45b14;
|
||||
--danger: #9d2f2f;
|
||||
--ok: #1c7a4c;
|
||||
--shadow: 0 10px 28px rgba(23, 32, 28, 0.08);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
button, input, textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login {
|
||||
width: min(420px, 100%);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.login h1, .view-title h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.login p, .view-title p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
padding: 9px 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 84px;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 111, 91, 0.14);
|
||||
}
|
||||
|
||||
.primary, .ghost, .danger {
|
||||
min-height: 38px;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: #fff1f1;
|
||||
color: var(--danger);
|
||||
border: 1px solid #edc9c9;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--line);
|
||||
background: #fff;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.brand strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.brand span, .user small, .meta {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nav button.active {
|
||||
background: var(--panel-soft);
|
||||
color: var(--accent-strong);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.user {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-width: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--warn);
|
||||
}
|
||||
|
||||
.dot.ok {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-width: 0;
|
||||
background: #ecf1ef;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.column h2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.task {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 14px rgba(23, 32, 28, 0.04);
|
||||
}
|
||||
|
||||
.task h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.task p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-footer {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--panel-soft);
|
||||
color: var(--accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.panel + .panel {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 12px;
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(18, 26, 23, 0.35);
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.nav, .user {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.board {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.board, .grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-height: 260px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user