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>
<div id="app"> <template id="install-instructions-template">
<!-- Landing Screen -->
<div id="landing-screen" class="screen center-content">
<h1>Kanban TODO</h1>
<p>Markdown based task management</p>
<p>To get started, open a Markdown file containing your tasks.</p>
<button id="open-file-btn">Open TODO.md</button>
<p>Install launch script:</p> <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> <pre>curl -O <a href="https://mccormick.cx/apps/kanban-todo/kbtd">https://mccormick.cx/apps/kanban-todo/kbtd</a>
chmod 755 kbtd chmod 755 kbtd
# If self-hosting, set KBTD_URL to your URL: # If self-hosting, set KBTD_URL to your URL:
# export KBTD_URL="<span id="current-url-landing"></span>" # export KBTD_URL="<span class="current-url"></span>"
# Then launch with: # Then launch with:
./kbtd TODO.md</pre> ./kbtd TODO.md</pre>
</template>
<div id="app">
<div id="landing-screen" class="screen center-content">
<h1>Kanban TODO</h1>
<p>Markdown based task management</p>
<p>To get started, open a Markdown file containing your tasks.</p>
<button id="open-file-btn">Open TODO.md</button>
<div id="landing-instructions"></div>
<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 = '';
section.columns.forEach((column, colIndex) => { const columns = section.columns.map((column, colIndex) => {
const colDiv = document.createElement('div'); const visibleCards = column.cards.filter(card =>
colDiv.className = 'column'; state.showCompleted || !card.completed
colDiv.dataset.colIndex = colIndex; );
const header = document.createElement('div'); const cardElements = [];
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);
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;
const checkbox = document.createElement('div');
checkbox.className = `card-checkbox ${card.completed ? 'checked' : ''}`;
checkbox.onclick = async (e) => {
e.stopPropagation(); e.stopPropagation();
const lineNum = parseInt(cardDiv.dataset.lineNumber); const lineNum = parseInt(cardDiv.dataset.lineNumber);
const originalLine = cardDiv.dataset.originalLine; const originalLine = cardDiv.dataset.originalLine;
const newLine = originalLine.includes('- [x]') const newLine = originalLine.includes('- [x]')
? originalLine.replace('- [x]', '- [ ]') ? originalLine.replace('- [x]', '- [ ]')
: originalLine.replace('- [ ]', '- [x]'); : originalLine.replace('- [ ]', '- [x]');
await updateLineInFile(lineNum, newLine); await updateLineInFile(lineNum, newLine);
}; }
cardDiv.appendChild(checkbox); });
const textSpan = document.createElement('span'); cardDiv.append(checkbox, h('span', { textContent: card.text }));
textSpan.textContent = card.text;
cardDiv.appendChild(textSpan);
cardDiv.ondragstart = (e) => handleDragStart(e, cardDiv); cardElements.push(
cardDiv.ondragend = handleDragEnd; cardDiv,
cardDiv.ondragenter = (e) => handleCardDragEnter(e, cardDiv, colIndex, cardIndex); h('div', {
className: 'drop-indicator',
cardList.appendChild(cardDiv); dataset: { colIndex, position: cardIndex + 1 }
})
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 cardList = h('div', {
className: 'card-list',
dataset: { colIndex },
ondragover: (e) => handleDragOver(e, colIndex, cardList),
ondrop: (e) => handleDrop(e, colIndex),
ondragleave: handleDragLeave
}, ...cardElements);
const footer = document.createElement('div'); const addCardForm = createAddForm('Card text...', async (text) => {
footer.className = 'column-footer'; await createCard(colIndex, text);
addCardForm.hide();
const addCardBtn = document.createElement('button');
addCardBtn.className = 'add-card-btn';
addCardBtn.textContent = '+';
addCardBtn.title = 'Add card';
addCardBtn.onclick = () => {
const form = footer.querySelector('.add-card-form');
addCardBtn.style.display = 'none';
form.classList.add('active');
form.querySelector('input').focus();
};
footer.appendChild(addCardBtn);
const form = document.createElement('div');
form.className = 'add-card-form';
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);
footer.appendChild(form);
colDiv.appendChild(footer);
container.appendChild(colDiv);
}); });
const addColumnDiv = document.createElement('div'); const addCardBtn = h('button', {
addColumnDiv.className = 'add-column'; className: 'add-card-btn',
textContent: '+',
const addBtn = document.createElement('button'); title: 'Add card',
addBtn.className = 'add-column-btn'; onclick: () => {
addBtn.textContent = '+'; addCardBtn.style.display = 'none';
addBtn.title = 'Add column'; addCardForm.show();
addBtn.onclick = () => {
const form = addColumnDiv.querySelector('.add-column-form');
form.classList.toggle('active');
if (form.classList.contains('active')) {
form.querySelector('input').focus();
} }
}; });
addColumnDiv.appendChild(addBtn);
const form = document.createElement('div'); return h('div', {
form.className = 'add-column-form'; className: 'column',
dataset: { colIndex }
const input = document.createElement('input'); },
input.type = 'text'; h('div', { className: 'column-header', textContent: column.name }),
input.placeholder = 'Column name...'; cardList,
input.onkeypress = (e) => { h('div', { className: 'column-footer' }, addCardBtn, addCardForm)
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');
}
async function handleCreateColumn(input, form) {
const columnName = input.value.trim();
if (!columnName) {
alert('Please enter a column name');
return;
}
const addColumnForm = createAddForm('Column name...', async (name) => {
const section = state.sections[state.currentSectionIndex]; const section = state.sections[state.currentSectionIndex];
if (section.columns.some(c => c.name === columnName)) { if (section.columns.some(c => c.name === name)) {
alert('A column with this name already exists'); alert('A column with this name already exists');
return; return;
} }
await createColumn(name);
addColumnForm.hide();
});
addColumnForm.className = 'add-column-form';
form.classList.remove('active'); const addColumnDiv = h('div', { className: 'add-column' },
input.value = ''; h('button', {
className: 'add-column-btn',
textContent: '+',
title: 'Add column',
onclick: () => addColumnForm.classList.toggle('active')
}),
addColumnForm
);
await createColumn(columnName); container.replaceChildren(...columns, addColumnDiv);
showScreen('board-screen');
} }
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({