Refactor to summarize.

This commit is contained in:
Chris McCormick 2026-01-01 14:35:19 +08:00
parent 39c59e951e
commit 3aa87c3faf

View file

@ -86,7 +86,6 @@
h3 { font-size: 1.17em; } h3 { font-size: 1.17em; }
h1, h2, h3 { margin-top: 0; } h1, h2, h3 { margin-top: 0; }
/* Layouts */
#app { #app {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
@ -113,7 +112,6 @@
gap: 1.5em; gap: 1.5em;
} }
/* Kanban Board */
header { header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -332,7 +330,6 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Section Picker */
.section-list { .section-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -374,7 +371,6 @@
border-radius: var(--radius); border-radius: var(--radius);
} }
/* Scrollbar styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
height: 10px; height: 10px;
@ -421,33 +417,34 @@
</style> </style>
</head> </head>
<body> <body>
<template id="install-instructions-template">
<p>Install launch script:</p>
<pre>curl -O <a href="https://mccormick.cx/apps/kanban-todo/kbtd">https://mccormick.cx/apps/kanban-todo/kbtd</a>
chmod 755 kbtd
# If self-hosting, set KBTD_URL to your URL:
# export KBTD_URL="<span class="current-url"></span>"
# Then launch with:
./kbtd TODO.md</pre>
</template>
<div id="app"> <div id="app">
<!-- Landing Screen -->
<div id="landing-screen" class="screen center-content"> <div id="landing-screen" class="screen center-content">
<h1>Kanban TODO</h1> <h1>Kanban TODO</h1>
<p>Markdown based task management</p> <p>Markdown based task management</p>
<p>To get started, open a Markdown file containing your tasks.</p> <p>To get started, open a Markdown file containing your tasks.</p>
<button id="open-file-btn">Open TODO.md</button> <button id="open-file-btn">Open TODO.md</button>
<p>Install launch script:</p> <div id="landing-instructions"></div>
<pre>curl -O <a href="https://mccormick.cx/apps/kanban-todo/kbtd">https://mccormick.cx/apps/kanban-todo/kbtd</a>
chmod 755 kbtd
# If self-hosting, set KBTD_URL to your URL:
# export KBTD_URL="<span id="current-url-landing"></span>"
# Then launch with:
./kbtd TODO.md</pre>
<div id="landing-error" class="error" style="display:none"></div> <div id="landing-error" class="error" style="display:none"></div>
</div> </div>
<!-- Permission Screen -->
<div id="permission-screen" class="screen center-content"> <div id="permission-screen" class="screen center-content">
<h1>Permission Required</h1> <h1>Permission Required</h1>
<p>Kanban TODO needs permission to view and edit your file.</p> <p>Kanban TODO needs permission to view and edit your file.</p>
<button id="grant-permission-btn">Grant Access</button> <button id="grant-permission-btn">Grant Access</button>
</div> </div>
<!-- Section Picker Screen -->
<div id="section-screen" class="screen center-content"> <div id="section-screen" class="screen center-content">
<h1>Select Project</h1> <h1>Select Project</h1>
<ul id="section-list" class="section-list"></ul> <ul id="section-list" class="section-list"></ul>
@ -456,18 +453,9 @@ chmod 755 kbtd
<button id="create-project-btn">Create</button> <button id="create-project-btn">Create</button>
</div> </div>
<button id="change-file-btn" style="margin-top: 1em">Change File</button> <button id="change-file-btn" style="margin-top: 1em">Change File</button>
<p>Install launch script:</p> <div id="section-instructions"></div>
<pre>curl -O <a href="https://mccormick.cx/apps/kanban-todo/kbtd">https://mccormick.cx/apps/kanban-todo/kbtd</a>
chmod 755 kbtd
# If self-hosting, set KBTD_URL to your URL:
# export KBTD_URL="<span id="current-url-section"></span>"
# Then launch with:
./kbtd TODO.md</pre>
</div> </div>
<!-- Board Screen -->
<div id="board-screen" class="screen"> <div id="board-screen" class="screen">
<header> <header>
<h2 id="board-title" style="margin:0">Project Name</h2> <h2 id="board-title" style="margin:0">Project Name</h2>
@ -481,7 +469,6 @@ chmod 755 kbtd
</div> </div>
<script> <script>
// --- Constants & State ---
const DB_NAME = 'kbtd'; const DB_NAME = 'kbtd';
const DB_VERSION = 1; const DB_VERSION = 1;
const STORE_NAME = 'fileHandles'; const STORE_NAME = 'fileHandles';
@ -500,7 +487,73 @@ chmod 755 kbtd
currentDropTarget: null currentDropTarget: null
}; };
// --- IndexedDB Helper --- function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(props)) {
if (k.startsWith('on')) el[k] = v;
else if (k === 'dataset') Object.assign(el.dataset, v);
else if (k === 'className') el.className = v;
else if (k === 'style' && typeof v === 'object') {
Object.assign(el.style, v);
} else el[k] = v;
}
children.flat().forEach(c => {
if (c != null) el.append(c);
});
return el;
}
function insertInstallInstructions(containerId) {
const template = document.getElementById('install-instructions-template');
const clone = template.content.cloneNode(true);
const currentUrl = window.location.href.split('?')[0];
clone.querySelector('.current-url').textContent = currentUrl;
document.getElementById(containerId).appendChild(clone);
}
function createAddForm(placeholder, onSubmit) {
const input = h('input', { type: 'text', placeholder });
const form = h('div', { className: 'add-card-form' }, input);
const handleSubmit = async () => {
const value = input.value.trim();
if (!value) {
alert(`Please enter ${placeholder.toLowerCase()}`);
return;
}
await onSubmit(value);
};
input.onkeypress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit();
}
};
const buttons = h('div', { className: 'add-card-form-buttons' },
h('button', { textContent: 'Add', onclick: handleSubmit }),
h('button', {
textContent: 'Cancel',
onclick: () => form.hide()
})
);
form.appendChild(buttons);
form.show = () => {
form.classList.add('active');
input.focus();
};
form.hide = () => {
form.classList.remove('active');
input.value = '';
};
return form;
}
const dbPromise = new Promise((resolve, reject) => { const dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION); const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error); request.onerror = () => reject(request.error);
@ -535,8 +588,6 @@ chmod 755 kbtd
}); });
} }
// --- Markdown Parsing ---
function parseMarkdown(text) { function parseMarkdown(text) {
const lines = text.split('\n'); const lines = text.split('\n');
const sections = []; const sections = [];
@ -599,8 +650,6 @@ chmod 755 kbtd
return sections; return sections;
} }
// --- File Operations ---
function reloadFromText(text) { function reloadFromText(text) {
console.log('Reloading from text'); console.log('Reloading from text');
state.sections = parseMarkdown(text); state.sections = parseMarkdown(text);
@ -869,8 +918,6 @@ chmod 755 kbtd
return false; return false;
} }
// --- UI Rendering ---
function showScreen(screenId) { function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(el => el.classList.remove('active')); document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
document.getElementById(screenId).classList.add('active'); document.getElementById(screenId).classList.add('active');
@ -878,20 +925,19 @@ chmod 755 kbtd
function renderSectionPicker() { function renderSectionPicker() {
const list = document.getElementById('section-list'); const list = document.getElementById('section-list');
list.innerHTML = ''; list.replaceChildren(
...state.sections.map((section, index) =>
state.sections.forEach((section, index) => { h('li', {},
const li = document.createElement('li'); h('button', {
const btn = document.createElement('button'); className: 'section-btn',
btn.className = 'section-btn'; textContent: section.name,
btn.textContent = section.name; onclick: () => selectSection(index)
btn.onclick = () => selectSection(index); })
li.appendChild(btn); )
list.appendChild(li); )
}); );
document.getElementById('new-project-input').value = ''; document.getElementById('new-project-input').value = '';
showScreen('section-screen'); showScreen('section-screen');
} }
@ -905,241 +951,132 @@ chmod 755 kbtd
toggleBtn.textContent = state.showCompleted ? 'Hide Completed' : 'Show Completed'; toggleBtn.textContent = state.showCompleted ? 'Hide Completed' : 'Show Completed';
const container = document.getElementById('board-container'); const container = document.getElementById('board-container');
container.innerHTML = '';
const columns = section.columns.map((column, colIndex) => {
section.columns.forEach((column, colIndex) => { const visibleCards = column.cards.filter(card =>
const colDiv = document.createElement('div'); state.showCompleted || !card.completed
colDiv.className = 'column'; );
colDiv.dataset.colIndex = colIndex;
const header = document.createElement('div');
header.className = 'column-header';
header.textContent = column.name;
colDiv.appendChild(header);
const cardList = document.createElement('div');
cardList.className = 'card-list';
cardList.dataset.colIndex = colIndex;
cardList.ondragover = (e) => handleDragOver(e, colIndex, cardList); const cardElements = [];
cardList.ondrop = (e) => handleDrop(e, colIndex);
cardList.ondragleave = handleDragLeave;
const visibleCards = column.cards.filter((card, idx) => {
if (!state.showCompleted && card.completed) {
return false;
}
return true;
});
if (visibleCards.length > 0) { if (visibleCards.length > 0) {
const firstIndicator = document.createElement('div'); cardElements.push(
firstIndicator.className = 'drop-indicator'; h('div', {
firstIndicator.dataset.colIndex = colIndex; className: 'drop-indicator',
firstIndicator.dataset.position = 0; dataset: { colIndex, position: 0 }
cardList.appendChild(firstIndicator); })
);
} }
column.cards.forEach((card, cardIndex) => { column.cards.forEach((card, cardIndex) => {
if (!state.showCompleted && card.completed) { if (!state.showCompleted && card.completed) return;
return;
}
const cardDiv = document.createElement('div'); const cardDiv = h('div', {
cardDiv.className = `card ${card.completed ? 'completed' : ''}`; className: `card ${card.completed ? 'completed' : ''}`,
cardDiv.draggable = true; draggable: true,
dataset: {
lineNumber: card.lineNumber,
originalLine: card.originalLine,
colIndex,
cardIndex
},
ondragstart: (e) => handleDragStart(e, cardDiv),
ondragend: handleDragEnd,
ondragenter: (e) => handleCardDragEnter(e, cardDiv, colIndex, cardIndex)
});
cardDiv.dataset.lineNumber = card.lineNumber; const checkbox = h('div', {
cardDiv.dataset.originalLine = card.originalLine; className: `card-checkbox ${card.completed ? 'checked' : ''}`,
cardDiv.dataset.colIndex = colIndex; onclick: async (e) => {
cardDiv.dataset.cardIndex = cardIndex; e.stopPropagation();
const lineNum = parseInt(cardDiv.dataset.lineNumber);
const originalLine = cardDiv.dataset.originalLine;
const newLine = originalLine.includes('- [x]')
? originalLine.replace('- [x]', '- [ ]')
: originalLine.replace('- [ ]', '- [x]');
await updateLineInFile(lineNum, newLine);
}
});
const checkbox = document.createElement('div'); cardDiv.append(checkbox, h('span', { textContent: card.text }));
checkbox.className = `card-checkbox ${card.completed ? 'checked' : ''}`;
checkbox.onclick = async (e) => {
e.stopPropagation();
const lineNum = parseInt(cardDiv.dataset.lineNumber);
const originalLine = cardDiv.dataset.originalLine;
const newLine = originalLine.includes('- [x]')
? originalLine.replace('- [x]', '- [ ]')
: originalLine.replace('- [ ]', '- [x]');
await updateLineInFile(lineNum, newLine);
};
cardDiv.appendChild(checkbox);
const textSpan = document.createElement('span'); cardElements.push(
textSpan.textContent = card.text; cardDiv,
cardDiv.appendChild(textSpan); h('div', {
className: 'drop-indicator',
cardDiv.ondragstart = (e) => handleDragStart(e, cardDiv); dataset: { colIndex, position: cardIndex + 1 }
cardDiv.ondragend = handleDragEnd; })
cardDiv.ondragenter = (e) => handleCardDragEnter(e, cardDiv, colIndex, cardIndex); );
cardList.appendChild(cardDiv);
const indicator = document.createElement('div');
indicator.className = 'drop-indicator';
indicator.dataset.colIndex = colIndex;
indicator.dataset.position = cardIndex + 1;
cardList.appendChild(indicator);
}); });
if (visibleCards.length === 0) { if (visibleCards.length === 0) {
const indicator = document.createElement('div'); cardElements.push(
indicator.className = 'drop-indicator'; h('div', {
indicator.dataset.colIndex = colIndex; className: 'drop-indicator',
indicator.dataset.position = 0; dataset: { colIndex, position: 0 }
cardList.appendChild(indicator); })
);
} }
colDiv.appendChild(cardList);
const footer = document.createElement('div');
footer.className = 'column-footer';
const addCardBtn = document.createElement('button'); const cardList = h('div', {
addCardBtn.className = 'add-card-btn'; className: 'card-list',
addCardBtn.textContent = '+'; dataset: { colIndex },
addCardBtn.title = 'Add card'; ondragover: (e) => handleDragOver(e, colIndex, cardList),
addCardBtn.onclick = () => { ondrop: (e) => handleDrop(e, colIndex),
const form = footer.querySelector('.add-card-form'); ondragleave: handleDragLeave
addCardBtn.style.display = 'none'; }, ...cardElements);
form.classList.add('active');
form.querySelector('input').focus();
};
footer.appendChild(addCardBtn);
const form = document.createElement('div'); const addCardForm = createAddForm('Card text...', async (text) => {
form.className = 'add-card-form'; await createCard(colIndex, text);
addCardForm.hide();
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Card text...';
input.onkeypress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCreateCard(colIndex, input, form, addCardBtn);
}
};
form.appendChild(input);
const buttons = document.createElement('div');
buttons.className = 'add-card-form-buttons';
const createBtn = document.createElement('button');
createBtn.textContent = 'Add';
createBtn.onclick = () => handleCreateCard(colIndex, input, form, addCardBtn);
buttons.appendChild(createBtn);
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => {
form.classList.remove('active');
addCardBtn.style.display = ''; addCardBtn.style.display = '';
input.value = ''; });
};
buttons.appendChild(cancelBtn);
form.appendChild(buttons); const addCardBtn = h('button', {
footer.appendChild(form); className: 'add-card-btn',
textContent: '+',
title: 'Add card',
onclick: () => {
addCardBtn.style.display = 'none';
addCardForm.show();
}
});
colDiv.appendChild(footer); return h('div', {
container.appendChild(colDiv); className: 'column',
dataset: { colIndex }
},
h('div', { className: 'column-header', textContent: column.name }),
cardList,
h('div', { className: 'column-footer' }, addCardBtn, addCardForm)
);
}); });
const addColumnDiv = document.createElement('div');
addColumnDiv.className = 'add-column';
const addBtn = document.createElement('button'); const addColumnForm = createAddForm('Column name...', async (name) => {
addBtn.className = 'add-column-btn'; const section = state.sections[state.currentSectionIndex];
addBtn.textContent = '+'; if (section.columns.some(c => c.name === name)) {
addBtn.title = 'Add column'; alert('A column with this name already exists');
addBtn.onclick = () => { return;
const form = addColumnDiv.querySelector('.add-column-form');
form.classList.toggle('active');
if (form.classList.contains('active')) {
form.querySelector('input').focus();
} }
}; await createColumn(name);
addColumnDiv.appendChild(addBtn); addColumnForm.hide();
});
addColumnForm.className = 'add-column-form';
const form = document.createElement('div'); const addColumnDiv = h('div', { className: 'add-column' },
form.className = 'add-column-form'; h('button', {
className: 'add-column-btn',
textContent: '+',
title: 'Add column',
onclick: () => addColumnForm.classList.toggle('active')
}),
addColumnForm
);
const input = document.createElement('input'); container.replaceChildren(...columns, addColumnDiv);
input.type = 'text';
input.placeholder = 'Column name...';
input.onkeypress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCreateColumn(input, form);
}
};
form.appendChild(input);
const buttons = document.createElement('div');
buttons.className = 'add-column-form-buttons';
const createBtn = document.createElement('button');
createBtn.textContent = 'Create';
createBtn.onclick = () => handleCreateColumn(input, form);
buttons.appendChild(createBtn);
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => {
form.classList.remove('active');
input.value = '';
};
buttons.appendChild(cancelBtn);
form.appendChild(buttons);
addColumnDiv.appendChild(form);
container.appendChild(addColumnDiv);
showScreen('board-screen'); showScreen('board-screen');
} }
async function handleCreateColumn(input, form) {
const columnName = input.value.trim();
if (!columnName) {
alert('Please enter a column name');
return;
}
const section = state.sections[state.currentSectionIndex];
if (section.columns.some(c => c.name === columnName)) {
alert('A column with this name already exists');
return;
}
form.classList.remove('active');
input.value = '';
await createColumn(columnName);
}
async function handleCreateCard(colIndex, input, form, addCardBtn) {
const cardText = input.value.trim();
if (!cardText) {
alert('Please enter card text');
return;
}
form.classList.remove('active');
addCardBtn.style.display = '';
input.value = '';
await createCard(colIndex, cardText);
}
// --- Actions ---
async function selectSection(index) { async function selectSection(index) {
state.currentSectionIndex = index; state.currentSectionIndex = index;
if (state.filePath) { if (state.filePath) {
@ -1172,8 +1109,6 @@ chmod 755 kbtd
renderBoard(); renderBoard();
} }
// --- Drag and Drop ---
function handleDragStart(e, cardDiv) { function handleDragStart(e, cardDiv) {
dragState.source = { dragState.source = {
lineNumber: parseInt(cardDiv.dataset.lineNumber), lineNumber: parseInt(cardDiv.dataset.lineNumber),
@ -1330,12 +1265,9 @@ chmod 755 kbtd
return false; return false;
} }
// --- Initialization & Polling ---
async function init() { async function init() {
const currentUrl = window.location.href.split('?')[0]; insertInstallInstructions('landing-instructions');
document.getElementById('current-url-landing').textContent = currentUrl; insertInstallInstructions('section-instructions');
document.getElementById('current-url-section').textContent = currentUrl;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
state.filePath = params.get('file'); state.filePath = params.get('file');
@ -1382,8 +1314,6 @@ chmod 755 kbtd
}, 250); }, 250);
} }
// --- Event Listeners ---
document.getElementById('open-file-btn').onclick = async () => { document.getElementById('open-file-btn').onclick = async () => {
try { try {
const [handle] = await window.showOpenFilePicker({ const [handle] = await window.showOpenFilePicker({