Delete v6_軽量KanbanBoard
Browse files- v6_軽量KanbanBoard/app.js +0 -1545
- v6_軽量KanbanBoard/index.html +0 -509
- v6_軽量KanbanBoard/tailwindcdn.js +0 -0
v6_軽量KanbanBoard/app.js
DELETED
|
@@ -1,1545 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
(() => {
|
| 3 |
-
"use strict";
|
| 4 |
-
|
| 5 |
-
const STORAGE_KEY = "toyota-kanban-tasks-v1";
|
| 6 |
-
const SETTINGS_KEY = "toyota-kanban-settings-v1";
|
| 7 |
-
const DEFAULT_TITLE = "トヨタ式カンバンボード";
|
| 8 |
-
const PANEL_KEYS = ["analytics", "filters", "form"];
|
| 9 |
-
|
| 10 |
-
const columnSettings = {
|
| 11 |
-
backlog: { wip: Number.POSITIVE_INFINITY, label: "バックログ" },
|
| 12 |
-
ready: { wip: 5, label: "準備中" },
|
| 13 |
-
inProgress: { wip: 3, label: "仕掛中" },
|
| 14 |
-
waiting: { wip: 2, label: "停止中" },
|
| 15 |
-
done: { wip: Number.POSITIVE_INFINITY, label: "完了" }
|
| 16 |
-
};
|
| 17 |
-
|
| 18 |
-
const form = document.getElementById("task-form");
|
| 19 |
-
const fields = {
|
| 20 |
-
title: document.getElementById("task-title"),
|
| 21 |
-
owner: document.getElementById("task-owner"),
|
| 22 |
-
due: document.getElementById("task-due"),
|
| 23 |
-
status: document.getElementById("task-status"),
|
| 24 |
-
notes: document.getElementById("task-notes"),
|
| 25 |
-
tags: document.getElementById("task-tags"),
|
| 26 |
-
link: document.getElementById("task-link")
|
| 27 |
-
};
|
| 28 |
-
const messageArea = document.getElementById("message-area");
|
| 29 |
-
const seedButton = document.getElementById("seed-demo");
|
| 30 |
-
const template = document.getElementById("task-template");
|
| 31 |
-
const dropzones = Array.from(document.querySelectorAll("[data-dropzone]"));
|
| 32 |
-
const counters = Array.from(document.querySelectorAll("[data-counter]"));
|
| 33 |
-
const metricElements = {
|
| 34 |
-
total: document.querySelector("[data-metric=\"total\"]"),
|
| 35 |
-
active: document.querySelector("[data-metric=\"active\"]"),
|
| 36 |
-
throughput7: document.querySelector("[data-metric=\"throughput7\"]"),
|
| 37 |
-
leadtime: document.querySelector("[data-metric=\"leadtime\"]")
|
| 38 |
-
};
|
| 39 |
-
const editDialog = document.getElementById("edit-dialog");
|
| 40 |
-
const editForm = document.getElementById("edit-form");
|
| 41 |
-
const editFields = editForm
|
| 42 |
-
? {
|
| 43 |
-
title: document.getElementById("edit-title"),
|
| 44 |
-
owner: document.getElementById("edit-owner"),
|
| 45 |
-
due: document.getElementById("edit-due"),
|
| 46 |
-
notes: document.getElementById("edit-notes"),
|
| 47 |
-
tags: document.getElementById("edit-tags"),
|
| 48 |
-
link: document.getElementById("edit-link")
|
| 49 |
-
}
|
| 50 |
-
: null;
|
| 51 |
-
|
| 52 |
-
const boardTitleHeading = document.getElementById("board-title-heading");
|
| 53 |
-
const titleEditButton = document.getElementById("title-edit");
|
| 54 |
-
const panelSections = Array.from(document.querySelectorAll("[data-panel]"));
|
| 55 |
-
const panels = new Map(panelSections.map((section) => [section.dataset.panel, section]));
|
| 56 |
-
const panelToggleButtons = Array.from(document.querySelectorAll("[data-action=\"toggle-panel\"]"));
|
| 57 |
-
|
| 58 |
-
const filterElements = {
|
| 59 |
-
keyword: document.getElementById("filter-keyword"),
|
| 60 |
-
dueStart: document.getElementById("filter-due-start"),
|
| 61 |
-
dueEnd: document.getElementById("filter-due-end"),
|
| 62 |
-
clear: document.getElementById("filter-clear"),
|
| 63 |
-
tagList: document.getElementById("filter-tag-list")
|
| 64 |
-
};
|
| 65 |
-
const selectionElements = {
|
| 66 |
-
counter: document.getElementById("selection-counter"),
|
| 67 |
-
hint: document.getElementById("selection-hint"),
|
| 68 |
-
selectAll: document.getElementById("selection-selectall"),
|
| 69 |
-
clear: document.getElementById("selection-clear"),
|
| 70 |
-
delete: document.getElementById("selection-delete")
|
| 71 |
-
};
|
| 72 |
-
const dataTransferElements = {
|
| 73 |
-
exportButton: document.getElementById("data-export"),
|
| 74 |
-
importButton: document.getElementById("data-import"),
|
| 75 |
-
importInput: document.getElementById("data-import-input")
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
const uiElements = {
|
| 79 |
-
viewToggle: document.getElementById("view-toggle")
|
| 80 |
-
};
|
| 81 |
-
|
| 82 |
-
const filterState = {
|
| 83 |
-
keyword: "",
|
| 84 |
-
dueStart: "",
|
| 85 |
-
dueEnd: "",
|
| 86 |
-
tags: new Set()
|
| 87 |
-
};
|
| 88 |
-
|
| 89 |
-
const selectionState = {
|
| 90 |
-
ids: new Set(),
|
| 91 |
-
lastVisibleIds: []
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
const uiState = {
|
| 95 |
-
compact: false
|
| 96 |
-
};
|
| 97 |
-
|
| 98 |
-
let settings = createDefaultSettings();
|
| 99 |
-
let tasks = [];
|
| 100 |
-
let messageTimer = null;
|
| 101 |
-
let draggingTaskId = null;
|
| 102 |
-
let editingTaskId = null;
|
| 103 |
-
|
| 104 |
-
init();
|
| 105 |
-
|
| 106 |
-
function createDefaultSettings() {
|
| 107 |
-
return {
|
| 108 |
-
title: DEFAULT_TITLE,
|
| 109 |
-
panels: PANEL_KEYS.reduce((acc, key) => {
|
| 110 |
-
acc[key] = false;
|
| 111 |
-
return acc;
|
| 112 |
-
}, {})
|
| 113 |
-
};
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
function init() {
|
| 117 |
-
loadSettings();
|
| 118 |
-
applyTitle();
|
| 119 |
-
applyPanelStates({ initial: true });
|
| 120 |
-
loadFromStorage();
|
| 121 |
-
attachFormEvents();
|
| 122 |
-
attachDropzoneEvents();
|
| 123 |
-
attachSeedButton();
|
| 124 |
-
attachEditForm();
|
| 125 |
-
attachFilterHandlers();
|
| 126 |
-
attachSelectionHandlers();
|
| 127 |
-
attachDataTransferHandlers();
|
| 128 |
-
attachViewHandlers();
|
| 129 |
-
attachPanelHandlers();
|
| 130 |
-
attachTitleEditor();
|
| 131 |
-
form?.addEventListener("reset", () => hideMessage());
|
| 132 |
-
updateViewToggleLabel();
|
| 133 |
-
updateSelectionUI();
|
| 134 |
-
render();
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
function loadSettings() {
|
| 138 |
-
try {
|
| 139 |
-
const raw = window.localStorage.getItem(SETTINGS_KEY);
|
| 140 |
-
if (raw) {
|
| 141 |
-
const parsed = JSON.parse(raw);
|
| 142 |
-
if (parsed && typeof parsed === "object") {
|
| 143 |
-
settings = {
|
| 144 |
-
title: typeof parsed.title === "string" && parsed.title.trim() ? parsed.title.trim() : DEFAULT_TITLE,
|
| 145 |
-
panels: { ...createDefaultSettings().panels, ...(parsed.panels || {}) }
|
| 146 |
-
};
|
| 147 |
-
return;
|
| 148 |
-
}
|
| 149 |
-
}
|
| 150 |
-
} catch (error) {
|
| 151 |
-
console.warn("Failed to load settings:", error);
|
| 152 |
-
}
|
| 153 |
-
settings = createDefaultSettings();
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
function saveSettings() {
|
| 157 |
-
try {
|
| 158 |
-
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
| 159 |
-
} catch (error) {
|
| 160 |
-
console.warn("Failed to save settings:", error);
|
| 161 |
-
}
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
function applyTitle() {
|
| 165 |
-
if (boardTitleHeading) {
|
| 166 |
-
boardTitleHeading.textContent = settings.title;
|
| 167 |
-
}
|
| 168 |
-
document.title = settings.title;
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
function attachTitleEditor() {
|
| 172 |
-
titleEditButton?.addEventListener("click", handleTitleEdit);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
function handleTitleEdit() {
|
| 176 |
-
const current = settings.title || DEFAULT_TITLE;
|
| 177 |
-
const next = window.prompt("ボードタイトルを入力してください。", current);
|
| 178 |
-
if (next === null) return;
|
| 179 |
-
const trimmed = next.trim();
|
| 180 |
-
settings.title = trimmed || DEFAULT_TITLE;
|
| 181 |
-
applyTitle();
|
| 182 |
-
saveSettings();
|
| 183 |
-
showMessage("タイトルを更新しました。", "info");
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
function attachPanelHandlers() {
|
| 187 |
-
panelToggleButtons.forEach((button) => {
|
| 188 |
-
button.addEventListener("click", () => {
|
| 189 |
-
const key = button.dataset.target;
|
| 190 |
-
const section = panels.get(key);
|
| 191 |
-
if (!key || !section) return;
|
| 192 |
-
const willCollapse = !section.classList.contains("panel-collapsed");
|
| 193 |
-
setPanelCollapsed(key, willCollapse);
|
| 194 |
-
});
|
| 195 |
-
});
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
function applyPanelStates({ initial = false } = {}) {
|
| 199 |
-
PANEL_KEYS.forEach((key) => {
|
| 200 |
-
const collapsed = Boolean(settings.panels[key]);
|
| 201 |
-
setPanelCollapsed(key, collapsed, { save: !initial });
|
| 202 |
-
});
|
| 203 |
-
}
|
| 204 |
-
function setPanelCollapsed(panelKey, collapsed, { save = true } = {}) {
|
| 205 |
-
const section = panels.get(panelKey);
|
| 206 |
-
if (!section) return;
|
| 207 |
-
section.classList.toggle("panel-collapsed", collapsed);
|
| 208 |
-
const body = section.querySelector(`[data-panel-body="${panelKey}"]`);
|
| 209 |
-
if (body) {
|
| 210 |
-
body.hidden = collapsed;
|
| 211 |
-
}
|
| 212 |
-
updatePanelButton(panelKey, collapsed);
|
| 213 |
-
settings.panels[panelKey] = collapsed;
|
| 214 |
-
if (save) {
|
| 215 |
-
saveSettings();
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
function updatePanelButton(panelKey, collapsed) {
|
| 220 |
-
const button = panelToggleButtons.find((item) => item.dataset.target === panelKey);
|
| 221 |
-
if (!button) return;
|
| 222 |
-
button.textContent = collapsed ? "展開する" : "折りたたむ";
|
| 223 |
-
button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
function attachFormEvents() {
|
| 227 |
-
form?.addEventListener("submit", handleSubmit);
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
function handleSubmit(event) {
|
| 231 |
-
event.preventDefault();
|
| 232 |
-
const rawTitle = fields.title.value.trim();
|
| 233 |
-
const rawOwner = fields.owner.value.trim();
|
| 234 |
-
const rawNotes = fields.notes.value.trim();
|
| 235 |
-
const status = fields.status.value;
|
| 236 |
-
const due = fields.due.value;
|
| 237 |
-
const tags = parseTags(fields.tags?.value ?? "");
|
| 238 |
-
const link = normalizeLink(fields.link?.value ?? "");
|
| 239 |
-
|
| 240 |
-
if (!rawTitle) {
|
| 241 |
-
showMessage("タイトルを入力してください。", "warn");
|
| 242 |
-
fields.title.focus();
|
| 243 |
-
return;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
if (!canEnterColumn(status)) {
|
| 247 |
-
const limit = columnSettings[status]?.wip;
|
| 248 |
-
const label = columnSettings[status]?.label ?? status;
|
| 249 |
-
showMessage(`「${label}」はWIP上限(${limit})を超えられません。`, "warn");
|
| 250 |
-
return;
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
const now = new Date();
|
| 254 |
-
const newTask = {
|
| 255 |
-
id: createId(),
|
| 256 |
-
title: rawTitle,
|
| 257 |
-
owner: rawOwner,
|
| 258 |
-
due,
|
| 259 |
-
notes: rawNotes,
|
| 260 |
-
status,
|
| 261 |
-
link,
|
| 262 |
-
createdAt: now.toISOString(),
|
| 263 |
-
tags
|
| 264 |
-
};
|
| 265 |
-
|
| 266 |
-
if (status === "done") {
|
| 267 |
-
newTask.completedAt = now.toISOString();
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
tasks.push(newTask);
|
| 271 |
-
persist();
|
| 272 |
-
render();
|
| 273 |
-
form.reset();
|
| 274 |
-
fields.title.focus();
|
| 275 |
-
showMessage("タスクを登録しました。", "info");
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
function attachDropzoneEvents() {
|
| 279 |
-
dropzones.forEach((zone) => {
|
| 280 |
-
zone.addEventListener("dragover", handleDragOver);
|
| 281 |
-
zone.addEventListener("dragleave", handleDragLeave);
|
| 282 |
-
zone.addEventListener("drop", handleDrop);
|
| 283 |
-
});
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
function attachSeedButton() {
|
| 287 |
-
seedButton?.addEventListener("click", seedDemoData);
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
function attachEditForm() {
|
| 291 |
-
if (!editForm || !editDialog || !editFields) return;
|
| 292 |
-
|
| 293 |
-
editForm.addEventListener("submit", handleEditSubmit);
|
| 294 |
-
const cancelButton = editForm.querySelector("[data-action=\"cancel-edit\"]");
|
| 295 |
-
cancelButton?.addEventListener("click", () => closeEditDialog());
|
| 296 |
-
|
| 297 |
-
editDialog.addEventListener("cancel", () => closeEditDialog(true));
|
| 298 |
-
editDialog.addEventListener("close", () => resetEditingState());
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
function attachFilterHandlers() {
|
| 302 |
-
filterElements.keyword?.addEventListener("input", handleKeywordFilter);
|
| 303 |
-
filterElements.dueStart?.addEventListener("change", handleDueFilter);
|
| 304 |
-
filterElements.dueEnd?.addEventListener("change", handleDueFilter);
|
| 305 |
-
filterElements.clear?.addEventListener("click", clearFilters);
|
| 306 |
-
filterElements.tagList?.addEventListener("click", handleTagFilterClick);
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
function attachSelectionHandlers() {
|
| 310 |
-
selectionElements.selectAll?.addEventListener("click", selectAllVisible);
|
| 311 |
-
selectionElements.clear?.addEventListener("click", () => {
|
| 312 |
-
clearSelection();
|
| 313 |
-
syncSelectionCheckboxes();
|
| 314 |
-
});
|
| 315 |
-
selectionElements.delete?.addEventListener("click", deleteSelectedTasks);
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
function attachDataTransferHandlers() {
|
| 319 |
-
const { exportButton, importButton, importInput } = dataTransferElements;
|
| 320 |
-
exportButton?.addEventListener("click", handleExportTasks);
|
| 321 |
-
importButton?.addEventListener("click", () => importInput?.click());
|
| 322 |
-
importInput?.addEventListener("change", handleImportFileSelection);
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
function handleExportTasks() {
|
| 326 |
-
if (!tasks.length) {
|
| 327 |
-
showMessage("エクスポートできるカードがありません。", "warn");
|
| 328 |
-
return;
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
try {
|
| 332 |
-
const now = new Date();
|
| 333 |
-
const payload = {
|
| 334 |
-
version: 1,
|
| 335 |
-
exportedAt: now.toISOString(),
|
| 336 |
-
boardTitle: settings.title,
|
| 337 |
-
tasks: tasks.map((task) => ({ ...task }))
|
| 338 |
-
};
|
| 339 |
-
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
| 340 |
-
const filename = `kanban-export-${now.toISOString().replace(/[:.]/g, "-")}.json`;
|
| 341 |
-
const url = URL.createObjectURL(blob);
|
| 342 |
-
const link = document.createElement("a");
|
| 343 |
-
link.href = url;
|
| 344 |
-
link.download = filename;
|
| 345 |
-
document.body.appendChild(link);
|
| 346 |
-
link.click();
|
| 347 |
-
window.setTimeout(() => {
|
| 348 |
-
document.body.removeChild(link);
|
| 349 |
-
URL.revokeObjectURL(url);
|
| 350 |
-
}, 0);
|
| 351 |
-
showMessage("カードデータをエクスポートしました。", "info");
|
| 352 |
-
} catch (error) {
|
| 353 |
-
console.warn("Failed to export tasks:", error);
|
| 354 |
-
showMessage("エクスポートに失敗しました。", "warn");
|
| 355 |
-
}
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
function handleImportFileSelection(event) {
|
| 359 |
-
const input = event.target;
|
| 360 |
-
if (!input?.files?.length) return;
|
| 361 |
-
|
| 362 |
-
const [file] = input.files;
|
| 363 |
-
const reader = new FileReader();
|
| 364 |
-
|
| 365 |
-
reader.addEventListener("load", () => {
|
| 366 |
-
try {
|
| 367 |
-
const imported = parseImportedTasks(reader.result);
|
| 368 |
-
if (!imported.length) {
|
| 369 |
-
showMessage("取り込めるカードが見つかりませんでした。", "warn");
|
| 370 |
-
} else {
|
| 371 |
-
tasks = imported;
|
| 372 |
-
selectionState.ids.clear();
|
| 373 |
-
persist();
|
| 374 |
-
render();
|
| 375 |
-
showMessage(`${imported.length}件のカードをインポートしました。`, "info");
|
| 376 |
-
}
|
| 377 |
-
} catch (error) {
|
| 378 |
-
console.warn("Failed to import tasks:", error);
|
| 379 |
-
showMessage("インポートに失敗しました。", "warn");
|
| 380 |
-
} finally {
|
| 381 |
-
input.value = "";
|
| 382 |
-
}
|
| 383 |
-
});
|
| 384 |
-
|
| 385 |
-
reader.addEventListener("error", () => {
|
| 386 |
-
console.warn("Failed to read import file:", reader.error);
|
| 387 |
-
showMessage("ファイルを読み取れませんでした。", "warn");
|
| 388 |
-
input.value = "";
|
| 389 |
-
});
|
| 390 |
-
|
| 391 |
-
reader.readAsText(file, "utf-8");
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
function parseImportedTasks(raw) {
|
| 395 |
-
if (raw == null) return [];
|
| 396 |
-
|
| 397 |
-
let data;
|
| 398 |
-
try {
|
| 399 |
-
data = JSON.parse(String(raw));
|
| 400 |
-
} catch (error) {
|
| 401 |
-
throw new Error("Invalid JSON");
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
const entries = Array.isArray(data) ? data : Array.isArray(data?.tasks) ? data.tasks : [];
|
| 405 |
-
if (!entries.length) {
|
| 406 |
-
return [];
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
const seenIds = new Set();
|
| 410 |
-
const imported = [];
|
| 411 |
-
|
| 412 |
-
entries.forEach((item) => {
|
| 413 |
-
if (!item || typeof item !== "object") return;
|
| 414 |
-
const candidate = { ...item };
|
| 415 |
-
let id = normalizeImportedId(candidate.id);
|
| 416 |
-
if (!id) {
|
| 417 |
-
id = createId();
|
| 418 |
-
}
|
| 419 |
-
while (seenIds.has(id)) {
|
| 420 |
-
id = createId();
|
| 421 |
-
}
|
| 422 |
-
candidate.id = id;
|
| 423 |
-
const normalized = normalizeTask(candidate);
|
| 424 |
-
if (!normalized.title) return;
|
| 425 |
-
seenIds.add(normalized.id);
|
| 426 |
-
imported.push(normalized);
|
| 427 |
-
});
|
| 428 |
-
|
| 429 |
-
return imported;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
function normalizeImportedId(value) {
|
| 433 |
-
if (typeof value === "string") {
|
| 434 |
-
const trimmed = value.trim();
|
| 435 |
-
return trimmed ? trimmed : "";
|
| 436 |
-
}
|
| 437 |
-
if (typeof value === "number" && Number.isFinite(value)) {
|
| 438 |
-
return String(value);
|
| 439 |
-
}
|
| 440 |
-
return "";
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
function attachViewHandlers() {
|
| 444 |
-
uiElements.viewToggle?.addEventListener("click", toggleViewMode);
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
function handleKeywordFilter(event) {
|
| 448 |
-
filterState.keyword = event.target.value.trim();
|
| 449 |
-
render();
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
function handleDueFilter() {
|
| 453 |
-
filterState.dueStart = filterElements.dueStart?.value ?? "";
|
| 454 |
-
filterState.dueEnd = filterElements.dueEnd?.value ?? "";
|
| 455 |
-
render();
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
function handleTagFilterClick(event) {
|
| 459 |
-
const button = event.target.closest("[data-tag-value]");
|
| 460 |
-
if (!button) return;
|
| 461 |
-
const tag = button.dataset.tagValue;
|
| 462 |
-
if (!tag) return;
|
| 463 |
-
|
| 464 |
-
if (filterState.tags.has(tag)) {
|
| 465 |
-
filterState.tags.delete(tag);
|
| 466 |
-
} else {
|
| 467 |
-
filterState.tags.add(tag);
|
| 468 |
-
}
|
| 469 |
-
render();
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
function clearFilters() {
|
| 473 |
-
filterState.keyword = "";
|
| 474 |
-
filterState.dueStart = "";
|
| 475 |
-
filterState.dueEnd = "";
|
| 476 |
-
filterState.tags.clear();
|
| 477 |
-
if (filterElements.keyword) {
|
| 478 |
-
filterElements.keyword.value = "";
|
| 479 |
-
}
|
| 480 |
-
if (filterElements.dueStart) {
|
| 481 |
-
filterElements.dueStart.value = "";
|
| 482 |
-
}
|
| 483 |
-
if (filterElements.dueEnd) {
|
| 484 |
-
filterElements.dueEnd.value = "";
|
| 485 |
-
}
|
| 486 |
-
render();
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
function handleDragStart(event) {
|
| 490 |
-
const card = event.currentTarget;
|
| 491 |
-
const taskId = card.dataset.taskId;
|
| 492 |
-
if (!taskId) return;
|
| 493 |
-
draggingTaskId = taskId;
|
| 494 |
-
event.dataTransfer.effectAllowed = "move";
|
| 495 |
-
event.dataTransfer.setData("text/plain", taskId);
|
| 496 |
-
card.classList.add("opacity-40");
|
| 497 |
-
}
|
| 498 |
-
|
| 499 |
-
function handleDragEnd(event) {
|
| 500 |
-
event.currentTarget.classList.remove("opacity-40");
|
| 501 |
-
draggingTaskId = null;
|
| 502 |
-
dropzones.forEach((zone) => setDropzoneHighlight(zone, "none"));
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
function handleDragOver(event) {
|
| 506 |
-
event.preventDefault();
|
| 507 |
-
const zone = event.currentTarget;
|
| 508 |
-
const targetColumn = zone.dataset.dropzone;
|
| 509 |
-
const allowed = canEnterColumn(targetColumn, draggingTaskId);
|
| 510 |
-
event.dataTransfer.dropEffect = allowed ? "move" : "none";
|
| 511 |
-
setDropzoneHighlight(zone, allowed ? "allowed" : "denied");
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
function handleDragLeave(event) {
|
| 515 |
-
setDropzoneHighlight(event.currentTarget, "none");
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
function handleDrop(event) {
|
| 519 |
-
event.preventDefault();
|
| 520 |
-
const zone = event.currentTarget;
|
| 521 |
-
const targetColumn = zone.dataset.dropzone;
|
| 522 |
-
const taskId = draggingTaskId || event.dataTransfer.getData("text/plain");
|
| 523 |
-
|
| 524 |
-
setDropzoneHighlight(zone, "none");
|
| 525 |
-
|
| 526 |
-
if (!taskId) return;
|
| 527 |
-
|
| 528 |
-
if (!canEnterColumn(targetColumn, taskId)) {
|
| 529 |
-
const limit = columnSettings[targetColumn]?.wip;
|
| 530 |
-
const label = columnSettings[targetColumn]?.label ?? targetColumn;
|
| 531 |
-
showMessage(`「${label}」はWIP上限(${limit})を超えられません。`, "warn");
|
| 532 |
-
return;
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
moveTask(taskId, targetColumn);
|
| 536 |
-
}
|
| 537 |
-
|
| 538 |
-
function moveTask(taskId, targetColumn) {
|
| 539 |
-
const task = tasks.find((item) => item.id === taskId);
|
| 540 |
-
if (!task || task.status === targetColumn) return;
|
| 541 |
-
|
| 542 |
-
task.status = targetColumn;
|
| 543 |
-
if (targetColumn === "done") {
|
| 544 |
-
task.completedAt = new Date().toISOString();
|
| 545 |
-
}
|
| 546 |
-
|
| 547 |
-
persist();
|
| 548 |
-
render();
|
| 549 |
-
showMessage(`タスクを「${columnSettings[targetColumn]?.label ?? targetColumn}」へ移動しました。`, "info");
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
function render() {
|
| 553 |
-
cleanupSelection();
|
| 554 |
-
renderTagFilters();
|
| 555 |
-
|
| 556 |
-
const filterActive = isFilterActive();
|
| 557 |
-
const filteredTasks = getFilteredTasks();
|
| 558 |
-
const tasksToRender = filterActive ? filteredTasks : tasks;
|
| 559 |
-
|
| 560 |
-
selectionState.lastVisibleIds = tasksToRender.map((task) => task.id);
|
| 561 |
-
|
| 562 |
-
dropzones.forEach((zone) => zone.replaceChildren());
|
| 563 |
-
|
| 564 |
-
const byColumn = new Map();
|
| 565 |
-
tasksToRender
|
| 566 |
-
.slice()
|
| 567 |
-
.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime())
|
| 568 |
-
.forEach((task) => {
|
| 569 |
-
const list = byColumn.get(task.status) ?? [];
|
| 570 |
-
list.push(task);
|
| 571 |
-
byColumn.set(task.status, list);
|
| 572 |
-
});
|
| 573 |
-
|
| 574 |
-
dropzones.forEach((zone) => {
|
| 575 |
-
const columnKey = zone.dataset.dropzone;
|
| 576 |
-
const columnTasks = byColumn.get(columnKey) ?? [];
|
| 577 |
-
columnTasks.forEach((task) => zone.appendChild(buildTaskCard(task)));
|
| 578 |
-
if (!zone.childElementCount) {
|
| 579 |
-
zone.appendChild(createEmptyState(filterActive));
|
| 580 |
-
}
|
| 581 |
-
});
|
| 582 |
-
|
| 583 |
-
updateCounters(filterActive, filteredTasks);
|
| 584 |
-
renderMetrics(filteredTasks, filterActive);
|
| 585 |
-
syncSelectionCheckboxes();
|
| 586 |
-
updateSelectionUI();
|
| 587 |
-
}
|
| 588 |
-
function buildTaskCard(task) {
|
| 589 |
-
const fragment = template.content.cloneNode(true);
|
| 590 |
-
const card = fragment.querySelector("[data-task-id]");
|
| 591 |
-
card.dataset.taskId = task.id;
|
| 592 |
-
|
| 593 |
-
const isSelected = selectionState.ids.has(task.id);
|
| 594 |
-
card.classList.toggle("kanban-card-selected", isSelected);
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
const titleEl = card.querySelector("[data-field=\"title\"]");
|
| 599 |
-
const ownerEl = card.querySelector("[data-field=\"owner\"]");
|
| 600 |
-
const dueEl = card.querySelector("[data-field=\"due\"]");
|
| 601 |
-
const notesEl = card.querySelector("[data-field=\"notes\"]");
|
| 602 |
-
const tagsEl = card.querySelector("[data-field=\"tags\"]");
|
| 603 |
-
const tagsSection = card.querySelector("[data-section=\"tags\"]");
|
| 604 |
-
const linkSection = card.querySelector("[data-section=\"link\"]");
|
| 605 |
-
const linkEl = card.querySelector("[data-field=\"link\"]");
|
| 606 |
-
const doneButton = card.querySelector("[data-action=\"mark-done\"]");
|
| 607 |
-
const editButton = card.querySelector("[data-action=\"edit-task\"]");
|
| 608 |
-
const deleteButton = card.querySelector("[data-action=\"delete-task\"]");
|
| 609 |
-
const checkbox = card.querySelector("[data-action=\"select-task\"]");
|
| 610 |
-
const toggleDetailsButton = card.querySelector("[data-action=\"toggle-details\"]");
|
| 611 |
-
|
| 612 |
-
titleEl.textContent = task.title;
|
| 613 |
-
ownerEl.textContent = task.owner || "未設定";
|
| 614 |
-
dueEl.textContent = formatDue(task.due);
|
| 615 |
-
notesEl.textContent = task.notes || "メモなし";
|
| 616 |
-
notesEl.classList.toggle("text-slate-500", !task.notes);
|
| 617 |
-
|
| 618 |
-
if (tagsEl) {
|
| 619 |
-
tagsEl.replaceChildren();
|
| 620 |
-
const hasTags = Array.isArray(task.tags) && task.tags.length > 0;
|
| 621 |
-
tagsSection?.classList.toggle("hidden", !hasTags);
|
| 622 |
-
tagsSection?.setAttribute("aria-hidden", hasTags ? "false" : "true");
|
| 623 |
-
tagsSection?.setAttribute("data-has-tags", hasTags ? "true" : "false");
|
| 624 |
-
if (hasTags) {
|
| 625 |
-
task.tags.forEach((tag) => {
|
| 626 |
-
const pill = document.createElement("span");
|
| 627 |
-
pill.textContent = `#${tag}`;
|
| 628 |
-
pill.className = buildTagPillClass(filterState.tags.has(tag));
|
| 629 |
-
tagsEl.appendChild(pill);
|
| 630 |
-
});
|
| 631 |
-
}
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
if (linkSection && linkEl) {
|
| 635 |
-
if (task.link) {
|
| 636 |
-
linkSection.classList.remove("hidden");
|
| 637 |
-
linkEl.href = task.link;
|
| 638 |
-
linkEl.textContent = formatLinkLabel(task.link);
|
| 639 |
-
linkEl.title = task.link;
|
| 640 |
-
} else {
|
| 641 |
-
linkSection.classList.add("hidden");
|
| 642 |
-
linkEl.removeAttribute("href");
|
| 643 |
-
linkEl.removeAttribute("title");
|
| 644 |
-
linkEl.textContent = "";
|
| 645 |
-
}
|
| 646 |
-
}
|
| 647 |
-
|
| 648 |
-
if (checkbox) {
|
| 649 |
-
checkbox.checked = isSelected;
|
| 650 |
-
checkbox.addEventListener("click", (event) => event.stopPropagation());
|
| 651 |
-
checkbox.addEventListener("mousedown", (event) => event.stopPropagation());
|
| 652 |
-
checkbox.addEventListener("change", (event) => toggleSelection(task.id, event.target.checked));
|
| 653 |
-
}
|
| 654 |
-
|
| 655 |
-
if (doneButton) {
|
| 656 |
-
const isDone = task.status === "done";
|
| 657 |
-
doneButton.classList.toggle("hidden", isDone);
|
| 658 |
-
doneButton.addEventListener("click", (event) => {
|
| 659 |
-
event.preventDefault();
|
| 660 |
-
event.stopPropagation();
|
| 661 |
-
moveTask(task.id, "done");
|
| 662 |
-
});
|
| 663 |
-
}
|
| 664 |
-
|
| 665 |
-
editButton?.addEventListener("click", (event) => {
|
| 666 |
-
event.preventDefault();
|
| 667 |
-
event.stopPropagation();
|
| 668 |
-
openEditDialog(task.id);
|
| 669 |
-
});
|
| 670 |
-
deleteButton?.addEventListener("click", (event) => {
|
| 671 |
-
event.preventDefault();
|
| 672 |
-
event.stopPropagation();
|
| 673 |
-
deleteTask(task.id);
|
| 674 |
-
});
|
| 675 |
-
|
| 676 |
-
if (toggleDetailsButton) {
|
| 677 |
-
toggleDetailsButton.addEventListener("click", (event) => {
|
| 678 |
-
event.preventDefault();
|
| 679 |
-
event.stopPropagation();
|
| 680 |
-
card.classList.toggle("kanban-card-expanded");
|
| 681 |
-
updateCardDetailVisibility(card);
|
| 682 |
-
});
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
card.addEventListener("dragstart", handleDragStart);
|
| 686 |
-
card.addEventListener("dragend", handleDragEnd);
|
| 687 |
-
|
| 688 |
-
applyCardLayout(card);
|
| 689 |
-
|
| 690 |
-
return card;
|
| 691 |
-
}
|
| 692 |
-
|
| 693 |
-
function applyCardLayout(card) {
|
| 694 |
-
if (!card) return;
|
| 695 |
-
card.classList.toggle("kanban-card-compact", uiState.compact);
|
| 696 |
-
if (!uiState.compact) {
|
| 697 |
-
card.classList.remove("kanban-card-expanded");
|
| 698 |
-
}
|
| 699 |
-
updateCardDetailVisibility(card);
|
| 700 |
-
}
|
| 701 |
-
|
| 702 |
-
function updateCardDetailVisibility(card) {
|
| 703 |
-
const isExpanded = card.classList.contains("kanban-card-expanded");
|
| 704 |
-
const showDetails = !uiState.compact || isExpanded;
|
| 705 |
-
|
| 706 |
-
const meta = card.querySelector("[data-section=\"meta\"]");
|
| 707 |
-
const notes = card.querySelector("[data-section=\"notes\"]");
|
| 708 |
-
const tags = card.querySelector("[data-section=\"tags\"]");
|
| 709 |
-
const toggleDetailsButton = card.querySelector("[data-action=\"toggle-details\"]");
|
| 710 |
-
|
| 711 |
-
if (meta) {
|
| 712 |
-
meta.classList.toggle("hidden", !showDetails);
|
| 713 |
-
}
|
| 714 |
-
if (notes) {
|
| 715 |
-
notes.classList.toggle("hidden", !showDetails);
|
| 716 |
-
notes.classList.toggle("kanban-note-clamped", uiState.compact && !isExpanded);
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
if (tags) {
|
| 720 |
-
const hasTags = tags && tags.getAttribute("data-has-tags") === "true";
|
| 721 |
-
if (hasTags) {
|
| 722 |
-
tags.classList.toggle("hidden", !showDetails);
|
| 723 |
-
}
|
| 724 |
-
}
|
| 725 |
-
if (toggleDetailsButton) {
|
| 726 |
-
const hasDetails = Boolean((meta && meta.textContent.trim()) || (notes && notes.textContent.trim() && notes.textContent.trim() !== "メモなし"));
|
| 727 |
-
const shouldShowToggle = uiState.compact && hasDetails;
|
| 728 |
-
toggleDetailsButton.classList.toggle("invisible", !shouldShowToggle);
|
| 729 |
-
toggleDetailsButton.classList.toggle("pointer-events-none", !shouldShowToggle);
|
| 730 |
-
toggleDetailsButton.textContent = isExpanded ? "折りたたむ" : "詳細";
|
| 731 |
-
toggleDetailsButton.setAttribute("aria-expanded", isExpanded ? "true" : "false");
|
| 732 |
-
}
|
| 733 |
-
}
|
| 734 |
-
|
| 735 |
-
function createEmptyState(filterActive) {
|
| 736 |
-
const empty = document.createElement("p");
|
| 737 |
-
empty.className = "rounded-lg border border-dashed border-slate-600/60 px-3 py-6 text-center text-xs text-slate-500";
|
| 738 |
-
empty.textContent = filterActive ? "該当するカードがありません" : "カードなし";
|
| 739 |
-
empty.setAttribute("aria-hidden", "true");
|
| 740 |
-
return empty;
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
function updateCounters(filterActive, filteredTasks) {
|
| 744 |
-
counters.forEach((counter) => {
|
| 745 |
-
const columnKey = counter.dataset.counter;
|
| 746 |
-
const totalCount = tasks.filter((task) => task.status === columnKey).length;
|
| 747 |
-
const visibleCount = filterActive
|
| 748 |
-
? filteredTasks.filter((task) => task.status === columnKey).length
|
| 749 |
-
: totalCount;
|
| 750 |
-
|
| 751 |
-
counter.textContent = filterActive ? `${visibleCount}/${totalCount}` : String(totalCount);
|
| 752 |
-
|
| 753 |
-
const parentArticle = counter.closest("article");
|
| 754 |
-
if (!parentArticle) return;
|
| 755 |
-
|
| 756 |
-
const limit = columnSettings[columnKey]?.wip;
|
| 757 |
-
const isLimited = Number.isFinite(limit);
|
| 758 |
-
parentArticle.classList.remove("border", "border-kanban-caution");
|
| 759 |
-
if (isLimited && totalCount >= limit) {
|
| 760 |
-
parentArticle.classList.add("border", "border-kanban-caution");
|
| 761 |
-
}
|
| 762 |
-
});
|
| 763 |
-
}
|
| 764 |
-
|
| 765 |
-
function renderMetrics(filteredTasks, filterActive) {
|
| 766 |
-
const activeStatuses = new Set(["ready", "inProgress", "waiting"]);
|
| 767 |
-
|
| 768 |
-
const visibleTasks = filterActive ? filteredTasks : tasks;
|
| 769 |
-
const totalTasks = tasks.length;
|
| 770 |
-
const visibleTotal = visibleTasks.length;
|
| 771 |
-
|
| 772 |
-
const totalDisplay = filterActive
|
| 773 |
-
? formatFilteredCount(visibleTotal, totalTasks)
|
| 774 |
-
: formatCount(totalTasks);
|
| 775 |
-
|
| 776 |
-
const totalActive = tasks.filter((task) => activeStatuses.has(task.status)).length;
|
| 777 |
-
const visibleActive = visibleTasks.filter((task) => activeStatuses.has(task.status)).length;
|
| 778 |
-
const activeDisplay = filterActive
|
| 779 |
-
? formatFilteredCount(visibleActive, totalActive)
|
| 780 |
-
: formatCount(totalActive);
|
| 781 |
-
|
| 782 |
-
const now = new Date();
|
| 783 |
-
const sevenDaysAgo = new Date(now);
|
| 784 |
-
sevenDaysAgo.setDate(now.getDate() - 7);
|
| 785 |
-
|
| 786 |
-
const totalDone = tasks.filter((task) => task.status === "done");
|
| 787 |
-
const visibleDone = visibleTasks.filter((task) => task.status === "done");
|
| 788 |
-
|
| 789 |
-
const totalThroughput7 = totalDone.filter((task) => isCompletedSince(task, sevenDaysAgo)).length;
|
| 790 |
-
const visibleThroughput7 = visibleDone.filter((task) => isCompletedSince(task, sevenDaysAgo)).length;
|
| 791 |
-
const throughputDisplay = filterActive
|
| 792 |
-
? formatFilteredCount(visibleThroughput7, totalThroughput7)
|
| 793 |
-
: formatCount(totalThroughput7);
|
| 794 |
-
|
| 795 |
-
const totalLeadAvg = computeAverageLead(totalDone);
|
| 796 |
-
const visibleLeadAvg = computeAverageLead(visibleDone);
|
| 797 |
-
const leadDisplay = filterActive
|
| 798 |
-
? formatLeadFiltered(visibleLeadAvg, totalLeadAvg)
|
| 799 |
-
: formatLeadFiltered(totalLeadAvg, null);
|
| 800 |
-
|
| 801 |
-
setMetric("total", totalDisplay);
|
| 802 |
-
setMetric("active", activeDisplay);
|
| 803 |
-
setMetric("throughput7", throughputDisplay);
|
| 804 |
-
setMetric("leadtime", leadDisplay);
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
function computeAverageLead(doneTasks) {
|
| 808 |
-
const leadTimes = doneTasks
|
| 809 |
-
.map((task) => computeLeadTimeDays(task))
|
| 810 |
-
.filter((value) => Number.isFinite(value));
|
| 811 |
-
if (!leadTimes.length) return null;
|
| 812 |
-
const total = leadTimes.reduce((sum, value) => sum + value, 0);
|
| 813 |
-
return total / leadTimes.length;
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
function formatLeadFiltered(primary, overall) {
|
| 817 |
-
const base = Number.isFinite(primary) ? `${primary.toFixed(1)}日` : "--";
|
| 818 |
-
if (overall === null || overall === undefined) {
|
| 819 |
-
return base;
|
| 820 |
-
}
|
| 821 |
-
const overallText = Number.isFinite(overall) ? `${overall.toFixed(1)}日` : "--";
|
| 822 |
-
return `${base} / 全${overallText}`;
|
| 823 |
-
}
|
| 824 |
-
|
| 825 |
-
function setMetric(key, text) {
|
| 826 |
-
const target = metricElements[key];
|
| 827 |
-
if (!target) return;
|
| 828 |
-
target.textContent = text;
|
| 829 |
-
}
|
| 830 |
-
|
| 831 |
-
function formatCount(count) {
|
| 832 |
-
return `${count}枚`;
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
-
function formatFilteredCount(visible, total) {
|
| 836 |
-
return `${formatCount(visible)} / 全${formatCount(total)}`;
|
| 837 |
-
}
|
| 838 |
-
|
| 839 |
-
function isCompletedSince(task, threshold) {
|
| 840 |
-
if (!task.completedAt) return false;
|
| 841 |
-
const completed = new Date(task.completedAt);
|
| 842 |
-
return !Number.isNaN(completed.getTime()) && completed >= threshold;
|
| 843 |
-
}
|
| 844 |
-
|
| 845 |
-
function computeLeadTimeDays(task) {
|
| 846 |
-
if (!task?.createdAt || !task?.completedAt) return Number.NaN;
|
| 847 |
-
const start = new Date(task.createdAt);
|
| 848 |
-
const end = new Date(task.completedAt);
|
| 849 |
-
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return Number.NaN;
|
| 850 |
-
const diffMs = end.getTime() - start.getTime();
|
| 851 |
-
if (diffMs < 0) return Number.NaN;
|
| 852 |
-
return diffMs / (1000 * 60 * 60 * 24);
|
| 853 |
-
}
|
| 854 |
-
|
| 855 |
-
function setDropzoneHighlight(zone, state) {
|
| 856 |
-
zone.classList.remove("ring-2", "ring-offset-2", "ring-offset-slate-900", "ring-kanban-accent", "ring-kanban-caution", "bg-slate-800/40", "bg-slate-900/40");
|
| 857 |
-
if (state === "allowed") {
|
| 858 |
-
zone.classList.add("ring-2", "ring-offset-2", "ring-offset-slate-900", "ring-kanban-accent", "bg-slate-800/40");
|
| 859 |
-
} else if (state === "denied") {
|
| 860 |
-
zone.classList.add("ring-2", "ring-offset-2", "ring-offset-slate-900", "ring-kanban-caution", "bg-slate-900/40");
|
| 861 |
-
}
|
| 862 |
-
}
|
| 863 |
-
|
| 864 |
-
function canEnterColumn(columnKey, movingTaskId) {
|
| 865 |
-
const settingsColumn = columnSettings[columnKey];
|
| 866 |
-
if (!settingsColumn) return true;
|
| 867 |
-
if (!Number.isFinite(settingsColumn.wip)) return true;
|
| 868 |
-
const current = tasks.filter((task) => task.status === columnKey && task.id !== movingTaskId).length;
|
| 869 |
-
return current < settingsColumn.wip;
|
| 870 |
-
}
|
| 871 |
-
|
| 872 |
-
function isFilterActive() {
|
| 873 |
-
return (
|
| 874 |
-
Boolean(filterState.keyword) ||
|
| 875 |
-
Boolean(filterState.dueStart) ||
|
| 876 |
-
Boolean(filterState.dueEnd) ||
|
| 877 |
-
filterState.tags.size > 0
|
| 878 |
-
);
|
| 879 |
-
}
|
| 880 |
-
|
| 881 |
-
function getFilteredTasks() {
|
| 882 |
-
if (!isFilterActive()) return tasks;
|
| 883 |
-
const keyword = filterState.keyword.toLowerCase();
|
| 884 |
-
const startDate = parseISODate(filterState.dueStart);
|
| 885 |
-
const endDate = parseISODate(filterState.dueEnd);
|
| 886 |
-
|
| 887 |
-
return tasks.filter((task) => {
|
| 888 |
-
const keywordMatch = keyword
|
| 889 |
-
? [task.title, task.owner, task.notes, task.due]
|
| 890 |
-
.filter(Boolean)
|
| 891 |
-
.some((value) => String(value).toLowerCase().includes(keyword))
|
| 892 |
-
: true;
|
| 893 |
-
|
| 894 |
-
const tagsMatch = filterState.tags.size
|
| 895 |
-
? [...filterState.tags].every((tag) => Array.isArray(task.tags) && task.tags.includes(tag))
|
| 896 |
-
: true;
|
| 897 |
-
|
| 898 |
-
const dueMatch = (() => {
|
| 899 |
-
if (!startDate && !endDate) return true;
|
| 900 |
-
if (!task.due) return false;
|
| 901 |
-
const dueDate = parseISODate(task.due);
|
| 902 |
-
if (!dueDate) return false;
|
| 903 |
-
if (startDate && dueDate < startDate) return false;
|
| 904 |
-
if (endDate && dueDate > endDate) return false;
|
| 905 |
-
return true;
|
| 906 |
-
})();
|
| 907 |
-
|
| 908 |
-
return keywordMatch && tagsMatch && dueMatch;
|
| 909 |
-
});
|
| 910 |
-
}
|
| 911 |
-
|
| 912 |
-
function renderTagFilters() {
|
| 913 |
-
if (!filterElements.tagList) return;
|
| 914 |
-
|
| 915 |
-
const keyword = filterState.keyword.trim().toLowerCase();
|
| 916 |
-
const startDate = parseISODate(filterState.dueStart);
|
| 917 |
-
const endDate = parseISODate(filterState.dueEnd);
|
| 918 |
-
|
| 919 |
-
const sourceTasks = tasks.filter((task) => {
|
| 920 |
-
const keywordMatch = keyword
|
| 921 |
-
? [task.title, task.owner, task.notes, task.due]
|
| 922 |
-
.filter(Boolean)
|
| 923 |
-
.some((value) => String(value).toLowerCase().includes(keyword))
|
| 924 |
-
: true;
|
| 925 |
-
|
| 926 |
-
const dueMatch = (() => {
|
| 927 |
-
if (!startDate && !endDate) return true;
|
| 928 |
-
if (!task.due) return false;
|
| 929 |
-
const dueDate = parseISODate(task.due);
|
| 930 |
-
if (!dueDate) return false;
|
| 931 |
-
if (startDate && dueDate < startDate) return false;
|
| 932 |
-
if (endDate && dueDate > endDate) return false;
|
| 933 |
-
return true;
|
| 934 |
-
})();
|
| 935 |
-
|
| 936 |
-
return keywordMatch && dueMatch;
|
| 937 |
-
});
|
| 938 |
-
|
| 939 |
-
const allTags = Array.from(
|
| 940 |
-
new Set(
|
| 941 |
-
sourceTasks
|
| 942 |
-
.flatMap((task) => (Array.isArray(task.tags) ? task.tags : []))
|
| 943 |
-
.map((tag) => tag.trim())
|
| 944 |
-
.filter(Boolean)
|
| 945 |
-
)
|
| 946 |
-
).sort((a, b) => a.localeCompare(b, 'ja'));
|
| 947 |
-
|
| 948 |
-
const validTags = new Set(allTags);
|
| 949 |
-
let removed = false;
|
| 950 |
-
[...filterState.tags].forEach((tag) => {
|
| 951 |
-
if (!validTags.has(tag)) {
|
| 952 |
-
filterState.tags.delete(tag);
|
| 953 |
-
removed = true;
|
| 954 |
-
}
|
| 955 |
-
});
|
| 956 |
-
if (removed && filterElements.keyword && !isFilterActive()) {
|
| 957 |
-
filterElements.keyword.value = filterState.keyword;
|
| 958 |
-
}
|
| 959 |
-
|
| 960 |
-
filterElements.tagList.replaceChildren();
|
| 961 |
-
|
| 962 |
-
if (!allTags.length) {
|
| 963 |
-
const empty = document.createElement('p');
|
| 964 |
-
empty.className = 'text-xs text-slate-500';
|
| 965 |
-
empty.textContent = 'タグはまだ登録されていません。';
|
| 966 |
-
filterElements.tagList.appendChild(empty);
|
| 967 |
-
return;
|
| 968 |
-
}
|
| 969 |
-
|
| 970 |
-
allTags.forEach((tag) => {
|
| 971 |
-
const button = document.createElement('button');
|
| 972 |
-
button.type = 'button';
|
| 973 |
-
button.dataset.tagValue = tag;
|
| 974 |
-
button.textContent = `#${tag}`;
|
| 975 |
-
button.className = buildTagFilterClass(filterState.tags.has(tag));
|
| 976 |
-
filterElements.tagList.appendChild(button);
|
| 977 |
-
});
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
function buildTagFilterClass(isActive) {
|
| 981 |
-
const base = "rounded-full border px-3 py-1 text-xs font-medium transition focus:outline-none focus-visible:ring focus-visible:ring-kanban-accent focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900";
|
| 982 |
-
return isActive
|
| 983 |
-
? `${base} border-kanban-accent bg-kanban-accent/20 text-kanban-accent`
|
| 984 |
-
: `${base} border-slate-600/70 bg-slate-800/80 text-slate-300 hover:border-slate-400 hover:text-white`;
|
| 985 |
-
}
|
| 986 |
-
|
| 987 |
-
function buildTagPillClass(isActive) {
|
| 988 |
-
const base = "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium";
|
| 989 |
-
return isActive
|
| 990 |
-
? `${base} border-kanban-accent bg-kanban-accent/20 text-kanban-accent`
|
| 991 |
-
: `${base} border-slate-600/70 bg-slate-800/80 text-slate-300`;
|
| 992 |
-
}
|
| 993 |
-
|
| 994 |
-
function cleanupSelection() {
|
| 995 |
-
const validIds = new Set(tasks.map((task) => task.id));
|
| 996 |
-
let changed = false;
|
| 997 |
-
selectionState.ids.forEach((id) => {
|
| 998 |
-
if (!validIds.has(id)) {
|
| 999 |
-
selectionState.ids.delete(id);
|
| 1000 |
-
changed = true;
|
| 1001 |
-
}
|
| 1002 |
-
});
|
| 1003 |
-
if (changed) {
|
| 1004 |
-
updateSelectionUI();
|
| 1005 |
-
syncSelectionCheckboxes();
|
| 1006 |
-
}
|
| 1007 |
-
}
|
| 1008 |
-
|
| 1009 |
-
function toggleSelection(taskId, isSelected) {
|
| 1010 |
-
if (isSelected) {
|
| 1011 |
-
selectionState.ids.add(taskId);
|
| 1012 |
-
} else {
|
| 1013 |
-
selectionState.ids.delete(taskId);
|
| 1014 |
-
}
|
| 1015 |
-
updateSelectionUI();
|
| 1016 |
-
syncSelectionCheckboxes();
|
| 1017 |
-
}
|
| 1018 |
-
|
| 1019 |
-
function selectAllVisible() {
|
| 1020 |
-
if (!selectionState.lastVisibleIds.length) return;
|
| 1021 |
-
selectionState.lastVisibleIds.forEach((id) => selectionState.ids.add(id));
|
| 1022 |
-
updateSelectionUI();
|
| 1023 |
-
syncSelectionCheckboxes();
|
| 1024 |
-
}
|
| 1025 |
-
|
| 1026 |
-
function clearSelection() {
|
| 1027 |
-
selectionState.ids.clear();
|
| 1028 |
-
updateSelectionUI();
|
| 1029 |
-
syncSelectionCheckboxes();
|
| 1030 |
-
}
|
| 1031 |
-
|
| 1032 |
-
function syncSelectionCheckboxes() {
|
| 1033 |
-
const checkboxes = document.querySelectorAll("[data-action=\"select-task\"]");
|
| 1034 |
-
checkboxes.forEach((checkbox) => {
|
| 1035 |
-
const card = checkbox.closest("[data-task-id]");
|
| 1036 |
-
if (!card) return;
|
| 1037 |
-
const isSelected = selectionState.ids.has(card.dataset.taskId);
|
| 1038 |
-
checkbox.checked = isSelected;
|
| 1039 |
-
card.classList.toggle("kanban-card-selected", isSelected);
|
| 1040 |
-
});
|
| 1041 |
-
}
|
| 1042 |
-
|
| 1043 |
-
function updateSelectionUI() {
|
| 1044 |
-
const count = selectionState.ids.size;
|
| 1045 |
-
if (selectionElements.counter) {
|
| 1046 |
-
selectionElements.counter.textContent = `選択中: ${count}枚`;
|
| 1047 |
-
}
|
| 1048 |
-
if (selectionElements.delete) {
|
| 1049 |
-
selectionElements.delete.disabled = count === 0;
|
| 1050 |
-
selectionElements.delete.setAttribute("aria-disabled", count === 0 ? "true" : "false");
|
| 1051 |
-
}
|
| 1052 |
-
if (selectionElements.clear) {
|
| 1053 |
-
selectionElements.clear.disabled = count === 0;
|
| 1054 |
-
}
|
| 1055 |
-
if (selectionElements.selectAll) {
|
| 1056 |
-
selectionElements.selectAll.disabled = selectionState.lastVisibleIds.length === 0;
|
| 1057 |
-
}
|
| 1058 |
-
}
|
| 1059 |
-
|
| 1060 |
-
function deleteTask(taskId) {
|
| 1061 |
-
const task = tasks.find((item) => item.id === taskId);
|
| 1062 |
-
if (!task) return;
|
| 1063 |
-
if (!window.confirm("このタスクを削除しますか?")) return;
|
| 1064 |
-
selectionState.ids.delete(taskId);
|
| 1065 |
-
tasks = tasks.filter((item) => item.id !== taskId);
|
| 1066 |
-
persist();
|
| 1067 |
-
render();
|
| 1068 |
-
showMessage("タスクを削除しました。", "info");
|
| 1069 |
-
}
|
| 1070 |
-
|
| 1071 |
-
function deleteSelectedTasks() {
|
| 1072 |
-
const ids = [...selectionState.ids];
|
| 1073 |
-
if (!ids.length) return;
|
| 1074 |
-
if (!window.confirm(`選択中の${ids.length}件を削除しますか?`)) return;
|
| 1075 |
-
const idSet = new Set(ids);
|
| 1076 |
-
tasks = tasks.filter((task) => !idSet.has(task.id));
|
| 1077 |
-
selectionState.ids.clear();
|
| 1078 |
-
persist();
|
| 1079 |
-
render();
|
| 1080 |
-
showMessage("選択したタスクを削除しました。", "info");
|
| 1081 |
-
}
|
| 1082 |
-
function loadFromStorage() {
|
| 1083 |
-
try {
|
| 1084 |
-
const raw = window.localStorage.getItem(STORAGE_KEY);
|
| 1085 |
-
if (!raw) return;
|
| 1086 |
-
const parsed = JSON.parse(raw);
|
| 1087 |
-
if (!Array.isArray(parsed)) return;
|
| 1088 |
-
|
| 1089 |
-
tasks = parsed
|
| 1090 |
-
.filter((item) => typeof item === "object" && item !== null && item.id && item.title)
|
| 1091 |
-
.map((item) => normalizeTask(item));
|
| 1092 |
-
} catch (error) {
|
| 1093 |
-
console.warn("Failed to load tasks from localStorage:", error);
|
| 1094 |
-
tasks = [];
|
| 1095 |
-
}
|
| 1096 |
-
}
|
| 1097 |
-
|
| 1098 |
-
function normalizeTask(item) {
|
| 1099 |
-
const status = columnSettings[item.status] ? item.status : "backlog";
|
| 1100 |
-
const createdAt = item.createdAt ?? new Date().toISOString();
|
| 1101 |
-
const normalized = {
|
| 1102 |
-
id: item.id,
|
| 1103 |
-
title: item.title,
|
| 1104 |
-
owner: item.owner ?? "",
|
| 1105 |
-
notes: item.notes ?? "",
|
| 1106 |
-
status,
|
| 1107 |
-
due: item.due ?? "",
|
| 1108 |
-
createdAt,
|
| 1109 |
-
tags: normalizeTags(item.tags),
|
| 1110 |
-
link: normalizeLink(item.link ?? item.url ?? "")
|
| 1111 |
-
};
|
| 1112 |
-
|
| 1113 |
-
if (status === "done") {
|
| 1114 |
-
normalized.completedAt = item.completedAt ?? item.updatedAt ?? createdAt;
|
| 1115 |
-
} else if (item.completedAt) {
|
| 1116 |
-
normalized.completedAt = item.completedAt;
|
| 1117 |
-
}
|
| 1118 |
-
|
| 1119 |
-
return normalized;
|
| 1120 |
-
}
|
| 1121 |
-
|
| 1122 |
-
function normalizeTags(value) {
|
| 1123 |
-
if (Array.isArray(value)) {
|
| 1124 |
-
return parseTags(value.join(","));
|
| 1125 |
-
}
|
| 1126 |
-
if (typeof value === "string") {
|
| 1127 |
-
return parseTags(value);
|
| 1128 |
-
}
|
| 1129 |
-
return [];
|
| 1130 |
-
}
|
| 1131 |
-
|
| 1132 |
-
function persist() {
|
| 1133 |
-
try {
|
| 1134 |
-
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
|
| 1135 |
-
} catch (error) {
|
| 1136 |
-
console.warn("Failed to write tasks to localStorage:", error);
|
| 1137 |
-
showMessage("ローカルストレージに保存できませんでした。", "warn");
|
| 1138 |
-
}
|
| 1139 |
-
}
|
| 1140 |
-
|
| 1141 |
-
function showMessage(message, tone = "info") {
|
| 1142 |
-
if (!messageArea) return;
|
| 1143 |
-
clearTimeout(messageTimer);
|
| 1144 |
-
messageArea.textContent = message;
|
| 1145 |
-
messageArea.classList.remove("opacity-0", "text-kanban-caution", "text-sky-300");
|
| 1146 |
-
messageArea.classList.add(tone === "warn" ? "text-kanban-caution" : "text-sky-300", "opacity-100");
|
| 1147 |
-
messageTimer = window.setTimeout(() => hideMessage(), 4200);
|
| 1148 |
-
}
|
| 1149 |
-
|
| 1150 |
-
function hideMessage() {
|
| 1151 |
-
if (!messageArea) return;
|
| 1152 |
-
messageArea.classList.add("opacity-0");
|
| 1153 |
-
}
|
| 1154 |
-
|
| 1155 |
-
function seedDemoData() {
|
| 1156 |
-
const now = new Date();
|
| 1157 |
-
const demoCards = [
|
| 1158 |
-
{
|
| 1159 |
-
title: "前日の不具合整理",
|
| 1160 |
-
owner: "田中",
|
| 1161 |
-
notes: "Eラインの停止要因を5Whyで再整理",
|
| 1162 |
-
status: "backlog",
|
| 1163 |
-
due: isoDateOffset(now, 1),
|
| 1164 |
-
tags: ["品質", "改善"],
|
| 1165 |
-
link: "https://example.com/quality-review"
|
| 1166 |
-
},
|
| 1167 |
-
{
|
| 1168 |
-
title: "朝会ファシリテーション",
|
| 1169 |
-
owner: "佐藤",
|
| 1170 |
-
notes: "指標ボード更新と本日の重点テーマ共有",
|
| 1171 |
-
status: "ready",
|
| 1172 |
-
due: isoDateOffset(now, 0),
|
| 1173 |
-
tags: ["コミュニケーション"],
|
| 1174 |
-
link: "https://example.com/daily-meeting"
|
| 1175 |
-
},
|
| 1176 |
-
{
|
| 1177 |
-
title: "標準作業票の更新",
|
| 1178 |
-
owner: "鈴木",
|
| 1179 |
-
notes: "工程C-12の新治具対応版。現場レビュー待ち。",
|
| 1180 |
-
status: "waiting",
|
| 1181 |
-
due: isoDateOffset(now, 3),
|
| 1182 |
-
tags: ["標準化", "改善"],
|
| 1183 |
-
link: "https://example.com/standard-work"
|
| 1184 |
-
},
|
| 1185 |
-
{
|
| 1186 |
-
title: "トレーニング計画ドラフト",
|
| 1187 |
-
owner: "山本",
|
| 1188 |
-
notes: "新入社員向け。ヒアリング結果を反映中。",
|
| 1189 |
-
status: "inProgress",
|
| 1190 |
-
due: isoDateOffset(now, 5),
|
| 1191 |
-
tags: ["人材育成"],
|
| 1192 |
-
link: "https://example.com/training-plan"
|
| 1193 |
-
},
|
| 1194 |
-
{
|
| 1195 |
-
title: "品質ロス分析",
|
| 1196 |
-
owner: "石井",
|
| 1197 |
-
notes: "先週分のデータを取り込み済み。残りは原因記入。",
|
| 1198 |
-
status: "ready",
|
| 1199 |
-
due: isoDateOffset(now, 2),
|
| 1200 |
-
tags: ["品質", "分析"],
|
| 1201 |
-
link: "https://example.com/quality-loss"
|
| 1202 |
-
},
|
| 1203 |
-
{
|
| 1204 |
-
title: "安全KY実施",
|
| 1205 |
-
owner: "加藤",
|
| 1206 |
-
notes: "夜勤チームへ展開済み。記録は箱に保管。",
|
| 1207 |
-
status: "done",
|
| 1208 |
-
due: isoDateOffset(now, -1),
|
| 1209 |
-
tags: ["安全", "教育"],
|
| 1210 |
-
link: "https://example.com/safety-ky"
|
| 1211 |
-
}
|
| 1212 |
-
];
|
| 1213 |
-
|
| 1214 |
-
demoCards.forEach((card, index) => {
|
| 1215 |
-
const desiredStatus = card.status;
|
| 1216 |
-
const status = canEnterColumn(desiredStatus) ? desiredStatus : "backlog";
|
| 1217 |
-
const createdAt = new Date(now.getTime() + index * 1000);
|
| 1218 |
-
const entry = {
|
| 1219 |
-
id: createId(),
|
| 1220 |
-
title: card.title,
|
| 1221 |
-
owner: card.owner,
|
| 1222 |
-
notes: card.notes,
|
| 1223 |
-
status,
|
| 1224 |
-
due: card.due,
|
| 1225 |
-
createdAt: createdAt.toISOString(),
|
| 1226 |
-
tags: parseTags(card.tags?.join(",") ?? ""),
|
| 1227 |
-
link: normalizeLink(card.link ?? "")
|
| 1228 |
-
};
|
| 1229 |
-
|
| 1230 |
-
if (status === "done") {
|
| 1231 |
-
entry.completedAt = new Date(createdAt.getTime() + 60 * 60 * 1000).toISOString();
|
| 1232 |
-
}
|
| 1233 |
-
|
| 1234 |
-
tasks.push(entry);
|
| 1235 |
-
});
|
| 1236 |
-
|
| 1237 |
-
persist();
|
| 1238 |
-
render();
|
| 1239 |
-
showMessage("デモデータを追加しました。", "info");
|
| 1240 |
-
}
|
| 1241 |
-
|
| 1242 |
-
function isoDateOffset(date, offsetDays) {
|
| 1243 |
-
const clone = new Date(date);
|
| 1244 |
-
clone.setDate(clone.getDate() + offsetDays);
|
| 1245 |
-
return clone.toISOString().slice(0, 10);
|
| 1246 |
-
}
|
| 1247 |
-
|
| 1248 |
-
function formatDue(value) {
|
| 1249 |
-
if (!value) return "未設定";
|
| 1250 |
-
try {
|
| 1251 |
-
const date = new Date(value);
|
| 1252 |
-
if (Number.isNaN(date.getTime())) return "未設定";
|
| 1253 |
-
return `${date.getFullYear()}年${padZero(date.getMonth() + 1)}月${padZero(date.getDate())}日`;
|
| 1254 |
-
} catch (_error) {
|
| 1255 |
-
return "未設定";
|
| 1256 |
-
}
|
| 1257 |
-
}
|
| 1258 |
-
|
| 1259 |
-
function padZero(num) {
|
| 1260 |
-
return String(num).padStart(2, "0");
|
| 1261 |
-
}
|
| 1262 |
-
|
| 1263 |
-
function createId() {
|
| 1264 |
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
| 1265 |
-
return crypto.randomUUID();
|
| 1266 |
-
}
|
| 1267 |
-
return `task-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
| 1268 |
-
}
|
| 1269 |
-
|
| 1270 |
-
function openEditDialog(taskId) {
|
| 1271 |
-
const task = tasks.find((item) => item.id === taskId);
|
| 1272 |
-
if (!task) return;
|
| 1273 |
-
|
| 1274 |
-
if (!editDialog || !editForm || !editFields) {
|
| 1275 |
-
fallbackEdit(task);
|
| 1276 |
-
return;
|
| 1277 |
-
}
|
| 1278 |
-
|
| 1279 |
-
editingTaskId = taskId;
|
| 1280 |
-
editFields.title.value = task.title;
|
| 1281 |
-
editFields.owner.value = task.owner ?? "";
|
| 1282 |
-
editFields.due.value = task.due ?? "";
|
| 1283 |
-
editFields.notes.value = task.notes ?? "";
|
| 1284 |
-
if (editFields.tags) {
|
| 1285 |
-
editFields.tags.value = Array.isArray(task.tags) ? task.tags.join(", ") : "";
|
| 1286 |
-
}
|
| 1287 |
-
editFields.link.value = task.link ?? "";
|
| 1288 |
-
|
| 1289 |
-
try {
|
| 1290 |
-
if (typeof editDialog.showModal === "function") {
|
| 1291 |
-
editDialog.showModal();
|
| 1292 |
-
} else {
|
| 1293 |
-
editDialog.setAttribute("open", "true");
|
| 1294 |
-
}
|
| 1295 |
-
window.setTimeout(() => editFields.title.focus(), 0);
|
| 1296 |
-
} catch (error) {
|
| 1297 |
-
console.warn("Failed to open dialog, using fallback.", error);
|
| 1298 |
-
fallbackEdit(task);
|
| 1299 |
-
}
|
| 1300 |
-
}
|
| 1301 |
-
|
| 1302 |
-
function handleEditSubmit(event) {
|
| 1303 |
-
event.preventDefault();
|
| 1304 |
-
if (!editFields || !editingTaskId) return;
|
| 1305 |
-
|
| 1306 |
-
const title = editFields.title.value.trim();
|
| 1307 |
-
if (!title) {
|
| 1308 |
-
showMessage("タイトルを入力してください。", "warn");
|
| 1309 |
-
editFields.title.focus();
|
| 1310 |
-
return;
|
| 1311 |
-
}
|
| 1312 |
-
|
| 1313 |
-
const updates = {
|
| 1314 |
-
title,
|
| 1315 |
-
owner: editFields.owner.value.trim(),
|
| 1316 |
-
due: editFields.due.value,
|
| 1317 |
-
notes: editFields.notes.value.trim(),
|
| 1318 |
-
tags: parseTags(editFields.tags ? editFields.tags.value : ""),
|
| 1319 |
-
link: normalizeLink(editFields.link?.value ?? "")
|
| 1320 |
-
};
|
| 1321 |
-
|
| 1322 |
-
applyTaskUpdates(editingTaskId, updates);
|
| 1323 |
-
closeEditDialog();
|
| 1324 |
-
}
|
| 1325 |
-
|
| 1326 |
-
function applyTaskUpdates(taskId, updates) {
|
| 1327 |
-
const task = tasks.find((item) => item.id === taskId);
|
| 1328 |
-
if (!task) return false;
|
| 1329 |
-
|
| 1330 |
-
task.title = updates.title;
|
| 1331 |
-
task.owner = updates.owner;
|
| 1332 |
-
task.due = updates.due;
|
| 1333 |
-
task.notes = updates.notes;
|
| 1334 |
-
task.tags = updates.tags;
|
| 1335 |
-
task.link = updates.link;
|
| 1336 |
-
|
| 1337 |
-
persist();
|
| 1338 |
-
render();
|
| 1339 |
-
showMessage("タスクを更新しました。", "info");
|
| 1340 |
-
return true;
|
| 1341 |
-
}
|
| 1342 |
-
|
| 1343 |
-
function closeEditDialog(fromEvent = false) {
|
| 1344 |
-
if (!editDialog) return;
|
| 1345 |
-
|
| 1346 |
-
if (!fromEvent) {
|
| 1347 |
-
if (typeof editDialog.close === "function" && editDialog.open) {
|
| 1348 |
-
editDialog.close();
|
| 1349 |
-
} else {
|
| 1350 |
-
editDialog.removeAttribute("open");
|
| 1351 |
-
}
|
| 1352 |
-
} else {
|
| 1353 |
-
editDialog.removeAttribute("open");
|
| 1354 |
-
}
|
| 1355 |
-
|
| 1356 |
-
resetEditingState();
|
| 1357 |
-
}
|
| 1358 |
-
|
| 1359 |
-
function resetEditingState() {
|
| 1360 |
-
editingTaskId = null;
|
| 1361 |
-
editForm?.reset();
|
| 1362 |
-
}
|
| 1363 |
-
|
| 1364 |
-
function fallbackEdit(task) {
|
| 1365 |
-
const title = window.prompt("タイトルを編集", task.title);
|
| 1366 |
-
if (title === null) return;
|
| 1367 |
-
const trimmedTitle = title.trim();
|
| 1368 |
-
if (!trimmedTitle) {
|
| 1369 |
-
showMessage("タイトルを入力してください。", "warn");
|
| 1370 |
-
return;
|
| 1371 |
-
}
|
| 1372 |
-
|
| 1373 |
-
const owner = window.prompt("担当を編集", task.owner ?? "") ?? task.owner ?? "";
|
| 1374 |
-
const due = window.prompt("期限 (YYYY-MM-DD)", task.due ?? "") ?? task.due ?? "";
|
| 1375 |
-
const notes = window.prompt("メモ", task.notes ?? "") ?? task.notes ?? "";
|
| 1376 |
-
const tagsInput = window.prompt("タグ(カンマ区切り)", Array.isArray(task.tags) ? task.tags.join(", ") : "") ?? (Array.isArray(task.tags) ? task.tags.join(", ") : "");
|
| 1377 |
-
const linkInput = window.prompt("参考リンク(URL)", task.link ?? "") ?? (task.link ?? "");
|
| 1378 |
-
|
| 1379 |
-
applyTaskUpdates(task.id, {
|
| 1380 |
-
title: trimmedTitle,
|
| 1381 |
-
owner: owner.trim(),
|
| 1382 |
-
due,
|
| 1383 |
-
notes: notes.trim(),
|
| 1384 |
-
tags: parseTags(tagsInput),
|
| 1385 |
-
link: normalizeLink(linkInput)
|
| 1386 |
-
});
|
| 1387 |
-
}
|
| 1388 |
-
|
| 1389 |
-
function toggleViewMode() {
|
| 1390 |
-
uiState.compact = !uiState.compact;
|
| 1391 |
-
updateViewToggleLabel();
|
| 1392 |
-
render();
|
| 1393 |
-
showMessage(uiState.compact ? "コンパクト表示に切り替えました。" : "標準表示に戻しました。", "info");
|
| 1394 |
-
}
|
| 1395 |
-
|
| 1396 |
-
function updateViewToggleLabel() {
|
| 1397 |
-
if (!uiElements.viewToggle) return;
|
| 1398 |
-
uiElements.viewToggle.textContent = uiState.compact ? "標準表示" : "コンパクト表示";
|
| 1399 |
-
}
|
| 1400 |
-
|
| 1401 |
-
function parseISODate(value) {
|
| 1402 |
-
if (!value) return null;
|
| 1403 |
-
try {
|
| 1404 |
-
const date = new Date(value);
|
| 1405 |
-
return Number.isNaN(date.getTime()) ? null : new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
| 1406 |
-
} catch (_error) {
|
| 1407 |
-
return null;
|
| 1408 |
-
}
|
| 1409 |
-
}
|
| 1410 |
-
|
| 1411 |
-
function formatLinkLabel(url) {
|
| 1412 |
-
try {
|
| 1413 |
-
const parsed = new URL(url);
|
| 1414 |
-
const path = parsed.pathname === "/" ? "" : parsed.pathname;
|
| 1415 |
-
return `${parsed.hostname}${path}`;
|
| 1416 |
-
} catch (_error) {
|
| 1417 |
-
return url;
|
| 1418 |
-
}
|
| 1419 |
-
}
|
| 1420 |
-
|
| 1421 |
-
function normalizeLink(input) {
|
| 1422 |
-
if (!input) return "";
|
| 1423 |
-
let raw0 = String(input).trim();
|
| 1424 |
-
if (!raw0) return "";
|
| 1425 |
-
|
| 1426 |
-
// ✅ 追加: 先頭・末尾のダブルクォーテーションを削除
|
| 1427 |
-
raw0 = raw0.replace(/^"(.*)"$/, "$1");
|
| 1428 |
-
|
| 1429 |
-
// 全角の「¥」をバックスラッシュに誤入力しているケースを補正
|
| 1430 |
-
const raw = raw0.replace(/¥/g, "\\");
|
| 1431 |
-
|
| 1432 |
-
const ensureDirSlash = (u) => {
|
| 1433 |
-
try {
|
| 1434 |
-
const url = new URL(u);
|
| 1435 |
-
if (url.protocol !== "file:") return u;
|
| 1436 |
-
if (url.pathname.endsWith("/")) return u;
|
| 1437 |
-
|
| 1438 |
-
const last = decodeURI(url.pathname).split("/").pop() || "";
|
| 1439 |
-
const looksLikeFile = /\.[A-Za-z0-9]{1,6}$/.test(last);
|
| 1440 |
-
return looksLikeFile ? u : (u + "/");
|
| 1441 |
-
} catch {
|
| 1442 |
-
return u;
|
| 1443 |
-
}
|
| 1444 |
-
};
|
| 1445 |
-
|
| 1446 |
-
// 1) Windows ドライブパス: C:\... → file:///C:/...
|
| 1447 |
-
if (/^[a-zA-Z]:\\/.test(raw)) {
|
| 1448 |
-
const path = raw.replace(/\\/g, "/");
|
| 1449 |
-
return ensureDirSlash(`file:///${encodeURI(path)}`);
|
| 1450 |
-
}
|
| 1451 |
-
|
| 1452 |
-
// 2) UNC: \\server\share\path → file://server/share/path
|
| 1453 |
-
if (/^\\\\[^\\]/.test(raw)) {
|
| 1454 |
-
const path = raw.replace(/^\\\\/, "").replace(/\\/g, "/");
|
| 1455 |
-
return ensureDirSlash(`file://${encodeURI(path)}`);
|
| 1456 |
-
}
|
| 1457 |
-
|
| 1458 |
-
// 3) 明示スキームあり(http/https/file)
|
| 1459 |
-
const hasScheme = /^[a-zA-Z][\\w+.-]*:/.test(raw);
|
| 1460 |
-
if (hasScheme) {
|
| 1461 |
-
try {
|
| 1462 |
-
const url = new URL(raw);
|
| 1463 |
-
if (/^https?:$/i.test(url.protocol)) return url.href;
|
| 1464 |
-
if (/^file:$/i.test(url.protocol)) return ensureDirSlash(url.href);
|
| 1465 |
-
return "";
|
| 1466 |
-
} catch {
|
| 1467 |
-
return "";
|
| 1468 |
-
}
|
| 1469 |
-
}
|
| 1470 |
-
|
| 1471 |
-
// 4) スキーム無し → Webドメインらしければ https:// を補完
|
| 1472 |
-
try {
|
| 1473 |
-
const candidate = `https://${raw}`;
|
| 1474 |
-
const url = new URL(candidate);
|
| 1475 |
-
return url.href;
|
| 1476 |
-
} catch {
|
| 1477 |
-
return "";
|
| 1478 |
-
}
|
| 1479 |
-
}
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
function parseTags(input) {
|
| 1483 |
-
if (!input) return [];
|
| 1484 |
-
const parts = Array.isArray(input) ? input : String(input).split(/[,、\s]+/);
|
| 1485 |
-
const seen = new Set();
|
| 1486 |
-
const result = [];
|
| 1487 |
-
parts
|
| 1488 |
-
.map((item) => String(item).trim())
|
| 1489 |
-
.filter(Boolean)
|
| 1490 |
-
.forEach((tag) => {
|
| 1491 |
-
const cleaned = tag.replace(/^#/, "").trim();
|
| 1492 |
-
if (!cleaned) return;
|
| 1493 |
-
const key = cleaned.toLowerCase();
|
| 1494 |
-
if (seen.has(key)) return;
|
| 1495 |
-
seen.add(key);
|
| 1496 |
-
result.push(cleaned);
|
| 1497 |
-
});
|
| 1498 |
-
return result;
|
| 1499 |
-
}
|
| 1500 |
-
})();
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
|
| 1505 |
-
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
| 1535 |
-
|
| 1536 |
-
|
| 1537 |
-
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
|
| 1543 |
-
|
| 1544 |
-
|
| 1545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
v6_軽量KanbanBoard/index.html
DELETED
|
@@ -1,509 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="ja">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>トヨタ式カンバンボード</title>
|
| 7 |
-
<!-- ✅ ローカルのTailwind CDNを先に読み込む -->
|
| 8 |
-
<script src="./tailwindcdn.js"></script>
|
| 9 |
-
|
| 10 |
-
<style>
|
| 11 |
-
:root {
|
| 12 |
-
color-scheme: dark;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
.kanban-column-body {
|
| 16 |
-
max-height: calc(100vh - 320px);
|
| 17 |
-
overflow-y: auto;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
@media (min-width: 768px) {
|
| 21 |
-
.kanban-column-body {
|
| 22 |
-
max-height: calc(100vh - 280px);
|
| 23 |
-
}
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
@media (min-width: 1280px) {
|
| 27 |
-
.kanban-column-body {
|
| 28 |
-
max-height: calc(100vh - 260px);
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
.kanban-scrollbar {
|
| 33 |
-
scrollbar-width: thin;
|
| 34 |
-
scrollbar-color: #334155 transparent;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
.kanban-scrollbar::-webkit-scrollbar {
|
| 38 |
-
width: 8px;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.kanban-scrollbar::-webkit-scrollbar-track {
|
| 42 |
-
background: transparent;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
.kanban-scrollbar::-webkit-scrollbar-thumb {
|
| 46 |
-
background-color: #334155;
|
| 47 |
-
border-radius: 9999px;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.kanban-note-clamped {
|
| 51 |
-
display: -webkit-box;
|
| 52 |
-
-webkit-line-clamp: 3;
|
| 53 |
-
-webkit-box-orient: vertical;
|
| 54 |
-
overflow: hidden;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
[data-panel-body][hidden] {
|
| 58 |
-
display: none;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
[data-panel].panel-collapsed {
|
| 62 |
-
border-color: rgba(148, 163, 184, 0.25);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.panel-toggle {
|
| 66 |
-
display: inline-flex;
|
| 67 |
-
align-items: center;
|
| 68 |
-
gap: 0.5rem;
|
| 69 |
-
border-radius: 0.5rem;
|
| 70 |
-
border: 1px solid rgba(100, 116, 139, 0.6);
|
| 71 |
-
padding: 0.35rem 0.75rem;
|
| 72 |
-
font-size: 0.75rem;
|
| 73 |
-
font-weight: 600;
|
| 74 |
-
color: rgba(226, 232, 240, 0.9);
|
| 75 |
-
background-color: transparent;
|
| 76 |
-
transition: border-color 0.2s ease, color 0.2s ease;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
.panel-toggle:hover {
|
| 80 |
-
border-color: rgba(226, 232, 240, 0.85);
|
| 81 |
-
color: #f8fafc;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
.panel-toggle:focus-visible {
|
| 85 |
-
outline: 2px solid rgba(56, 189, 248, 0.6);
|
| 86 |
-
outline-offset: 2px;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
.kanban-card {
|
| 90 |
-
display: flex;
|
| 91 |
-
flex-direction: column;
|
| 92 |
-
gap: 0.75rem;
|
| 93 |
-
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
.kanban-card:hover {
|
| 97 |
-
transform: translateY(-2px);
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
.kanban-card-actions {
|
| 101 |
-
transition: opacity 0.2s ease;
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
.kanban-card-actions:focus-within {
|
| 105 |
-
opacity: 1;
|
| 106 |
-
pointer-events: auto;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
.kanban-card-title {
|
| 110 |
-
display: block;
|
| 111 |
-
overflow-wrap: anywhere;
|
| 112 |
-
word-break: break-word;
|
| 113 |
-
line-height: 1.4;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.kanban-card-compact {
|
| 117 |
-
padding-top: 1rem;
|
| 118 |
-
padding-bottom: 1rem;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
.kanban-card-compact .kanban-card-title {
|
| 122 |
-
font-size: 0.95rem;
|
| 123 |
-
display: -webkit-box;
|
| 124 |
-
-webkit-line-clamp: 2;
|
| 125 |
-
-webkit-box-orient: vertical;
|
| 126 |
-
overflow: hidden;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
.kanban-card-compact .kanban-card-actions {
|
| 130 |
-
opacity: 0;
|
| 131 |
-
pointer-events: none;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
.kanban-card-notes {
|
| 135 |
-
overflow-wrap: anywhere;
|
| 136 |
-
line-height: 1.5;
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
.kanban-card-compact.kanban-card-expanded .kanban-card-actions,
|
| 140 |
-
.kanban-card:hover .kanban-card-actions {
|
| 141 |
-
opacity: 1;
|
| 142 |
-
pointer-events: auto;
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
.kanban-card-selected {
|
| 146 |
-
border-color: rgba(56, 189, 248, 0.7) !important;
|
| 147 |
-
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.35);
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
.kanban-card-main {
|
| 151 |
-
flex: 1 1 260px;
|
| 152 |
-
min-width: 220px;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.kanban-card-aside {
|
| 156 |
-
flex: 0 0 180px;
|
| 157 |
-
min-width: 160px;
|
| 158 |
-
align-items: flex-end;
|
| 159 |
-
text-align: right;
|
| 160 |
-
gap: 0.5rem;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
@media (max-width: 768px) {
|
| 164 |
-
.kanban-card-aside {
|
| 165 |
-
flex: 1 1 200px;
|
| 166 |
-
align-items: flex-start;
|
| 167 |
-
text-align: left;
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
.kanban-card-meta {
|
| 172 |
-
display: flex;
|
| 173 |
-
flex-wrap: wrap;
|
| 174 |
-
gap: 0.75rem;
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
.kanban-card-meta-item {
|
| 178 |
-
flex: 1 1 200px;
|
| 179 |
-
min-width: 180px;
|
| 180 |
-
}
|
| 181 |
-
</style>
|
| 182 |
-
</head>
|
| 183 |
-
<body class="min-h-screen bg-slate-950 text-slate-100">
|
| 184 |
-
<div class="mx-auto flex max-w-7xl flex-col gap-10 px-6 py-10">
|
| 185 |
-
<header class="space-y-4">
|
| 186 |
-
<div class="flex flex-wrap items-center justify-between gap-3">
|
| 187 |
-
<span class="inline-flex items-center gap-2 rounded-full bg-slate-800/70 px-4 py-1 text-xs uppercase tracking-wide text-slate-300">Made by T</span>
|
| 188 |
-
<button id="title-edit" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-400 hover:text-white">タイトル編集</button>
|
| 189 |
-
</div>
|
| 190 |
-
<h1 id="board-title-heading" class="text-3xl font-bold text-white md:text-4xl">トヨタ式カンバンボード</h1>
|
| 191 |
-
<p class="max-w-2xl text-sm text-slate-300 md:text-base">
|
| 192 |
-
現場の「見���る化」を支援する軽量なカンバンボード。<br>WIP制限を守りつつ、タスクの流れとボトルネックを素早く把握!
|
| 193 |
-
</p>
|
| 194 |
-
</header>
|
| 195 |
-
|
| 196 |
-
<section aria-labelledby="analytics-title" class="rounded-2xl bg-slate-800/70 p-6 shadow-lg shadow-black/20" data-panel="analytics">
|
| 197 |
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
| 198 |
-
<div>
|
| 199 |
-
<h2 id="analytics-title" class="text-lg font-semibold text-white">改善指標</h2>
|
| 200 |
-
<p class="text-sm text-slate-400">カードの滞留や流量を把握し、改善サイクルを素早く回すためのヒントとなる指標!</p>
|
| 201 |
-
</div>
|
| 202 |
-
<button type="button" class="panel-toggle" data-action="toggle-panel" data-target="analytics" aria-expanded="true">折りたたむ</button>
|
| 203 |
-
</div>
|
| 204 |
-
<div class="mt-6" data-panel-body="analytics">
|
| 205 |
-
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
| 206 |
-
<article class="rounded-xl border border-slate-700/60 bg-slate-900/40 p-4 shadow-inner shadow-black/10">
|
| 207 |
-
<span class="text-xs uppercase tracking-wide text-slate-400">総カード数</span>
|
| 208 |
-
<p class="mt-2 text-3xl font-semibold text-white" data-metric="total">0枚</p>
|
| 209 |
-
<p class="mt-2 text-xs text-slate-400">登録済みカードの全体数。</p>
|
| 210 |
-
</article>
|
| 211 |
-
<article class="rounded-xl border border-slate-700/60 bg-slate-900/40 p-4 shadow-inner shadow-black/10">
|
| 212 |
-
<span class="text-xs uppercase tracking-wide text-slate-400">仕掛り中(準備中・仕掛中・停止中)</span>
|
| 213 |
-
<p class="mt-2 text-3xl font-semibold text-white" data-metric="active">0枚</p>
|
| 214 |
-
<p class="mt-2 text-xs text-slate-400">ボトルネック発見のために観察したい枚数。</p>
|
| 215 |
-
</article>
|
| 216 |
-
<article class="rounded-xl border border-slate-700/60 bg-slate-900/40 p-4 shadow-inner shadow-black/10">
|
| 217 |
-
<span class="text-xs uppercase tracking-wide text-slate-400">直近7日間の完了</span>
|
| 218 |
-
<p class="mt-2 text-3xl font-semibold text-white" data-metric="throughput7">0枚</p>
|
| 219 |
-
<p class="mt-2 text-xs text-slate-400">完了ペース(スループット)の目安。</p>
|
| 220 |
-
</article>
|
| 221 |
-
<article class="rounded-xl border border-slate-700/60 bg-slate-900/40 p-4 shadow-inner shadow-black/10">
|
| 222 |
-
<span class="text-xs uppercase tracking-wide text-slate-400">平均リードタイム</span>
|
| 223 |
-
<p class="mt-2 text-3xl font-semibold text-white" data-metric="leadtime">--</p>
|
| 224 |
-
<p class="mt-2 text-xs text-slate-400">完了までの日数。平準化の指標として活用可能。</p>
|
| 225 |
-
</article>
|
| 226 |
-
</div>
|
| 227 |
-
</div>
|
| 228 |
-
</section>
|
| 229 |
-
|
| 230 |
-
<section aria-labelledby="filter-title" class="rounded-2xl bg-slate-800/70 p-6 shadow-lg shadow-black/20" data-panel="filters">
|
| 231 |
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
| 232 |
-
<h2 id="filter-title" class="text-lg font-semibold text-white">フィルタと検索</h2>
|
| 233 |
-
<button type="button" class="panel-toggle" data-action="toggle-panel" data-target="filters" aria-expanded="true">折りたたむ</button>
|
| 234 |
-
</div>
|
| 235 |
-
<div class="mt-6 space-y-6" data-panel-body="filters">
|
| 236 |
-
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
| 237 |
-
<label class="space-y-2 text-sm">
|
| 238 |
-
<span class="text-slate-200">キーワード検索</span>
|
| 239 |
-
<input id="filter-keyword" type="search" placeholder="例: 標準作業票" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 240 |
-
</label>
|
| 241 |
-
<label class="space-y-2 text-sm">
|
| 242 |
-
<span class="text-slate-200">期限(開始)</span>
|
| 243 |
-
<input id="filter-due-start" type="date" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 244 |
-
</label>
|
| 245 |
-
<label class="space-y-2 text-sm">
|
| 246 |
-
<span class="text-slate-200">期限(終了)</span>
|
| 247 |
-
<input id="filter-due-end" type="date" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 248 |
-
</label>
|
| 249 |
-
</div>
|
| 250 |
-
<div class="flex flex-wrap gap-2">
|
| 251 |
-
<button id="filter-clear" type="button" class="rounded-lg border border-slate-600 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">
|
| 252 |
-
条件クリア
|
| 253 |
-
</button>
|
| 254 |
-
</div>
|
| 255 |
-
<div class="space-y-3">
|
| 256 |
-
<div class="flex items-center justify-between">
|
| 257 |
-
<span class="text-sm font-semibold text-white">タグで絞り込み</span>
|
| 258 |
-
</div>
|
| 259 |
-
<div id="filter-tag-list" class="flex flex-wrap gap-2"></div>
|
| 260 |
-
<p class="text-xs text-slate-500">タグをクリックしてON/OFFを切り替え。複数選択でAND条件。</p>
|
| 261 |
-
</div>
|
| 262 |
-
</div>
|
| 263 |
-
</section>
|
| 264 |
-
|
| 265 |
-
<section aria-labelledby="form-title" class="rounded-2xl bg-slate-800/70 p-6 shadow-lg shadow-black/20" data-panel="form">
|
| 266 |
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
| 267 |
-
<div>
|
| 268 |
-
<h2 id="form-title" class="text-lg font-semibold text-white">タスク登録</h2>
|
| 269 |
-
<p class="text-sm text-slate-400">ポイントは小さく、進捗はこまめに。日々の改善に役立てよう!</p>
|
| 270 |
-
</div>
|
| 271 |
-
<div class="flex flex-wrap gap-2">
|
| 272 |
-
<button type="button" class="panel-toggle" data-action="toggle-panel" data-target="form" aria-expanded="true">折りたたむ</button>
|
| 273 |
-
</div>
|
| 274 |
-
</div>
|
| 275 |
-
<div class="mt-6" data-panel-body="form">
|
| 276 |
-
<form id="task-form" class="grid gap-4 md:grid-cols-12 md:items-end">
|
| 277 |
-
<label class="space-y-2 text-sm md:col-span-4">
|
| 278 |
-
<span class="text-slate-200">タイトル</span>
|
| 279 |
-
<input id="task-title" name="title" type="text" required placeholder="例: 毎朝の現場ミーティング準備" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 280 |
-
</label>
|
| 281 |
-
<label class="space-y-2 text-sm md:col-span-3">
|
| 282 |
-
<span class="text-slate-200">担当</span>
|
| 283 |
-
<input id="task-owner" name="owner" type="text" placeholder="例: 佐藤" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 284 |
-
</label>
|
| 285 |
-
<label class="space-y-2 text-sm md:col-span-3">
|
| 286 |
-
<span class="text-slate-200">期限</span>
|
| 287 |
-
<input id="task-due" name="due" type="date" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 288 |
-
</label>
|
| 289 |
-
<label class="space-y-2 text-sm md:col-span-2">
|
| 290 |
-
<span class="text-slate-200">工程</span>
|
| 291 |
-
<select id="task-status" name="status" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40">
|
| 292 |
-
<option value="backlog">バックログ</option>
|
| 293 |
-
<option value="ready">準備中</option>
|
| 294 |
-
<option value="inProgress">仕掛中</option>
|
| 295 |
-
<option value="waiting">停止中</option>
|
| 296 |
-
<option value="done">完了</option>
|
| 297 |
-
</select>
|
| 298 |
-
</label>
|
| 299 |
-
<label class="space-y-2 text-sm md:col-span-4 md:col-start-1 md:row-start-2">
|
| 300 |
-
<span class="text-slate-200">タグ(カンマ区切り)</span>
|
| 301 |
-
<input id="task-tags" name="tags" type="text" placeholder="例: 改善,品質" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 302 |
-
</label>
|
| 303 |
-
<label class="space-y-2 text-sm md:col-span-4 md:row-start-2">
|
| 304 |
-
<span class="text-slate-200">参考リンク(URL)</span>
|
| 305 |
-
<input id="task-link" name="link" type="text" placeholder="例: https://example.com" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 306 |
-
</label>
|
| 307 |
-
<label class="space-y-2 text-sm md:col-span-12 md:row-start-3">
|
| 308 |
-
<span class="text-slate-200">メモ</span>
|
| 309 |
-
<textarea id="task-notes" name="notes" rows="2" placeholder="作業手順や注意点などを記入" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 placeholder:text-slate-500 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40"></textarea>
|
| 310 |
-
</label>
|
| 311 |
-
<div class="md:col-span-4 md:col-start-9 md:row-start-4 md:flex md:justify-end md:space-x-3">
|
| 312 |
-
<button type="reset" class="mt-2 w-full rounded-lg border border-slate-600 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-400 hover:text-white md:mt-0 md:w-auto">
|
| 313 |
-
クリア
|
| 314 |
-
</button>
|
| 315 |
-
<button type="submit" class="mt-2 w-full rounded-lg bg-sky-400 px-4 py-2 text-sm font-semibold text-slate-900 transition hover:bg-sky-300 md:mt-0 md:w-auto">
|
| 316 |
-
登録
|
| 317 |
-
</button>
|
| 318 |
-
</div>
|
| 319 |
-
</form>
|
| 320 |
-
<p id="message-area" class="mt-4 text-sm font-medium text-orange-500 opacity-0 transition-opacity duration-300" aria-live="assertive"></p>
|
| 321 |
-
</div>
|
| 322 |
-
</section>
|
| 323 |
-
|
| 324 |
-
<section aria-labelledby="board-title" class="space-y-4">
|
| 325 |
-
<div class="flex flex-wrap items-center justify-between gap-3">
|
| 326 |
-
<div>
|
| 327 |
-
<h2 id="board-title" class="text-lg font-semibold text-white">フローの見える化</h2>
|
| 328 |
-
<p class="text-sm text-slate-400">カードはドラッグ&ドロップで工程移動が可能。仕掛り中の制限数を超えないように管理。</p>
|
| 329 |
-
</div>
|
| 330 |
-
<div class="flex gap-2 text-xs text-slate-300">
|
| 331 |
-
<span class="rounded-full bg-slate-800/60 px-3 py-1">準備中 WIP 5</span>
|
| 332 |
-
<span class="rounded-full bg-slate-800/60 px-3 py-1">仕掛中 WIP 3</span>
|
| 333 |
-
<span class="rounded-full bg-slate-800/60 px-3 py-1">停止中 WIP 2</span>
|
| 334 |
-
</div>
|
| 335 |
-
</div>
|
| 336 |
-
|
| 337 |
-
<div class="flex flex-wrap items-center justify-between gap-4 rounded-2xl bg-slate-800/70 px-4 py-3 text-sm text-slate-300">
|
| 338 |
-
<div class="flex flex-wrap items-center gap-3">
|
| 339 |
-
<span id="selection-counter">選択中: 0枚</span>
|
| 340 |
-
<span class="text-xs text-slate-500" id="selection-hint">カード左端のチェックで選択、ツールで一括操作可能。</span>
|
| 341 |
-
</div>
|
| 342 |
-
<div class="flex flex-wrap gap-2">
|
| 343 |
-
<button id="view-toggle" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">
|
| 344 |
-
コンパクト表示
|
| 345 |
-
</button>
|
| 346 |
-
<button id="selection-selectall" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">
|
| 347 |
-
全選択
|
| 348 |
-
</button>
|
| 349 |
-
<button id="selection-clear" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-medium text-slate-200 transition hover:border-slate-400 hover:text-white" disabled>
|
| 350 |
-
選択解除
|
| 351 |
-
</button>
|
| 352 |
-
<button id="selection-delete" type="button" class="rounded-lg bg-rose-500/80 px-3 py-1 text-xs font-semibold text-rose-50 transition hover:bg-rose-500 disabled:cursor-not-allowed disabled:bg-rose-900/40" disabled>
|
| 353 |
-
選択削除
|
| 354 |
-
</button>
|
| 355 |
-
|
| 356 |
-
<button id="data-export" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">
|
| 357 |
-
エクスポート
|
| 358 |
-
</button>
|
| 359 |
-
<button id="data-import" type="button" class="rounded-lg border border-slate-600 px-3 py-1 text-xs font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">
|
| 360 |
-
インポート
|
| 361 |
-
</button>
|
| 362 |
-
<input id="data-import-input" type="file" accept="application/json" class="hidden" />
|
| 363 |
-
</div>
|
| 364 |
-
</div>
|
| 365 |
-
|
| 366 |
-
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-5">
|
| 367 |
-
<article class="flex min-h-[240px] flex-col rounded-2xl bg-slate-800/70 p-4 shadow-lg shadow-black/10" data-column="backlog">
|
| 368 |
-
<header class="mb-3 flex items-center justify-between text-sm text-slate-300">
|
| 369 |
-
<h3 class="text-base font-semibold text-white">バックログ</h3>
|
| 370 |
-
<span class="rounded-full bg-slate-800/70 px-2 py-0.5 text-xs font-medium" data-counter="backlog">0</span>
|
| 371 |
-
</header>
|
| 372 |
-
<div class="kanban-column-body kanban-scrollbar flex flex-1 flex-col gap-3" data-dropzone="backlog"></div>
|
| 373 |
-
</article>
|
| 374 |
-
|
| 375 |
-
<article class="flex min-h-[240px] flex-col rounded-2xl bg-slate-800/70 p-4 shadow-lg shadow-black/10" data-column="ready" data-wip="5">
|
| 376 |
-
<header class="mb-3 flex items-center justify-between text-sm text-slate-300">
|
| 377 |
-
<h3 class="text-base font-semibold text-white">準備中</h3>
|
| 378 |
-
<span class="rounded-full bg-slate-800/70 px-2 py-0.5 text-xs font-medium" data-counter="ready">0</span>
|
| 379 |
-
</header>
|
| 380 |
-
<div class="kanban-column-body kanban-scrollbar flex flex-1 flex-col gap-3" data-dropzone="ready"></div>
|
| 381 |
-
</article>
|
| 382 |
-
|
| 383 |
-
<article class="flex min-h-[240px] flex-col rounded-2xl bg-slate-800/70 p-4 shadow-lg shadow-black/10" data-column="inProgress" data-wip="3">
|
| 384 |
-
<header class="mb-3 flex items-center justify-between text-sm text-slate-300">
|
| 385 |
-
<h3 class="text-base font-semibold text-white">仕掛中</h3>
|
| 386 |
-
<span class="rounded-full bg-slate-800/70 px-2 py-0.5 text-xs font-medium" data-counter="inProgress">0</span>
|
| 387 |
-
</header>
|
| 388 |
-
<div class="kanban-column-body kanban-scrollbar flex flex-1 flex-col gap-3" data-dropzone="inProgress"></div>
|
| 389 |
-
</article>
|
| 390 |
-
|
| 391 |
-
<article class="flex min-h-[240px] flex-col rounded-2xl bg-slate-800/70 p-4 shadow-lg shadow-black/10" data-column="waiting" data-wip="2">
|
| 392 |
-
<header class="mb-3 flex items-center justify-between text-sm text-slate-300">
|
| 393 |
-
<h3 class="text-base font-semibold text-white">停止中</h3>
|
| 394 |
-
<span class="rounded-full bg-slate-800/70 px-2 py-0.5 text-xs font-medium" data-counter="waiting">0</span>
|
| 395 |
-
</header>
|
| 396 |
-
<div class="kanban-column-body kanban-scrollbar flex flex-1 flex-col gap-3" data-dropzone="waiting"></div>
|
| 397 |
-
</article>
|
| 398 |
-
|
| 399 |
-
<article class="flex min-h-[240px] flex-col rounded-2xl bg-slate-800/70 p-4 shadow-lg shadow-black/10" data-column="done">
|
| 400 |
-
<header class="mb-3 flex items-center justify-between text-sm text-slate-300">
|
| 401 |
-
<h3 class="text-base font-semibold text-white">完了</h3>
|
| 402 |
-
<span class="rounded-full bg-slate-800/70 px-2 py-0.5 text-xs font-medium" data-counter="done">0</span>
|
| 403 |
-
</header>
|
| 404 |
-
<div class="kanban-column-body kanban-scrollbar flex flex-1 flex-col gap-3" data-dropzone="done"></div>
|
| 405 |
-
</article>
|
| 406 |
-
</div>
|
| 407 |
-
</section>
|
| 408 |
-
</div>
|
| 409 |
-
|
| 410 |
-
<template id="task-template">
|
| 411 |
-
<div class="kanban-card group rounded-xl border border-slate-700 bg-slate-800/80 p-4 shadow-sm shadow-black/20 transition hover:border-sky-400/60" draggable="true" data-task-id="">
|
| 412 |
-
<div class="kanban-card-header flex flex-wrap items-start justify-between gap-3">
|
| 413 |
-
<div class="kanban-card-main flex min-w-0 flex-1 flex-col gap-2">
|
| 414 |
-
<div class="flex flex-wrap items-start gap-3">
|
| 415 |
-
<label class="mt-1 inline-flex shrink-0 items-center">
|
| 416 |
-
<input type="checkbox" data-action="select-task" class="h-4 w-4 rounded border-slate-600 bg-slate-900 text-sky-400 focus:ring-2 focus:ring-sky-400/40" />
|
| 417 |
-
<span class="sr-only">カードを選択</span>
|
| 418 |
-
</label>
|
| 419 |
-
<div class="flex-1 min-w-0 space-y-1">
|
| 420 |
-
<h3 data-field="title" class="kanban-card-title text-base font-semibold text-white leading-tight"></h3>
|
| 421 |
-
<div class="hidden flex flex-wrap gap-2 text-xs text-slate-300" data-section="tags" aria-hidden="true">
|
| 422 |
-
<span class="sr-only">タグ</span>
|
| 423 |
-
<div data-field="tags" class="flex flex-wrap gap-2"></div>
|
| 424 |
-
</div>
|
| 425 |
-
</div>
|
| 426 |
-
</div>
|
| 427 |
-
</div>
|
| 428 |
-
<div class="kanban-card-aside flex flex-col items-end gap-2 text-xs text-slate-300">
|
| 429 |
-
<div class="hidden flex flex-wrap items-center gap-2" data-section="link">
|
| 430 |
-
<svg aria-hidden="true" class="h-3.5 w-3.5 text-sky-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
| 431 |
-
<path d="M9.91 13.91a3 3 0 0 1 0-4.24l2.83-2.83a3 3 0 0 1 4.24 4.24l-.7.7"/>
|
| 432 |
-
<path d="M14.09 10.09a3 3 0 0 1 0 4.24l-2.83 2.83a3 3 0 0 1-4.24-4.24l.7-.7"/>
|
| 433 |
-
</svg>
|
| 434 |
-
<a data-field="link" class="block break-words text-sky-300 underline-offset-2 hover:underline" href="#" target="_blank" rel="noreferrer noopener">リンクを開く</a>
|
| 435 |
-
</div>
|
| 436 |
-
</div>
|
| 437 |
-
</div>
|
| 438 |
-
<div class="kanban-card-actions mt-3 flex flex-wrap justify-end gap-2">
|
| 439 |
-
<button class="rounded-md border border-slate-500 px-2 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-300 hover:text-white" type="button" data-action="toggle-details" aria-expanded="false">
|
| 440 |
-
詳細
|
| 441 |
-
</button>
|
| 442 |
-
<button class="hidden rounded-md bg-sky-400 px-2 py-1 text-xs font-semibold text-slate-900 transition hover:bg-sky-300 group-hover:inline-flex" type="button" data-action="mark-done">
|
| 443 |
-
完了へ
|
| 444 |
-
</button>
|
| 445 |
-
<button class="hidden rounded-md border border-slate-500 px-2 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-300 hover:text-white group-hover:inline-flex" type="button" data-action="edit-task">
|
| 446 |
-
編集
|
| 447 |
-
</button>
|
| 448 |
-
<button class="hidden rounded-md border border-rose-600 px-2 py-1 text-xs font-semibold text-rose-200 transition hover:border-rose-400 hover:text-rose-50 group-hover:inline-flex" type="button" data-action="delete-task">
|
| 449 |
-
削除
|
| 450 |
-
</button>
|
| 451 |
-
</div>
|
| 452 |
-
<dl class="kanban-card-meta mt-3 text-xs text-slate-300" data-section="meta">
|
| 453 |
-
<div class="kanban-card-meta-item space-y-1">
|
| 454 |
-
<dt class="uppercase tracking-wide text-slate-500">担当</dt>
|
| 455 |
-
<dd class="font-medium text-white" data-field="owner"></dd>
|
| 456 |
-
</div>
|
| 457 |
-
<div class="kanban-card-meta-item space-y-1">
|
| 458 |
-
<dt class="uppercase tracking-wide text-slate-500">期限</dt>
|
| 459 |
-
<dd class="font-medium text-white" data-field="due"></dd>
|
| 460 |
-
</div>
|
| 461 |
-
</dl>
|
| 462 |
-
<p class="kanban-card-notes mt-3 whitespace-pre-line text-sm text-slate-200" data-section="notes" data-field="notes"></p>
|
| 463 |
-
</div>
|
| 464 |
-
</template>
|
| 465 |
-
|
| 466 |
-
<dialog id="edit-dialog" class="w-full max-w-xl rounded-2xl border border-slate-600/60 bg-slate-900/95 p-0 text-slate-100 backdrop:bg-slate-900/60">
|
| 467 |
-
<form id="edit-form" class="grid gap-4 p-6" method="dialog">
|
| 468 |
-
<header class="space-y-1">
|
| 469 |
-
<h2 class="text-lg font-semibold text-white">タスクの編集</h2>
|
| 470 |
-
<p class="text-sm text-slate-400">内容の更新や振り返りを反映できます。</p>
|
| 471 |
-
</header>
|
| 472 |
-
<label class="space-y-2 text-sm">
|
| 473 |
-
<span class="text-slate-200">タイトル</span>
|
| 474 |
-
<input id="edit-title" name="title" type="text" required class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 475 |
-
</label>
|
| 476 |
-
<div class="grid gap-4 md:grid-cols-2">
|
| 477 |
-
<label class="space-y-2 text-sm">
|
| 478 |
-
<span class="text-slate-200">担当</span>
|
| 479 |
-
<input id="edit-owner" name="owner" type="text" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 480 |
-
</label>
|
| 481 |
-
<label class="space-y-2 text-sm">
|
| 482 |
-
<span class="text-slate-200">期限</span>
|
| 483 |
-
<input id="edit-due" name="due" type="date" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 484 |
-
</label>
|
| 485 |
-
</div>
|
| 486 |
-
<label class="space-y-2 text-sm">
|
| 487 |
-
<span class="text-slate-200">メモ</span>
|
| 488 |
-
<textarea id="edit-notes" name="notes" rows="3" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40"></textarea>
|
| 489 |
-
</label>
|
| 490 |
-
|
| 491 |
-
<label class="space-y-2 text-sm">
|
| 492 |
-
<span class="text-slate-200">タグ(カンマ区切り)</span>
|
| 493 |
-
<input id="edit-tags" name="tags" type="text" placeholder="例: デザイン, レビュー" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 494 |
-
</label>
|
| 495 |
-
|
| 496 |
-
<label class="space-y-2 text-sm">
|
| 497 |
-
<span class="text-slate-200">参考リンク(URL)</span>
|
| 498 |
-
<input id="edit-link" name="link" type="text" class="w-full rounded-lg border border-transparent bg-slate-800/80 px-3 py-2 text-slate-100 focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400/40" />
|
| 499 |
-
</label>
|
| 500 |
-
<div class="flex justify-end gap-3 pt-2">
|
| 501 |
-
<button type="button" data-action="cancel-edit" class="rounded-lg border border-slate-600 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-400 hover:text-white">キャンセル</button>
|
| 502 |
-
<button type="submit" class="rounded-lg bg-sky-400 px-4 py-2 text-sm font-semibold text-slate-900 transition hover:bg-sky-300">保存</button>
|
| 503 |
-
</div>
|
| 504 |
-
</form>
|
| 505 |
-
</dialog>
|
| 506 |
-
|
| 507 |
-
<script src="./app.js"></script>
|
| 508 |
-
</body>
|
| 509 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
v6_軽量KanbanBoard/tailwindcdn.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|