・カンバン方式
・インストール不要。メモ帳とブラウザがあればどこでも動く
・エクスポート・インポート機能あり
kanban.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>カンバンタスク管理</title>
<style>
body {
font-family: 'Segoe UI', 'Meiryo UI', sans-serif;
margin: 20px;
background-color: #f9f9f9;
color: #333;
}
.controls {
margin-bottom: 20px;
}
.controls button {
background-color: #6d28d9;
color: white;
border: none;
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
margin-right: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
.controls button:hover {
background-color: #5b21b6;
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.container {
display: flex;
gap: 20px;
overflow-x: auto;
padding: 20px 0;
scrollbar-width: thin;
scrollbar-color: #6d28d9 #f0f0f0;
}
.container::-webkit-scrollbar {
height: 8px;
}
.container::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 4px;
}
.container::-webkit-scrollbar-thumb {
background-color: #6d28d9;
border-radius: 4px;
}
.column {
background: #f3f4f6;
border-radius: 12px;
min-width: 300px;
max-width: 300px;
padding: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
border: 1px solid #e5e7eb;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.column-header input {
background: transparent;
border: none;
font-size: 16px;
font-weight: bold;
color: #4b5563;
padding: 5px;
border-radius: 4px;
width: 80%;
}
.column-header input:focus {
background: #ffffff;
outline: 2px solid #6d28d9;
}
.column-header button {
background: none;
border: none;
color: #9ca3af;
font-size: 18px;
cursor: pointer;
padding: 0 5px;
transition: color 0.2s;
}
.column-header button:hover {
color: #ef4444;
}
.task {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
cursor: move;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.15s, box-shadow 0.15s;
border-left: 10px solid #6d28d9;
word-wrap: break-word;
overflow-wrap: break-word;
}
.task:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.task-content {
margin-bottom: 12px;
}
.task-title {
font-weight: bold;
margin-bottom: 8px;
font-size: 15px;
color: #1f2937;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
.task-memo {
font-size: 13px;
color: #6b7280;
white-space: pre-wrap;
margin-bottom: 10px;
max-height: 100px;
overflow-y: auto;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
.task button {
background-color: #f3f4f6;
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
margin-right: 5px;
}
.edit-btn {
background-color: #8b5cf6 !important;
color: white !important;
}
.edit-btn:hover {
background-color: #7c3aed !important;
}
.task button:last-child:hover {
background-color: #fecaca;
color: #b91c1c;
}
.add-task-btn {
width: 100%;
padding: 8px;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border: 1px dashed #6d28d9;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
font-size: 14px;
}
.add-task-btn:hover {
background-color: rgba(109, 40, 217, 0.2);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 100;
}
.modal-content {
background: white;
padding: 25px;
width: 500px;
max-width: 90%;
margin: 100px auto;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
animation: modalFadeIn 0.3s;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal h3 {
margin-top: 0;
color: #1f2937;
font-size: 18px;
margin-bottom: 15px;
}
.modal input, .modal textarea {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-family: inherit;
font-size: 14px;
box-sizing: border-box;
}
.modal input:focus, .modal textarea:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
}
.modal textarea {
height: 120px;
resize: vertical;
margin: 10px 0;
}
.color-palette {
display: flex;
gap: 12px;
margin: 15px 0;
flex-wrap: wrap;
}
.color-option {
width: 36px;
height: 36px;
border: 2px solid #e5e7eb;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option[data-color="#FFCCD0"] { background-color: #FFCCD0; }
.color-option[data-color="#B5EAD7"] { background-color: #B5EAD7; }
.color-option[data-color="#C7CEEA"] { background-color: #C7CEEA; }
.color-option[data-color="#FDE9C2"] { background-color: #FDE9C2; }
#editColor {
width: 36px;
height: 36px;
border: 2px solid #e5e7eb;
background: none;
padding: 0;
cursor: pointer;
border-radius: 50%;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.modal-buttons button {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.save-btn {
background-color: #8b5cf6;
color: white;
border: none;
}
.save-btn:hover {
background-color: #7c3aed;
}
.cancel-btn {
background-color: #f3f4f6;
color: #4b5563;
border: 1px solid #d1d5db;
}
.cancel-btn:hover {
background-color: #e5e7eb;
}
.task-placeholder {
height: 4px;
background: #8b5cf6;
margin: 8px 0;
visibility: hidden;
border-radius: 2px;
}
.dragging {
opacity: 0.5;
}
.drag-over {
background-color: #f0f8ff;
}
/* カスタムスクロールバー */
.task-memo::-webkit-scrollbar {
width: 6px;
}
.task-memo::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.task-memo::-webkit-scrollbar-thumb {
background: #c4b5fd;
border-radius: 3px;
}
.task-memo::-webkit-scrollbar-thumb:hover {
background: #a78bfa;
}
</style>
</head>
<body>
<div class="controls">
<button onclick="addColumn()">列を追加</button>
<button onclick="exportData()">エクスポート</button>
<input type="file" id="importFile" accept=".json" style="display: none;">
<button onclick="document.getElementById('importFile').click()">インポート</button>
</div>
<div class="container" id="container"></div>
<!-- モーダルウィンドウ -->
<div id="taskModal" class="modal">
<div class="modal-content">
<h3>タスクの編集</h3>
<input type="text" id="editTitle" placeholder="タイトル" maxlength="100">
<textarea id="editMemo" placeholder="詳細メモ"></textarea>
<div class="color-palette">
<button class="color-option" data-color="#FFCCD0" title="淡いピンク"></button>
<button class="color-option" data-color="#B5EAD7" title="淡いミント"></button>
<button class="color-option" data-color="#C7CEEA" title="淡いパープル"></button>
<button class="color-option" data-color="#FDE9C2" title="淡いイエロー"></button>
<input type="color" id="editColor" title="カスタムカラー">
</div>
<div class="modal-buttons">
<button class="cancel-btn" onclick="closeModal()">キャンセル</button>
<button class="save-btn" onclick="saveTaskChanges()">保存</button>
</div>
</div>
</div>
<script>
// カラーオプションクリック処理
document.querySelectorAll('.color-option').forEach(btn => {
btn.addEventListener('click', function() {
const color = this.dataset.color;
document.getElementById('editColor').value = color;
// 選択状態の視覚的フィードバック
document.querySelectorAll('.color-option').forEach(o =>
o.style.borderColor = '#e5e7eb');
this.style.borderColor = '#6d28d9';
});
});
// グローバル変数宣言
let columns = JSON.parse(localStorage.getItem('kanban')) || [
{ id: 'col1', name: 'To Do', tasks: [] },
{ id: 'col2', name: 'Doing', tasks: [] },
{ id: 'col3', name: 'Done', tasks: [] }
];
let draggedTask = null;
let sourceColumn = null;
let currentEditingTask = null;
// データ保存関数
function saveData() {
localStorage.setItem('kanban', JSON.stringify(columns));
render();
}
// レンダリング関数
function render() {
const container = document.getElementById('container');
container.innerHTML = '';
columns.forEach(column => {
const colEl = document.createElement('div');
colEl.className = 'column';
colEl.dataset.columnId = column.id;
colEl.ondragover = handleDragOver;
colEl.ondrop = handleDrop;
// 列ヘッダー
const header = document.createElement('div');
header.className = 'column-header';
const nameInput = document.createElement('input');
nameInput.value = column.name;
nameInput.onchange = (e) => {
column.name = e.target.value;
saveData();
};
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '×';
deleteBtn.onclick = () => {
if (column.tasks.length > 0) {
if (!confirm(`「${column.name}」列には${column.tasks.length}個のタスクがあります。\n削除してもよろしいですか?`)) {
return;
}
}
columns = columns.filter(c => c.id !== column.id);
saveData();
};
header.appendChild(nameInput);
header.appendChild(deleteBtn);
colEl.appendChild(header);
// タスクリスト
const taskList = document.createElement('div');
taskList.className = 'task-list';
column.tasks.forEach(task => {
const taskEl = document.createElement('div');
taskEl.className = 'task';
taskEl.draggable = true;
taskEl.dataset.taskId = task.id;
taskEl.style.borderLeftColor = task.color || '#6d28d9';
taskEl.ondragstart = handleDragStart;
// タスク内容
const content = document.createElement('div');
content.className = 'task-content';
const title = document.createElement('div');
title.className = 'task-title';
title.textContent = task.title;
const memo = document.createElement('div');
memo.className = 'task-memo';
memo.textContent = task.memo || '';
// 編集ボタン
const editBtn = document.createElement('button');
editBtn.textContent = '編集';
editBtn.className = 'edit-btn';
editBtn.onclick = () => openEditModal(task, column);
// 削除ボタン
const deleteTaskBtn = document.createElement('button');
deleteTaskBtn.textContent = '削除';
deleteTaskBtn.onclick = (e) => {
e.stopPropagation();
if (confirm('このタスクを削除してもよろしいですか?')) {
column.tasks = column.tasks.filter(t => t.id !== task.id);
saveData();
}
};
content.appendChild(title);
content.appendChild(memo);
taskEl.appendChild(content);
taskEl.appendChild(editBtn);
taskEl.appendChild(deleteTaskBtn);
taskList.appendChild(taskEl);
});
colEl.appendChild(taskList);
// タスク追加ボタン
const addTaskBtn = document.createElement('button');
addTaskBtn.textContent = '+ タスク追加';
addTaskBtn.className = 'add-task-btn';
addTaskBtn.onclick = () => {
const newTask = {
id: Date.now().toString(),
title: '',
memo: '',
color: '#C7CEEA'
};
column.tasks.push(newTask);
saveData();
// 追加後すぐに編集モーダルを開く
setTimeout(() => {
openEditModal(newTask, column);
}, 100);
};
colEl.appendChild(addTaskBtn);
container.appendChild(colEl);
});
}
// ドラッグ&ドロップ処理
function handleDragStart(e) {
draggedTask = e.target.closest('.task');
sourceColumn = draggedTask.closest('.column');
draggedTask.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragOver(e) {
e.preventDefault();
const targetColumn = e.target.closest('.column');
const taskList = targetColumn.querySelector('.task-list') || targetColumn;
// プレースホルダーの削除
document.querySelectorAll('.task-placeholder').forEach(p => p.remove());
// マウスの位置からの挿入位置を計算
const afterElement = getDragAfterElement(targetColumn, e.clientY);
// プレースホルダーの表示
const placeholder = document.createElement('div');
placeholder.className = 'task-placeholder';
if (afterElement) {
taskList.insertBefore(placeholder, afterElement);
} else {
taskList.appendChild(placeholder);
}
placeholder.style.visibility = 'visible';
}
function handleDrop(e) {
e.preventDefault();
const targetColumn = e.target.closest('.column');
// プレースホルダーの削除
document.querySelectorAll('.task-placeholder').forEach(p => p.remove());
if (!targetColumn) return;
const sourceCol = columns.find(c => c.id === sourceColumn.dataset.columnId);
const targetCol = columns.find(c => c.id === targetColumn.dataset.columnId);
const taskId = draggedTask.dataset.taskId;
const task = sourceCol.tasks.find(t => t.id === taskId);
// 挿入位置の決定
const afterElement = getDragAfterElement(targetColumn, e.clientY);
const insertIndex = afterElement && afterElement.dataset.taskId
? targetCol.tasks.findIndex(t => t.id === afterElement.dataset.taskId)
: targetCol.tasks.length;
// タスクの移動
sourceCol.tasks = sourceCol.tasks.filter(t => t.id !== taskId);
targetCol.tasks.splice(insertIndex, 0, task);
draggedTask.classList.remove('dragging');
saveData();
}
// 挿入位置検出用ヘルパー関数
function getDragAfterElement(container, y) {
const tasks = [...container.querySelectorAll('.task:not(.dragging)')];
return tasks.reduce((closest, task) => {
const box = task.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: task };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
// モーダル関連関数
function openEditModal(task, column) {
currentEditingTask = task;
document.getElementById('editTitle').value = task.title;
document.getElementById('editMemo').value = task.memo || '';
document.getElementById('editColor').value = task.color || '#C7CEEA';
// 現在の色がプリセットの場合のハイライト
document.querySelectorAll('.color-option').forEach(btn => {
btn.style.borderColor = btn.dataset.color === task.color ? '#6d28d9' : '#e5e7eb';
});
document.getElementById('taskModal').style.display = 'block';
document.getElementById('editTitle').focus();
}
function closeModal() {
document.getElementById('taskModal').style.display = 'none';
}
function saveTaskChanges() {
if (!currentEditingTask) return;
const newTitle = document.getElementById('editTitle').value.trim();
if (!newTitle) {
alert('タイトルは必須です');
return;
}
currentEditingTask.title = newTitle;
currentEditingTask.memo = document.getElementById('editMemo').value;
currentEditingTask.color = document.getElementById('editColor').value;
saveData();
closeModal();
}
// その他の関数
function addColumn() {
const name = prompt('新しい列の名前を入力してください');
if (name && name.trim()) {
columns.push({
id: Date.now().toString(),
name: name.trim(),
tasks: []
});
saveData();
}
}
function exportData() {
const data = JSON.stringify(columns, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'kanban-data.json';
a.click();
URL.revokeObjectURL(url);
}
document.getElementById('importFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function() {
try {
const importedData = JSON.parse(reader.result);
if (Array.isArray(importedData)) {
columns = importedData;
saveData();
alert('データのインポートが完了しました');
} else {
alert('無効なデータ形式です');
}
} catch (error) {
alert('データの読み込みに失敗しました: ' + error.message);
}
};
reader.readAsText(file);
e.target.value = '';
});
// キーボードショートカット
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
} else if (e.key === 'Enter' && e.ctrlKey && document.getElementById('taskModal').style.display === 'block') {
saveTaskChanges();
}
});
// クリック時のモーダル外閉じる
window.addEventListener('click', function(e) {
const modal = document.getElementById('taskModal');
if (e.target === modal) {
closeModal();
}
});
// 初期表示
render();
</script>
</body>
</html>