Files
Alem Tuzlak 79ce60c580 chore: migrate from eslint+prettier to oxlint+oxfmt
Replace eslint and prettier with oxlint and oxfmt for faster linting
and formatting across the monorepo. Remove all eslint and prettier
configs, dependencies, and related packages. Add .oxlintrc.json and
.oxfmtrc.json for the new tooling. Update CI workflows and lefthook
hooks accordingly. Reformat codebase with oxfmt.

https://claude.ai/code/session_01GMkSf29p78HuMR1mbXn8He
2026-04-02 16:39:05 +02:00

911 lines
26 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo List</title>
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
/* ==========================================================================
DESIGN SYSTEM - CopilotKit Glassmorphism Theme
========================================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--color-lilac: #bec2ff;
--color-lilac-light: #d4d7ff;
--color-lilac-dark: #9599cc;
--color-mint: #85e0ce;
--color-mint-light: #a8e9dc;
--color-mint-dark: #1b936f;
--color-surface: #dedee9;
--color-surface-light: #f7f7f9;
--color-container: #ffffff;
--color-text-primary: #010507;
--color-text-secondary: #57575b;
--color-text-tertiary: #8e8e93;
--color-border: #dbdbe5;
--color-border-light: #ebebf0;
--color-border-glass: rgba(255, 255, 255, 0.3);
--color-glass: rgba(255, 255, 255, 0.7);
--color-glass-subtle: rgba(255, 255, 255, 0.5);
--color-glass-dark: rgba(255, 255, 255, 0.85);
--color-success: #22c55e;
--color-success-light: #dcfce7;
--color-warning: #f59e0b;
--color-warning-light: #fef3c7;
--color-danger: #ef4444;
--color-danger-light: #fee2e2;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 25px -3px rgba(0, 0, 0, 0.08);
--shadow-glass: 0 4px 30px rgba(0, 0, 0, 0.1);
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
--font-family: "Plus Jakarta Sans", system-ui, sans-serif;
--font-size-xs: 11px;
--font-size-sm: 13px;
--font-size-base: 15px;
--font-size-lg: 17px;
--font-size-xl: 20px;
}
body {
font-family: var(--font-family);
background: linear-gradient(
135deg,
var(--color-surface-light),
var(--color-surface)
);
color: var(--color-text-primary);
line-height: 1.5;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
body::before {
content: "";
position: fixed;
top: -150px;
right: -100px;
width: 400px;
height: 400px;
background: var(--color-lilac);
border-radius: 50%;
filter: blur(100px);
opacity: 0.4;
z-index: -1;
animation: blob1 25s ease-in-out infinite;
}
body::after {
content: "";
position: fixed;
bottom: -100px;
left: -100px;
width: 350px;
height: 350px;
background: var(--color-mint);
border-radius: 50%;
filter: blur(100px);
opacity: 0.3;
z-index: -1;
animation: blob2 30s ease-in-out infinite;
}
@keyframes blob1 {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(-20px, 30px) scale(1.05);
}
66% {
transform: translate(15px, -20px) scale(0.95);
}
}
@keyframes blob2 {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(30px, -25px) scale(1.08);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes slide-up {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
#app {
padding: var(--space-4);
width: 100%;
}
.hidden {
display: none !important;
}
/* Todo Container */
.todo-container {
background: var(--color-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-2xl);
padding: var(--space-5);
box-shadow: var(--shadow-glass);
animation: slide-up 0.3s ease;
}
/* Header */
.todo-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.todo-icon {
width: 40px;
height: 40px;
background: linear-gradient(
135deg,
var(--color-lilac),
var(--color-mint)
);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.todo-icon svg {
width: 20px;
height: 20px;
}
.todo-title {
font-size: var(--font-size-xl);
font-weight: 700;
}
/* Stats */
.stats {
display: flex;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.stat {
background: var(--color-glass-subtle);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: var(--space-1);
}
.stat svg {
width: 12px;
height: 12px;
}
.stat-value {
font-weight: 700;
color: var(--color-text-primary);
}
/* Add Todo Form */
.add-form {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.add-input {
flex: 1;
padding: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
font-size: var(--font-size-sm);
font-family: var(--font-family);
background: var(--color-container);
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.add-input:focus {
outline: none;
border-color: var(--color-lilac);
box-shadow: 0 0 0 3px rgba(190, 194, 255, 0.2);
}
.add-btn {
padding: var(--space-3) var(--space-4);
background: linear-gradient(
135deg,
var(--color-lilac),
var(--color-mint)
);
color: white;
border: none;
border-radius: var(--radius-lg);
font-weight: 600;
font-family: var(--font-family);
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
transition: all 0.2s;
}
.add-btn:hover {
box-shadow: 0 4px 12px rgba(190, 194, 255, 0.4);
}
.add-btn:active {
transform: scale(0.98);
}
.add-btn svg {
width: 16px;
height: 16px;
}
/* Filters */
.filters {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.filter-btn {
padding: var(--space-2) var(--space-3);
background: var(--color-glass-subtle);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: var(--font-family);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.filter-btn:hover {
background: var(--color-glass);
border-color: var(--color-lilac);
}
.filter-btn.active {
background: var(--color-lilac);
border-color: var(--color-lilac);
color: white;
}
/* Todo List */
.todo-list {
max-height: 400px;
overflow-y: auto;
}
.todo-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: var(--color-glass-dark);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-lg);
margin-bottom: var(--space-2);
transition: all 0.2s;
animation: slide-up 0.2s ease;
}
.todo-item:hover {
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: var(--color-text-tertiary);
}
/* Checkbox */
.todo-checkbox {
width: 22px;
height: 22px;
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
background: var(--color-container);
}
.todo-checkbox:hover {
border-color: var(--color-lilac);
}
.todo-checkbox.checked {
background: linear-gradient(
135deg,
var(--color-mint),
var(--color-mint-dark)
);
border-color: var(--color-mint);
}
.todo-checkbox svg {
width: 14px;
height: 14px;
color: white;
opacity: 0;
}
.todo-checkbox.checked svg {
opacity: 1;
}
/* Todo Content */
.todo-content {
flex: 1;
min-width: 0;
}
.todo-text {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: var(--space-1);
}
.todo-meta {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.todo-tag {
font-size: 10px;
padding: 2px var(--space-2);
background: var(--color-glass-subtle);
border-radius: var(--radius-sm);
color: var(--color-text-tertiary);
}
.priority-high {
background: var(--color-danger-light);
color: var(--color-danger);
}
.priority-medium {
background: var(--color-warning-light);
color: var(--color-warning);
}
.priority-low {
background: var(--color-success-light);
color: var(--color-success);
}
/* Delete Button */
.todo-delete {
width: 28px;
height: 28px;
border: none;
background: transparent;
border-radius: var(--radius-md);
cursor: pointer;
color: var(--color-text-tertiary);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
opacity: 0;
}
.todo-item:hover .todo-delete {
opacity: 1;
}
.todo-delete:hover {
background: var(--color-danger-light);
color: var(--color-danger);
}
.todo-delete svg {
width: 16px;
height: 16px;
}
/* Empty State */
.empty-state {
text-align: center;
padding: var(--space-6);
color: var(--color-text-tertiary);
}
.empty-icon {
width: 48px;
height: 48px;
margin: 0 auto var(--space-3);
color: var(--color-border);
}
/* Footer */
.todo-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid var(--color-border-light);
}
.footer-text {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
}
.clear-btn {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
background: none;
border: none;
cursor: pointer;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
font-family: var(--font-family);
}
.clear-btn:hover {
background: var(--color-surface-light);
color: var(--color-text-secondary);
}
/* Loading */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-6);
}
.loading-icon {
width: 48px;
height: 48px;
margin-bottom: var(--space-4);
color: var(--color-lilac-dark);
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--color-border-light);
border-top-color: var(--color-lilac);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: var(--space-4);
}
.loading-text {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-secondary);
}
</style>
</head>
<body>
<div id="app">
<!-- Loading -->
<div id="loading" class="loading-container">
<svg
class="loading-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<div class="loading-spinner"></div>
<p class="loading-text">Loading your tasks...</p>
</div>
<!-- Content -->
<div id="content" class="hidden">
<div class="todo-container">
<div class="todo-header">
<div class="todo-icon">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<h1 class="todo-title" id="list-name">My Tasks</h1>
</div>
<div id="stats" class="stats"></div>
<div class="add-form">
<input
type="text"
id="add-input"
class="add-input"
placeholder="Add a new task..."
/>
<button id="add-btn" class="add-btn">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M12 5v14M5 12h14" />
</svg>
Add
</button>
</div>
<div class="filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="pending">Pending</button>
<button class="filter-btn" data-filter="in_progress">
In Progress
</button>
<button class="filter-btn" data-filter="completed">
Completed
</button>
</div>
<div id="todo-list" class="todo-list"></div>
<div id="footer" class="todo-footer">
<span class="footer-text"
><span id="items-left">0</span> items remaining</span
>
<button id="clear-completed" class="clear-btn">
Clear completed
</button>
</div>
</div>
</div>
</div>
<script type="module">
// MCP App Communication
const mcpApp = (() => {
let requestId = 1;
const pendingRequests = new Map();
const notificationHandlers = {};
function sendRequest(method, params) {
const id = requestId++;
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve, reject });
window.parent.postMessage(
{ jsonrpc: "2.0", id, method, params },
"*",
);
});
}
function sendNotification(method, params) {
window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
}
window.addEventListener("message", (event) => {
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.id && pendingRequests.has(data.id)) {
const { resolve, reject } = pendingRequests.get(data.id);
pendingRequests.delete(data.id);
data.error ? reject(data.error) : resolve(data.result);
return;
}
if (data.method && notificationHandlers[data.method]) {
notificationHandlers[data.method](data.params);
}
});
function onNotification(method, handler) {
notificationHandlers[method] = handler;
}
function reportSize() {
const app = document.getElementById("app");
sendNotification("ui/notifications/size-change", {
width: Math.ceil(app.scrollWidth),
height: Math.ceil(app.scrollHeight),
});
}
return { sendRequest, sendNotification, onNotification, reportSize };
})();
// State
let todoList = null;
let currentFilter = "all";
// DOM
const el = {
loading: document.getElementById("loading"),
content: document.getElementById("content"),
listName: document.getElementById("list-name"),
stats: document.getElementById("stats"),
addInput: document.getElementById("add-input"),
addBtn: document.getElementById("add-btn"),
todoList: document.getElementById("todo-list"),
itemsLeft: document.getElementById("items-left"),
clearCompleted: document.getElementById("clear-completed"),
};
// Icons
const icons = {
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>`,
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`,
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M9 2v4h6V2"/><rect x="4" y="4" width="16" height="18" rx="2"/></svg>`,
};
// Render
function render() {
if (!todoList) return;
el.loading.classList.add("hidden");
el.content.classList.remove("hidden");
el.listName.textContent = todoList.name;
// Stats
const stats = getStats();
el.stats.innerHTML = `
<div class="stat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg> Total: <span class="stat-value">${stats.total}</span></div>
<div class="stat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg> Done: <span class="stat-value">${stats.completed}</span></div>
${stats.overdue > 0 ? `<div class="stat" style="color: var(--color-danger);"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg> Overdue: <span class="stat-value">${stats.overdue}</span></div>` : ""}
`;
// Filter items
const items = getFilteredItems();
el.itemsLeft.textContent = stats.pending + stats.inProgress;
if (!items.length) {
el.todoList.innerHTML = `
<div class="empty-state">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<p>${currentFilter === "all" ? "No tasks yet. Add one above!" : `No ${currentFilter.replace("_", " ")} tasks`}</p>
</div>
`;
} else {
el.todoList.innerHTML = items
.map(
(item) => `
<div class="todo-item ${item.status === "completed" ? "completed" : ""}" data-id="${item.id}">
<div class="todo-checkbox ${item.status === "completed" ? "checked" : ""}" data-action="toggle" data-id="${item.id}">
${icons.check}
</div>
<div class="todo-content">
<div class="todo-text">${item.title}</div>
<div class="todo-meta">
<span class="todo-tag priority-${item.priority}">${item.priority}</span>
${item.tags
.slice(0, 2)
.map((t) => `<span class="todo-tag">${t}</span>`)
.join("")}
</div>
</div>
<button class="todo-delete" data-action="delete" data-id="${item.id}">${icons.trash}</button>
</div>
`,
)
.join("");
}
// Event listeners
el.todoList.querySelectorAll('[data-action="toggle"]').forEach((cb) => {
cb.addEventListener("click", () => toggleItem(cb.dataset.id));
});
el.todoList
.querySelectorAll('[data-action="delete"]')
.forEach((btn) => {
btn.addEventListener("click", () => deleteItem(btn.dataset.id));
});
mcpApp.reportSize();
}
function getStats() {
const items = todoList?.items || [];
const now = new Date();
return {
total: items.length,
pending: items.filter((i) => i.status === "pending").length,
inProgress: items.filter((i) => i.status === "in_progress").length,
completed: items.filter((i) => i.status === "completed").length,
overdue: items.filter(
(i) =>
i.status !== "completed" &&
i.dueDate &&
new Date(i.dueDate) < now,
).length,
};
}
function getFilteredItems() {
const items = todoList?.items || [];
if (currentFilter === "all") return items;
return items.filter((i) => i.status === currentFilter);
}
// Actions
async function addItem() {
const title = el.addInput.value.trim();
if (!title || !todoList) return;
try {
const res = await mcpApp.sendRequest("tools/call", {
name: "add-todo-item",
arguments: { listId: todoList.id, title, priority: "medium" },
});
if (res?.structuredContent?.list) {
todoList = res.structuredContent.list;
el.addInput.value = "";
render();
}
} catch (e) {
console.error("Add failed:", e);
}
}
async function toggleItem(itemId) {
if (!todoList) return;
const item = todoList.items.find((i) => i.id === itemId);
if (!item) return;
const newStatus = item.status === "completed" ? "pending" : "completed";
try {
const res = await mcpApp.sendRequest("tools/call", {
name:
newStatus === "completed"
? "complete-todo-item"
: "reopen-todo-item",
arguments: { listId: todoList.id, itemId },
});
if (res?.structuredContent?.list) {
todoList = res.structuredContent.list;
render();
}
} catch (e) {
console.error("Toggle failed:", e);
}
}
async function deleteItem(itemId) {
if (!todoList) return;
try {
const res = await mcpApp.sendRequest("tools/call", {
name: "delete-todo-item",
arguments: { listId: todoList.id, itemId },
});
if (res?.structuredContent?.list) {
todoList = res.structuredContent.list;
render();
}
} catch (e) {
console.error("Delete failed:", e);
}
}
async function clearCompleted() {
if (!todoList) return;
try {
const res = await mcpApp.sendRequest("tools/call", {
name: "clear-completed",
arguments: { listId: todoList.id },
});
if (res?.structuredContent?.list) {
todoList = res.structuredContent.list;
render();
}
} catch (e) {
console.error("Clear failed:", e);
}
}
// Event Listeners
el.addBtn.addEventListener("click", addItem);
el.addInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") addItem();
});
el.clearCompleted.addEventListener("click", clearCompleted);
document.querySelectorAll(".filter-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document
.querySelectorAll(".filter-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentFilter = btn.dataset.filter;
render();
});
});
// Init
async function initialize() {
try {
await mcpApp.sendRequest("ui/initialize", {
protocolVersion: "2025-06-18",
appInfo: { name: "Todo List", version: "1.0.0" },
appCapabilities: {},
});
mcpApp.sendNotification("ui/notifications/initialized", {});
new ResizeObserver(() => mcpApp.reportSize()).observe(
document.getElementById("app"),
);
} catch (e) {
console.error("Init failed:", e);
}
}
mcpApp.onNotification("ui/notifications/tool-result", (params) => {
const result = params?.structuredContent;
if (result?.list) {
todoList = result.list;
render();
}
});
window.addEventListener("load", initialize);
</script>
</body>
</html>