Refactor to summarize.
This commit is contained in:
parent
39c59e951e
commit
3aa87c3faf
1 changed files with 193 additions and 263 deletions
456
index.html
456
index.html
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue