Added some WIP assets for the Local/World/UI views and the cover art.
This commit is contained in:
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "assets-vor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A repo used for tracking and containg the assets for the game Visions Of Reality",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "prospera:assets-vor.git"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"kanban": "serve tools/kanban/ -p 5173"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"devDependencies": {
|
||||||
|
"serve": "^14.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/Marketing/PSD/game_cover.psd
LFS
Normal file
BIN
src/Marketing/PSD/game_cover.psd
LFS
Normal file
Binary file not shown.
BIN
src/Marketing/game_cover.png
LFS
Normal file
BIN
src/Marketing/game_cover.png
LFS
Normal file
Binary file not shown.
BIN
src/UI/gem.psd
LFS
Normal file
BIN
src/UI/gem.psd
LFS
Normal file
Binary file not shown.
BIN
src/UI/item_pickaxe.psd
LFS
Normal file
BIN
src/UI/item_pickaxe.psd
LFS
Normal file
Binary file not shown.
BIN
src/UI/item_wand_01.psd
LFS
Normal file
BIN
src/UI/item_wand_01.psd
LFS
Normal file
Binary file not shown.
BIN
src/UI/moon.psd
LFS
Normal file
BIN
src/UI/moon.psd
LFS
Normal file
Binary file not shown.
BIN
src/UI/resource_wood.aseprite
Normal file
BIN
src/UI/resource_wood.aseprite
Normal file
Binary file not shown.
BIN
src/UI/terrain_forest.psd
LFS
Normal file
BIN
src/UI/terrain_forest.psd
LFS
Normal file
Binary file not shown.
BIN
src/UI/terrain_mountain.psd
LFS
Normal file
BIN
src/UI/terrain_mountain.psd
LFS
Normal file
Binary file not shown.
BIN
src/WorldView/campfire_unlit.vox
Normal file
BIN
src/WorldView/campfire_unlit.vox
Normal file
Binary file not shown.
BIN
src/WorldView/forest.vox
Normal file
BIN
src/WorldView/forest.vox
Normal file
Binary file not shown.
BIN
src/WorldView/graveyard.vox
Normal file
BIN
src/WorldView/graveyard.vox
Normal file
Binary file not shown.
BIN
src/WorldView/nine-tile-track.vox
Normal file
BIN
src/WorldView/nine-tile-track.vox
Normal file
Binary file not shown.
217
tools/kanban/kanban.html
Normal file
217
tools/kanban/kanban.html
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Simple Kanban Board</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="src/styles.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="src/kanban.ico">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-gray-800" id="main-body">
|
||||||
|
|
||||||
|
<!-- Dark mode toggle in upper right -->
|
||||||
|
<div class="fixed top-4 right-4 z-50">
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="dark-mode-toggle" class="sr-only">
|
||||||
|
<span class="w-10 h-6 flex items-center bg-gray-300 rounded-full p-1 transition-colors duration-200">
|
||||||
|
<span class="dot w-4 h-4 bg-white rounded-full shadow-md transform transition-transform duration-200"></span>
|
||||||
|
</span>
|
||||||
|
<span class="ml-2">
|
||||||
|
<svg id="darkmode-lightbulb" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 2a7 7 0 017 7c0 3.5-2.5 5.5-3.5 6.5-.5.5-.5 1.5-.5 2.5v1a1 1 0 01-1 1h-2a1 1 0 01-1-1v-1c0-1-.1-2 .5-2.5C7.5 14.5 5 12.5 5 9a7 7 0 017-7zm0 20a2 2 0 002-2H10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
<header class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900">Kanban Board</h1>
|
||||||
|
<p class="text-gray-600 mt-2">Drag and drop tasks to organize your workflow.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 mt-4 mb-6 justify-between">
|
||||||
|
<div class="flex items-center gap-2 w-full sm:w-auto justify-start">
|
||||||
|
<button class="add-task-btn flex items-center justify-center w-10 h-10 bg-red-500 rounded-full shadow hover:bg-red-600 transition duration-150" data-column="todo-column" style="border: none; padding: 0;">
|
||||||
|
<span class="sr-only">Add Task</span>
|
||||||
|
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||||
|
<line x1="12" y1="6" x2="12" y2="18" />
|
||||||
|
<line x1="6" y1="12" x2="18" y2="12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="ml-2 text-gray-700 font-medium text-base hidden sm:inline">Add Task</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||||
|
<label for="tag-filter" class="text-gray-700 font-medium text-base mr-2">Filter by Tag:</label>
|
||||||
|
<select id="tag-filter" class="border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-red-400 shadow-sm w-full sm:w-48">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="unassigned">Unassigned</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kanban Board Columns -->
|
||||||
|
<div id="kanban-board" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- To Do Column -->
|
||||||
|
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="todo-column">
|
||||||
|
<div class="flex justify-between items-center pb-2 border-b-2 border-red-400 mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700">To Do</h2>
|
||||||
|
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="todo-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="todo">
|
||||||
|
<!-- Tasks Go Here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- In Progress Column -->
|
||||||
|
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="inprogress-column">
|
||||||
|
<div class="flex justify-between items-center pb-2 border-b-2 border-yellow-400 mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700">In Progress</h2>
|
||||||
|
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="inprogress-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="inprogress">
|
||||||
|
<!-- Tasks Go Here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Completed Column -->
|
||||||
|
<div class="kanban-column bg-white rounded-lg shadow-md p-4 flex flex-col" id="completed-column">
|
||||||
|
<div class="flex justify-between items-center pb-2 border-b-2 border-green-400 mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700">Completed</h2>
|
||||||
|
<button class="sort-btn text-sm text-gray-600 hover:text-gray-900 font-medium py-1 px-2 rounded-md hover:bg-gray-100 transition-colors flex items-center gap-1" data-column-id="completed-column">Sort <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="tasks flex-grow space-y-3 overflow-y-auto" data-column-id="completed">
|
||||||
|
<!-- Tasks Go Here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Task Modal -->
|
||||||
|
<div id="task-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-md">
|
||||||
|
<h3 class="text-lg font-bold mb-4" id="modal-title">Add New Task</h3>
|
||||||
|
<textarea id="task-input" class="w-full border border-gray-300 rounded-md p-2" placeholder="Enter task description..."></textarea>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="task-due-date" class="block text-sm font-medium text-gray-700">Due Date</label>
|
||||||
|
<input type="date" id="task-due-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="stakeholders-input" class="block text-sm font-medium text-gray-700">Key Stakeholders (optional)</label>
|
||||||
|
<input type="text" id="stakeholders-input" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Enter key stakeholders...">
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="notes-input" class="block text-sm font-medium text-gray-700">Notes (optional)</label>
|
||||||
|
<textarea id="notes-input" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Enter notes..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end space-x-2">
|
||||||
|
<button id="cancel-task" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancel</button>
|
||||||
|
<button id="save-task" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save Task</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tag Management Modal -->
|
||||||
|
<div id="tag-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-md">
|
||||||
|
<h3 class="text-lg font-bold mb-4">Manage Tag</h3>
|
||||||
|
<div id="existing-tags-container" class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Select Existing Tag</label>
|
||||||
|
<div id="tags-list" class="flex flex-wrap gap-2">
|
||||||
|
<!-- Existing tags will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="my-4">
|
||||||
|
<div>
|
||||||
|
<label for="new-tag-name" class="block text-sm font-medium text-gray-700">Or Create New Tag</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="text" id="new-tag-name" class="flex-grow border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Tag name...">
|
||||||
|
<input type="color" id="new-tag-color" class="w-10 h-10 p-1 border border-gray-300 rounded-md cursor-pointer" value="#3b82f6">
|
||||||
|
<button id="save-tag" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="remove-tag-name" class="block text-sm font-medium text-gray-700">Or Remove Tag</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<button id="remove-tag" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600">Remove Tag</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end space-x-2">
|
||||||
|
<button id="cancel-tag" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||||
|
<h3 class="text-lg font-bold mb-4">Confirm Deletion</h3>
|
||||||
|
<p class="text-gray-600">Are you sure you want to delete this task?</p>
|
||||||
|
<div class="mt-6 flex justify-center space-x-4">
|
||||||
|
<button id="cancel-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||||
|
<button id="confirm-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task Details Modal -->
|
||||||
|
<div id="task-details-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-xl font-bold">Task Details</h3>
|
||||||
|
<button id="close-task-details" class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Task Description -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label class="text-sm font-medium text-gray-700">Task Description</label>
|
||||||
|
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="description">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div id="display-description" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||||
|
<textarea id="edit-description" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- Due Date -->
|
||||||
|
<div>
|
||||||
|
<label for="edit-due-date" class="block text-sm font-medium text-gray-700">Due Date</label>
|
||||||
|
<input type="date" id="edit-due-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<!-- Date Started -->
|
||||||
|
<div>
|
||||||
|
<label for="edit-started-date" class="block text-sm font-medium text-gray-700">Date Started</label>
|
||||||
|
<input type="date" id="edit-started-date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<!-- Stakeholders -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label class="text-sm font-medium text-gray-700">Key Stakeholders</label>
|
||||||
|
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="stakeholders">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div id="display-stakeholders" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||||
|
<textarea id="edit-stakeholders" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- Notes -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label class="text-sm font-medium text-gray-700">Notes</label>
|
||||||
|
<button class="edit-field-btn text-blue-600 hover:text-blue-800 text-sm" data-field="notes">Edit</button>
|
||||||
|
</div>
|
||||||
|
<div id="display-notes" class="text-gray-900 p-2 border rounded bg-gray-50"></div>
|
||||||
|
<textarea id="edit-notes" class="w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 hidden" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end space-x-2">
|
||||||
|
<button id="save-task-changes" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="src/SimpleKanban.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
916
tools/kanban/src/SimpleKanban.js
Normal file
916
tools/kanban/src/SimpleKanban.js
Normal file
@@ -0,0 +1,916 @@
|
|||||||
|
// Tag delete confirmation modal logic
|
||||||
|
let tagToDelete = null;
|
||||||
|
let tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||||
|
let tagDeleteNameSpan = null;
|
||||||
|
let tagDeleteConfirmBtn = null;
|
||||||
|
let tagDeleteCancelBtn = null;
|
||||||
|
function ensureTagDeleteModal() {
|
||||||
|
tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||||
|
if (!tagDeleteModal) {
|
||||||
|
tagDeleteModal = document.createElement('div');
|
||||||
|
tagDeleteModal.id = 'tag-delete-modal';
|
||||||
|
tagDeleteModal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden';
|
||||||
|
tagDeleteModal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||||
|
<h3 class="text-lg font-bold mb-4">Delete Tag</h3>
|
||||||
|
<p class="text-gray-600 mb-2">Are you sure you want to delete the tag <span id="tag-delete-name" class="font-bold"></span>? This will remove the tag from all tasks.</p>
|
||||||
|
<div class="mt-6 flex justify-center space-x-4">
|
||||||
|
<button id="cancel-tag-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||||
|
<button id="confirm-tag-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(tagDeleteModal);
|
||||||
|
}
|
||||||
|
tagDeleteNameSpan = tagDeleteModal.querySelector('#tag-delete-name');
|
||||||
|
tagDeleteConfirmBtn = tagDeleteModal.querySelector('#confirm-tag-delete');
|
||||||
|
tagDeleteCancelBtn = tagDeleteModal.querySelector('#cancel-tag-delete');
|
||||||
|
}
|
||||||
|
ensureTagDeleteModal();
|
||||||
|
function showTagDeleteModal(tagName) {
|
||||||
|
tagToDelete = tagName;
|
||||||
|
ensureTagDeleteModal();
|
||||||
|
tagDeleteNameSpan.textContent = tagName;
|
||||||
|
tagDeleteModal.classList.remove('hidden');
|
||||||
|
tagDeleteCancelBtn.onclick = hideTagDeleteModal;
|
||||||
|
tagDeleteConfirmBtn.onclick = function() {
|
||||||
|
if (tagToDelete) {
|
||||||
|
// Remove tag from global tags
|
||||||
|
delete tags[tagToDelete];
|
||||||
|
// Remove tag from all tasks
|
||||||
|
document.querySelectorAll('.task').forEach(task => {
|
||||||
|
if (task.dataset.tagName === tagToDelete) {
|
||||||
|
delete task.dataset.tagName;
|
||||||
|
delete task.dataset.tagColor;
|
||||||
|
const tagCircle = task.querySelector('.tag-circle');
|
||||||
|
if (tagCircle) {
|
||||||
|
tagCircle.style.backgroundColor = 'transparent';
|
||||||
|
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||||
|
}
|
||||||
|
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||||
|
if (tooltipOverlay) {
|
||||||
|
tooltipOverlay.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
openTagModal();
|
||||||
|
hideTagDeleteModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function hideTagDeleteModal() {
|
||||||
|
tagDeleteModal.classList.add('hidden');
|
||||||
|
tagToDelete = null;
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
let draggedTask = null;
|
||||||
|
let targetColumn = null;
|
||||||
|
let taskToDelete = null;
|
||||||
|
let currentTaskForTagging = null;
|
||||||
|
|
||||||
|
// Global tags object
|
||||||
|
let tags = {
|
||||||
|
// Example: "UI/UX": "#3b82f6",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tag delete confirmation modal logic (move inside DOMContentLoaded for correct tags reference)
|
||||||
|
let tagToDelete = null;
|
||||||
|
let tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||||
|
let tagDeleteNameSpan = null;
|
||||||
|
let tagDeleteConfirmBtn = null;
|
||||||
|
let tagDeleteCancelBtn = null;
|
||||||
|
function ensureTagDeleteModal() {
|
||||||
|
tagDeleteModal = document.getElementById('tag-delete-modal');
|
||||||
|
if (!tagDeleteModal) {
|
||||||
|
tagDeleteModal = document.createElement('div');
|
||||||
|
tagDeleteModal.id = 'tag-delete-modal';
|
||||||
|
tagDeleteModal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden';
|
||||||
|
tagDeleteModal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 w-11/12 max-w-sm text-center">
|
||||||
|
<h3 class="text-lg font-bold mb-4">Delete Tag</h3>
|
||||||
|
<p class="text-gray-600 mb-2">Are you sure you want to delete the tag <span id="tag-delete-name" class="font-bold"></span>? This will remove the tag from all tasks.</p>
|
||||||
|
<div class="mt-6 flex justify-center space-x-4">
|
||||||
|
<button id="cancel-tag-delete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 w-24">Cancel</button>
|
||||||
|
<button id="confirm-tag-delete" class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 w-24">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(tagDeleteModal);
|
||||||
|
}
|
||||||
|
tagDeleteNameSpan = tagDeleteModal.querySelector('#tag-delete-name');
|
||||||
|
tagDeleteConfirmBtn = tagDeleteModal.querySelector('#confirm-tag-delete');
|
||||||
|
tagDeleteCancelBtn = tagDeleteModal.querySelector('#cancel-tag-delete');
|
||||||
|
}
|
||||||
|
ensureTagDeleteModal();
|
||||||
|
function showTagDeleteModal(tagName) {
|
||||||
|
tagToDelete = tagName;
|
||||||
|
ensureTagDeleteModal();
|
||||||
|
tagDeleteNameSpan.textContent = tagName;
|
||||||
|
tagDeleteModal.classList.remove('hidden');
|
||||||
|
tagDeleteCancelBtn.onclick = hideTagDeleteModal;
|
||||||
|
tagDeleteConfirmBtn.onclick = function() {
|
||||||
|
if (tagToDelete) {
|
||||||
|
// Remove tag from global tags
|
||||||
|
delete tags[tagToDelete];
|
||||||
|
// Remove tag from all tasks
|
||||||
|
document.querySelectorAll('.task').forEach(task => {
|
||||||
|
if (task.dataset.tagName === tagToDelete) {
|
||||||
|
delete task.dataset.tagName;
|
||||||
|
delete task.dataset.tagColor;
|
||||||
|
const tagCircle = task.querySelector('.tag-circle');
|
||||||
|
if (tagCircle) {
|
||||||
|
tagCircle.style.backgroundColor = 'transparent';
|
||||||
|
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||||
|
}
|
||||||
|
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||||
|
if (tooltipOverlay) {
|
||||||
|
tooltipOverlay.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
openTagModal();
|
||||||
|
hideTagDeleteModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function hideTagDeleteModal() {
|
||||||
|
tagDeleteModal.classList.add('hidden');
|
||||||
|
tagToDelete = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagModal = document.getElementById('tag-modal');
|
||||||
|
const cancelTagBtn = document.getElementById('cancel-tag');
|
||||||
|
const saveTagBtn = document.getElementById('save-tag');
|
||||||
|
const removeTagBtn = document.getElementById('remove-tag');
|
||||||
|
const newTagNameInput = document.getElementById('new-tag-name');
|
||||||
|
const newTagColorInput = document.getElementById('new-tag-color');
|
||||||
|
const tagsListContainer = document.getElementById('tags-list');
|
||||||
|
|
||||||
|
// Function to initialize event listeners for a task
|
||||||
|
function initializeTaskEvents(task) {
|
||||||
|
task.addEventListener('dragstart', () => {
|
||||||
|
draggedTask = task;
|
||||||
|
setTimeout(() => {
|
||||||
|
task.classList.add('dragging');
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
task.addEventListener('dragend', () => {
|
||||||
|
task.classList.remove('dragging');
|
||||||
|
draggedTask = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Double-click to open details modal
|
||||||
|
task.addEventListener('dblclick', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openTaskDetailsModal(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteBtn = task.querySelector('.delete-task-btn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
taskToDelete = task;
|
||||||
|
deleteModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagCircle = task.querySelector('.tag-circle');
|
||||||
|
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||||
|
|
||||||
|
if (tagCircle && tooltipOverlay) {
|
||||||
|
tagCircle.addEventListener('mouseenter', () => {
|
||||||
|
if (task.dataset.tagName) {
|
||||||
|
tooltipOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tagCircle.addEventListener('mouseleave', () => {
|
||||||
|
tooltipOverlay.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
tagCircle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
currentTaskForTagging = task;
|
||||||
|
openTagModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to open and populate the tag modal
|
||||||
|
function openTagModal() {
|
||||||
|
tagsListContainer.innerHTML = '';
|
||||||
|
Object.entries(tags).forEach(([name, color]) => {
|
||||||
|
const tagChip = document.createElement('div');
|
||||||
|
tagChip.className = 'px-3 py-1 rounded-full text-sm font-medium cursor-pointer flex items-center gap-2';
|
||||||
|
tagChip.style.backgroundColor = color;
|
||||||
|
tagChip.style.color = getContrastYIQ(color);
|
||||||
|
tagChip.textContent = name;
|
||||||
|
tagChip.dataset.tagName = name;
|
||||||
|
tagChip.dataset.tagColor = color;
|
||||||
|
tagChip.addEventListener('click', () => {
|
||||||
|
applyTagToTask(name, color);
|
||||||
|
closeTagModal();
|
||||||
|
});
|
||||||
|
// Add delete button for tag
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'ml-2 text-xs text-white bg-red-600 rounded-full w-5 h-5 flex items-center justify-center hover:bg-red-800';
|
||||||
|
deleteBtn.innerHTML = '<span>×</span>';
|
||||||
|
deleteBtn.title = 'Delete Tag';
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showTagDeleteModal(name);
|
||||||
|
});
|
||||||
|
tagChip.appendChild(deleteBtn);
|
||||||
|
tagsListContainer.appendChild(tagChip);
|
||||||
|
});
|
||||||
|
newTagNameInput.value = '';
|
||||||
|
tagModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTagModal() {
|
||||||
|
tagModal.classList.add('hidden');
|
||||||
|
currentTaskForTagging = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTagToTask(name, color) {
|
||||||
|
if (!currentTaskForTagging) return;
|
||||||
|
const tagCircle = currentTaskForTagging.querySelector('.tag-circle');
|
||||||
|
currentTaskForTagging.dataset.tagName = name;
|
||||||
|
currentTaskForTagging.dataset.tagColor = color;
|
||||||
|
tagCircle.style.backgroundColor = color;
|
||||||
|
tagCircle.classList.remove('border-2', 'border-gray-400');
|
||||||
|
// Add or update tooltip
|
||||||
|
const tooltipOverlay = currentTaskForTagging.querySelector('.task-tooltip-overlay');
|
||||||
|
if (tooltipOverlay) {
|
||||||
|
tooltipOverlay.textContent = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTagFromTask() {
|
||||||
|
if (!currentTaskForTagging) return;
|
||||||
|
const tagCircle = currentTaskForTagging.querySelector('.tag-circle');
|
||||||
|
delete currentTaskForTagging.dataset.tagName;
|
||||||
|
delete currentTaskForTagging.dataset.tagColor;
|
||||||
|
tagCircle.style.backgroundColor = 'transparent';
|
||||||
|
tagCircle.classList.add('border-2', 'border-gray-400');
|
||||||
|
const tooltipOverlay = currentTaskForTagging.querySelector('.task-tooltip-overlay');
|
||||||
|
if (tooltipOverlay) {
|
||||||
|
tooltipOverlay.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTagBtn.addEventListener('click', () => {
|
||||||
|
const newName = newTagNameInput.value.trim();
|
||||||
|
const newColor = newTagColorInput.value;
|
||||||
|
if (!newName) {
|
||||||
|
newTagNameInput.focus();
|
||||||
|
return; // Do not close modal if name is empty
|
||||||
|
}
|
||||||
|
// Add to global tags if it's new
|
||||||
|
if (!tags[newName]) {
|
||||||
|
tags[newName] = newColor;
|
||||||
|
}
|
||||||
|
applyTagToTask(newName, newColor);
|
||||||
|
closeTagModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
removeTagBtn.addEventListener('click', () => {
|
||||||
|
removeTagFromTask();
|
||||||
|
closeTagModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelTagBtn.addEventListener('click', closeTagModal);
|
||||||
|
|
||||||
|
// Linkify function for emails and URLs
|
||||||
|
function linkifyText(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
// Linkify emails
|
||||||
|
text = text.replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, '<a href="mailto:$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||||
|
// Linkify URLs
|
||||||
|
text = text.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||||
|
text = text.replace(/(www\.[^\s]+)/g, '<a href="http://$1" class="text-blue-600 hover:underline" target="_blank">$1</a>');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task Details Modal logic
|
||||||
|
const taskDetailsModal = document.getElementById('task-details-modal');
|
||||||
|
const closeTaskDetailsBtn = document.getElementById('close-task-details');
|
||||||
|
const saveTaskChangesBtn = document.getElementById('save-task-changes');
|
||||||
|
const editFieldBtns = document.querySelectorAll('.edit-field-btn');
|
||||||
|
let currentEditingTask = null;
|
||||||
|
|
||||||
|
function openTaskDetailsModal(task) {
|
||||||
|
currentEditingTask = task;
|
||||||
|
// Populate display fields with linkified data
|
||||||
|
document.getElementById('display-description').innerHTML = linkifyText(task.querySelector('p').textContent);
|
||||||
|
document.getElementById('display-stakeholders').innerHTML = linkifyText(task.dataset.stakeholders || '');
|
||||||
|
document.getElementById('display-notes').innerHTML = linkifyText(task.dataset.notes || '');
|
||||||
|
// Populate edit fields with current values
|
||||||
|
document.getElementById('edit-description').value = task.querySelector('p').textContent;
|
||||||
|
document.getElementById('edit-due-date').value = task.dataset.dueDate || '';
|
||||||
|
document.getElementById('edit-started-date').value = task.dataset.startedDate || '';
|
||||||
|
document.getElementById('edit-stakeholders').value = task.dataset.stakeholders || '';
|
||||||
|
document.getElementById('edit-notes').value = task.dataset.notes || '';
|
||||||
|
// Hide all edit fields initially (except dates which are always visible)
|
||||||
|
document.querySelectorAll('[id^="edit-"]:not(#edit-due-date):not(#edit-started-date)').forEach(el => el.classList.add('hidden'));
|
||||||
|
document.querySelectorAll('[id^="display-"]').forEach(el => el.classList.remove('hidden'));
|
||||||
|
taskDetailsModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTaskDetailsModal() {
|
||||||
|
taskDetailsModal.classList.add('hidden');
|
||||||
|
currentEditingTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEditMode(field) {
|
||||||
|
const displayEl = document.getElementById(`display-${field}`);
|
||||||
|
const editEl = document.getElementById(`edit-${field}`);
|
||||||
|
if (editEl.classList.contains('hidden')) {
|
||||||
|
// Switch to edit mode
|
||||||
|
editEl.classList.remove('hidden');
|
||||||
|
displayEl.classList.add('hidden');
|
||||||
|
if (field === 'description') {
|
||||||
|
editEl.value = currentEditingTask.querySelector('p').textContent;
|
||||||
|
} else if (field === 'stakeholders') {
|
||||||
|
editEl.value = currentEditingTask.dataset.stakeholders || '';
|
||||||
|
} else if (field === 'notes') {
|
||||||
|
editEl.value = currentEditingTask.dataset.notes || '';
|
||||||
|
}
|
||||||
|
editEl.focus();
|
||||||
|
} else {
|
||||||
|
// Switch back to display mode
|
||||||
|
editEl.classList.add('hidden');
|
||||||
|
displayEl.classList.remove('hidden');
|
||||||
|
// Update display with the edited value
|
||||||
|
if (field === 'description') {
|
||||||
|
displayEl.innerHTML = linkifyText(editEl.value);
|
||||||
|
} else if (field === 'stakeholders') {
|
||||||
|
displayEl.innerHTML = linkifyText(editEl.value);
|
||||||
|
} else if (field === 'notes') {
|
||||||
|
displayEl.innerHTML = linkifyText(editEl.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTaskChanges() {
|
||||||
|
if (!currentEditingTask) return;
|
||||||
|
// Update task data from edit fields
|
||||||
|
const newDescription = document.getElementById('edit-description').value.trim();
|
||||||
|
const newDueDate = document.getElementById('edit-due-date').value;
|
||||||
|
const newStartedDate = document.getElementById('edit-started-date').value;
|
||||||
|
const newStakeholders = document.getElementById('edit-stakeholders').value.trim();
|
||||||
|
const newNotes = document.getElementById('edit-notes').value.trim();
|
||||||
|
|
||||||
|
// Update task element
|
||||||
|
currentEditingTask.querySelector('p').innerHTML = linkifyText(newDescription);
|
||||||
|
currentEditingTask.dataset.dueDate = newDueDate;
|
||||||
|
currentEditingTask.dataset.startedDate = newStartedDate;
|
||||||
|
currentEditingTask.dataset.stakeholders = newStakeholders;
|
||||||
|
currentEditingTask.dataset.notes = newNotes;
|
||||||
|
|
||||||
|
// Update display spans on task card
|
||||||
|
const dueDateSpan = currentEditingTask.querySelector('.due-date-display');
|
||||||
|
if (dueDateSpan) {
|
||||||
|
if (newDueDate) {
|
||||||
|
const date = new Date(newDueDate);
|
||||||
|
dueDateSpan.textContent = `Due: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||||
|
dueDateSpan.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
dueDateSpan.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedDateSpan = currentEditingTask.querySelector('.started-date-display');
|
||||||
|
if (startedDateSpan) {
|
||||||
|
if (newStartedDate) {
|
||||||
|
const date = new Date(newStartedDate);
|
||||||
|
startedDateSpan.textContent = `Started: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||||
|
startedDateSpan.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
startedDateSpan.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBoardState();
|
||||||
|
closeTaskDetailsModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeTaskDetailsBtn) {
|
||||||
|
closeTaskDetailsBtn.addEventListener('click', closeTaskDetailsModal);
|
||||||
|
}
|
||||||
|
if (saveTaskChangesBtn) {
|
||||||
|
saveTaskChangesBtn.addEventListener('click', saveTaskChanges);
|
||||||
|
}
|
||||||
|
editFieldBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const field = e.target.dataset.field;
|
||||||
|
toggleEditMode(field);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal on Escape key
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && !taskDetailsModal.classList.contains('hidden')) {
|
||||||
|
closeTaskDetailsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to create a new task element
|
||||||
|
function createNewTask(text, dueDate, stakeholders, notes) {
|
||||||
|
const task = document.createElement('div');
|
||||||
|
task.className = 'task p-4 bg-gray-50 rounded-lg shadow cursor-grab active:cursor-grabbing relative';
|
||||||
|
task.draggable = true;
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.textContent = text;
|
||||||
|
task.appendChild(p);
|
||||||
|
// Due Date
|
||||||
|
const dueDateSpan = document.createElement('span');
|
||||||
|
dueDateSpan.className = 'due-date-display text-xs text-gray-500 mt-2 block';
|
||||||
|
if (dueDate) {
|
||||||
|
task.dataset.dueDate = dueDate; // Store date for sorting
|
||||||
|
const dateParts = dueDate.split('-');
|
||||||
|
const date = new Date(Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2]));
|
||||||
|
dueDateSpan.textContent = `Due: ${date.toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||||
|
} else {
|
||||||
|
dueDateSpan.classList.add('hidden');
|
||||||
|
dueDateSpan.textContent = 'Due: --';
|
||||||
|
}
|
||||||
|
task.appendChild(dueDateSpan);
|
||||||
|
// Store stakeholders and notes in dataset
|
||||||
|
if (stakeholders) task.dataset.stakeholders = stakeholders;
|
||||||
|
if (notes) task.dataset.notes = notes;
|
||||||
|
// Started Date
|
||||||
|
const startedDateSpan = document.createElement('span');
|
||||||
|
startedDateSpan.className = 'started-date-display text-xs text-gray-500 mt-1 block hidden';
|
||||||
|
startedDateSpan.textContent = 'Started: --';
|
||||||
|
task.appendChild(startedDateSpan);
|
||||||
|
// Completed Date
|
||||||
|
const completedDateSpan = document.createElement('span');
|
||||||
|
completedDateSpan.className = 'completed-date-display text-xs text-gray-500 mt-1 block hidden';
|
||||||
|
completedDateSpan.textContent = 'Completed: --';
|
||||||
|
task.appendChild(completedDateSpan);
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'delete-task-btn absolute top-2 right-2 text-red-500 hover:text-red-700 hidden';
|
||||||
|
deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>`;
|
||||||
|
task.appendChild(deleteBtn);
|
||||||
|
const tagCircle = document.createElement('div');
|
||||||
|
tagCircle.className = 'tag-circle absolute bottom-2 right-2 w-4 h-4 rounded-full border-2 border-gray-400 cursor-pointer';
|
||||||
|
task.appendChild(tagCircle);
|
||||||
|
const tooltipOverlay = document.createElement('div');
|
||||||
|
tooltipOverlay.className = 'task-tooltip-overlay absolute inset-0 bg-black bg-opacity-60 rounded-lg flex items-center justify-center text-white font-bold hidden pointer-events-none';
|
||||||
|
task.appendChild(tooltipOverlay);
|
||||||
|
initializeTaskEvents(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize existing tasks
|
||||||
|
const tasks = document.querySelectorAll('.task');
|
||||||
|
tasks.forEach(initializeTaskEvents);
|
||||||
|
// Event listeners for drop zones (task containers)
|
||||||
|
const taskContainers = document.querySelectorAll('.tasks');
|
||||||
|
const todayDisplayString = new Date().toLocaleDateString(undefined, { timeZone: 'UTC' });
|
||||||
|
const todayDataString = new Date().toISOString().split('T')[0];
|
||||||
|
taskContainers.forEach(container => {
|
||||||
|
container.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const afterElement = getDragAfterElement(container, e.clientY);
|
||||||
|
const currentlyDragged = document.querySelector('.dragging');
|
||||||
|
if (afterElement == null) {
|
||||||
|
container.appendChild(currentlyDragged);
|
||||||
|
} else {
|
||||||
|
container.insertBefore(currentlyDragged, afterElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!draggedTask) return;
|
||||||
|
const columnId = container.dataset.columnId;
|
||||||
|
const startedDateDisplay = draggedTask.querySelector('.started-date-display');
|
||||||
|
const completedDateDisplay = draggedTask.querySelector('.completed-date-display');
|
||||||
|
const deleteBtn = draggedTask.querySelector('.delete-task-btn');
|
||||||
|
switch (columnId) {
|
||||||
|
case 'inprogress':
|
||||||
|
// Set started date only if it's not already set
|
||||||
|
if (!draggedTask.dataset.startedDate) {
|
||||||
|
draggedTask.dataset.startedDate = todayDataString;
|
||||||
|
if(startedDateDisplay) {
|
||||||
|
startedDateDisplay.textContent = `Started: ${todayDisplayString}`;
|
||||||
|
startedDateDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear completed date
|
||||||
|
draggedTask.removeAttribute('data-completed-date');
|
||||||
|
if(completedDateDisplay) {
|
||||||
|
completedDateDisplay.textContent = 'Completed: --';
|
||||||
|
completedDateDisplay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (deleteBtn) deleteBtn.classList.add('hidden');
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
// Set started date if it's not already set
|
||||||
|
if (!draggedTask.dataset.startedDate) {
|
||||||
|
draggedTask.dataset.startedDate = todayDataString;
|
||||||
|
if(startedDateDisplay) {
|
||||||
|
startedDateDisplay.textContent = `Started: ${todayDisplayString}`;
|
||||||
|
startedDateDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set completed date
|
||||||
|
draggedTask.dataset.completedDate = todayDataString;
|
||||||
|
if(completedDateDisplay) {
|
||||||
|
completedDateDisplay.textContent = `Completed: ${todayDisplayString}`;
|
||||||
|
completedDateDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
if (deleteBtn) deleteBtn.classList.remove('hidden');
|
||||||
|
break;
|
||||||
|
case 'todo':
|
||||||
|
// Clear both dates
|
||||||
|
draggedTask.removeAttribute('data-started-date');
|
||||||
|
draggedTask.removeAttribute('data-completed-date');
|
||||||
|
if(startedDateDisplay) {
|
||||||
|
startedDateDisplay.textContent = 'Started: --';
|
||||||
|
startedDateDisplay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if(completedDateDisplay) {
|
||||||
|
completedDateDisplay.textContent = 'Completed: --';
|
||||||
|
completedDateDisplay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (deleteBtn) deleteBtn.classList.add('hidden');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
draggedTask.dataset.columnId = columnId; // Persist new column for this task
|
||||||
|
saveBoardState(); // Persist after drop
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to determine where to drop the element
|
||||||
|
function getDragAfterElement(container, y) {
|
||||||
|
const draggableElements = [...container.querySelectorAll('.task:not(.dragging)')];
|
||||||
|
return draggableElements.reduce((closest, child) => {
|
||||||
|
const box = child.getBoundingClientRect();
|
||||||
|
const offset = y - box.top - box.height / 2;
|
||||||
|
if (offset < 0 && offset > closest.offset) {
|
||||||
|
return { offset: offset, element: child };
|
||||||
|
} else {
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||||
|
}
|
||||||
|
// Modal functionality
|
||||||
|
const modal = document.getElementById('task-modal');
|
||||||
|
const addTaskButtons = document.querySelectorAll('.add-task-btn');
|
||||||
|
const cancelTaskButton = document.getElementById('cancel-task');
|
||||||
|
const saveTaskButton = document.getElementById('save-task');
|
||||||
|
const taskInput = document.getElementById('task-input');
|
||||||
|
const dueDateInput = document.getElementById('task-due-date');
|
||||||
|
const stakeholdersInput = document.getElementById('stakeholders-input');
|
||||||
|
const notesInput = document.getElementById('notes-input');
|
||||||
|
const deleteModal = document.getElementById('delete-modal');
|
||||||
|
const cancelDeleteBtn = document.getElementById('cancel-delete');
|
||||||
|
const confirmDeleteBtn = document.getElementById('confirm-delete');
|
||||||
|
addTaskButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
targetColumn = document.querySelector(`#${button.dataset.column} .tasks`);
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
taskInput.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function closeModal() {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
taskInput.value = '';
|
||||||
|
dueDateInput.value = '';
|
||||||
|
if (stakeholdersInput) stakeholdersInput.value = '';
|
||||||
|
if (notesInput) notesInput.value = '';
|
||||||
|
targetColumn = null;
|
||||||
|
}
|
||||||
|
cancelTaskButton.addEventListener('click', closeModal);
|
||||||
|
saveTaskButton.addEventListener('click', () => {
|
||||||
|
const taskText = taskInput.value.trim();
|
||||||
|
const dueDate = dueDateInput.value;
|
||||||
|
const stakeholders = stakeholdersInput ? stakeholdersInput.value.trim() : '';
|
||||||
|
const notes = notesInput ? notesInput.value.trim() : '';
|
||||||
|
if (taskText && targetColumn) {
|
||||||
|
const newTask = createNewTask(taskText, dueDate, stakeholders, notes);
|
||||||
|
targetColumn.appendChild(newTask);
|
||||||
|
saveBoardState(); // Persist new task
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Close modal on Escape key
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (!modal.classList.contains('hidden')) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
if (!tagModal.classList.contains('hidden')) {
|
||||||
|
closeTagModal();
|
||||||
|
}
|
||||||
|
if (!deleteModal.classList.contains('hidden')) {
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
taskToDelete = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Delete modal functionality
|
||||||
|
cancelDeleteBtn.addEventListener('click', () => {
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
taskToDelete = null;
|
||||||
|
});
|
||||||
|
confirmDeleteBtn.addEventListener('click', () => {
|
||||||
|
if (taskToDelete) {
|
||||||
|
taskToDelete.remove();
|
||||||
|
saveBoardState(); // Ensure localStorage is updated after deletion
|
||||||
|
}
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
taskToDelete = null;
|
||||||
|
});
|
||||||
|
// Sort functionality
|
||||||
|
const sortButtons = document.querySelectorAll('.sort-btn');
|
||||||
|
sortButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const columnId = button.dataset.columnId;
|
||||||
|
const column = document.getElementById(columnId);
|
||||||
|
const taskContainer = column.querySelector('.tasks');
|
||||||
|
const tasks = Array.from(taskContainer.querySelectorAll('.task'));
|
||||||
|
tasks.sort((a, b) => {
|
||||||
|
const dateA = a.dataset.dueDate ? new Date(a.dataset.dueDate) : null;
|
||||||
|
const dateB = b.dataset.dueDate ? new Date(b.dataset.dueDate) : null;
|
||||||
|
if (dateA && dateB) {
|
||||||
|
return dateA - dateB; // Sort by date ascending
|
||||||
|
}
|
||||||
|
if (dateA) {
|
||||||
|
return -1; // A has a date, B doesn't, so A comes first
|
||||||
|
}
|
||||||
|
if (dateB) {
|
||||||
|
return 1; // B has a date, A doesn't, so B comes first
|
||||||
|
}
|
||||||
|
return 0; // Both have no date, keep original order
|
||||||
|
});
|
||||||
|
// Re-append tasks in sorted order
|
||||||
|
tasks.forEach(task => taskContainer.appendChild(task));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Helper to determine text color based on background
|
||||||
|
function getContrastYIQ(hexcolor){
|
||||||
|
hexcolor = hexcolor.replace("#", "");
|
||||||
|
var r = parseInt(hexcolor.substr(0,2),16);
|
||||||
|
var g = parseInt(hexcolor.substr(2,2),16);
|
||||||
|
var b = parseInt(hexcolor.substr(4,2),16);
|
||||||
|
var yiq = ((r*299)+(g*587)+(b*114))/1000;
|
||||||
|
return (yiq >= 128) ? 'black' : 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag filter dropdown logic
|
||||||
|
const tagFilter = document.getElementById('tag-filter');
|
||||||
|
const tasksContainers = document.querySelectorAll('.tasks');
|
||||||
|
function getAllTags() {
|
||||||
|
const tags = new Set();
|
||||||
|
document.querySelectorAll('.task').forEach(task => {
|
||||||
|
if (task.dataset.tagName) {
|
||||||
|
tags.add(task.dataset.tagName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(tags);
|
||||||
|
}
|
||||||
|
function updateTagOptions() {
|
||||||
|
const select = tagFilter;
|
||||||
|
const currentValue = select.value; // Save current selection
|
||||||
|
// Remove all except All and Unassigned
|
||||||
|
select.querySelectorAll('option:not([value="all"]):not([value="unassigned"])').forEach(opt => opt.remove());
|
||||||
|
getAllTags().forEach(tag => {
|
||||||
|
if (!select.querySelector('option[value="' + tag + '"]')) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = tag;
|
||||||
|
opt.textContent = tag;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
select.value = currentValue; // Restore selection
|
||||||
|
}
|
||||||
|
function filterTasks() {
|
||||||
|
const value = tagFilter.value;
|
||||||
|
document.querySelectorAll('.task').forEach(task => {
|
||||||
|
if (value === 'all') {
|
||||||
|
task.style.display = '';
|
||||||
|
} else if (value === 'unassigned') {
|
||||||
|
if (!task.dataset.tagName) {
|
||||||
|
task.style.display = '';
|
||||||
|
} else {
|
||||||
|
task.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (task.dataset.tagName === value) {
|
||||||
|
task.style.display = '';
|
||||||
|
} else {
|
||||||
|
task.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tagFilter.addEventListener('change', filterTasks);
|
||||||
|
// Update tag options on DOM changes
|
||||||
|
setInterval(updateTagOptions, 1000);
|
||||||
|
|
||||||
|
// Save board locally and remotely
|
||||||
|
function saveBoardState() {
|
||||||
|
const save_json = convertBoardToJSON();
|
||||||
|
saveBoardToLocalStorage(save_json);
|
||||||
|
saveBoardToRemoteStorage(save_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert current board state to JSON
|
||||||
|
function convertBoardToJSON() {
|
||||||
|
const boardData = {
|
||||||
|
tasks: [],
|
||||||
|
tags: {...tags}
|
||||||
|
};
|
||||||
|
document.querySelectorAll('.tasks').forEach(container => {
|
||||||
|
const columnId = container.dataset.columnId;
|
||||||
|
Array.from(container.querySelectorAll('.task')).forEach(task => {
|
||||||
|
boardData.tasks.push({
|
||||||
|
text: task.querySelector('p').textContent,
|
||||||
|
dueDate: task.dataset.dueDate || '',
|
||||||
|
startedDate: task.dataset.startedDate || '',
|
||||||
|
completedDate: task.dataset.completedDate || '',
|
||||||
|
tagName: task.dataset.tagName || '',
|
||||||
|
tagColor: task.dataset.tagColor || '',
|
||||||
|
stakeholders: task.dataset.stakeholders || '',
|
||||||
|
notes: task.dataset.notes || '',
|
||||||
|
column: columnId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return JSON.stringify(boardData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local Storage Persistence
|
||||||
|
function saveBoardToLocalStorage(save_json) {
|
||||||
|
localStorage.setItem('kanbanBoard', save_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteStorage Persistence
|
||||||
|
function saveBoardToRemoteStorage(save_json) {
|
||||||
|
// Placeholder for Post to RESTFUL endpoint, etc...
|
||||||
|
console.log('Saving board to remote storage not yet implemented:', save_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Board From Remote Storage
|
||||||
|
function loadBoardFromRemoteStorage(input_json) {
|
||||||
|
convertJSONToBoard(input_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load board state from local storage
|
||||||
|
function loadBoardFromLocalStorage() {
|
||||||
|
const data = localStorage.getItem('kanbanBoard');
|
||||||
|
if (!data) return;
|
||||||
|
convertJSONToBoard(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JSON back to board state
|
||||||
|
function convertJSONToBoard(json) {
|
||||||
|
let boardData;
|
||||||
|
try {
|
||||||
|
boardData = JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse kanbanBoard JSON:', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Clear all columns
|
||||||
|
document.querySelectorAll('.tasks').forEach(container => container.innerHTML = '');
|
||||||
|
// Restore tags
|
||||||
|
Object.keys(tags).forEach(k => delete tags[k]);
|
||||||
|
Object.assign(tags, boardData.tags);
|
||||||
|
// Restore tasks
|
||||||
|
boardData.tasks.forEach(taskData => {
|
||||||
|
const task = createNewTask(taskData.text, taskData.dueDate, taskData.stakeholders, taskData.notes);
|
||||||
|
if (taskData.startedDate) task.dataset.startedDate = taskData.startedDate;
|
||||||
|
if (taskData.completedDate) task.dataset.completedDate = taskData.completedDate;
|
||||||
|
if (taskData.tagName) {
|
||||||
|
task.dataset.tagName = taskData.tagName;
|
||||||
|
task.dataset.tagColor = taskData.tagColor;
|
||||||
|
const tagCircle = task.querySelector('.tag-circle');
|
||||||
|
if (tagCircle) {
|
||||||
|
tagCircle.style.backgroundColor = taskData.tagColor;
|
||||||
|
tagCircle.classList.remove('border-2', 'border-gray-400');
|
||||||
|
}
|
||||||
|
const tooltipOverlay = task.querySelector('.task-tooltip-overlay');
|
||||||
|
if (tooltipOverlay) {
|
||||||
|
tooltipOverlay.textContent = taskData.tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set started/completed date display
|
||||||
|
const startedDateDisplay = task.querySelector('.started-date-display');
|
||||||
|
if (startedDateDisplay && taskData.startedDate) {
|
||||||
|
startedDateDisplay.textContent = `Started: ${new Date(taskData.startedDate).toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||||
|
startedDateDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
const completedDateDisplay = task.querySelector('.completed-date-display');
|
||||||
|
if (completedDateDisplay && taskData.completedDate) {
|
||||||
|
completedDateDisplay.textContent = `Completed: ${new Date(taskData.completedDate).toLocaleDateString(undefined, { timeZone: 'UTC' })}`;
|
||||||
|
completedDateDisplay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
// Show/hide delete button
|
||||||
|
const deleteBtn = task.querySelector('.delete-task-btn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
if (taskData.column === 'completed') {
|
||||||
|
deleteBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
deleteBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Append to correct column
|
||||||
|
const column = document.querySelector(`.tasks[data-column-id="${taskData.column}"]`);
|
||||||
|
if (column) column.appendChild(task);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call load on startup
|
||||||
|
loadBoardFromLocalStorage();
|
||||||
|
|
||||||
|
// Save after every UI change
|
||||||
|
function saveAfter(fn) {
|
||||||
|
return function(...args) {
|
||||||
|
const result = fn.apply(this, args);
|
||||||
|
saveBoardState();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Patch UI actions to save
|
||||||
|
const originalCreateNewTask = createNewTask;
|
||||||
|
createNewTask = function(...args) {
|
||||||
|
const task = originalCreateNewTask.apply(this, args);
|
||||||
|
saveBoardState();
|
||||||
|
return task;
|
||||||
|
};
|
||||||
|
const originalApplyTagToTask = applyTagToTask;
|
||||||
|
applyTagToTask = function(...args) {
|
||||||
|
originalApplyTagToTask.apply(this, args);
|
||||||
|
saveBoardState();
|
||||||
|
};
|
||||||
|
const originalRemoveTagFromTask = removeTagFromTask;
|
||||||
|
removeTagFromTask = function(...args) {
|
||||||
|
originalRemoveTagFromTask.apply(this, args);
|
||||||
|
saveBoardState();
|
||||||
|
};
|
||||||
|
const originalOpenTagModal = openTagModal;
|
||||||
|
openTagModal = function(...args) {
|
||||||
|
originalOpenTagModal.apply(this, args);
|
||||||
|
saveBoardState();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dark mode toggle logic
|
||||||
|
function setupDarkModeToggle() {
|
||||||
|
const darkToggle = document.getElementById('dark-mode-toggle');
|
||||||
|
const dot = document.querySelector('.dot');
|
||||||
|
const mainBody = document.getElementById('main-body');
|
||||||
|
|
||||||
|
// Check for saved preference or system preference
|
||||||
|
const savedMode = localStorage.getItem('darkMode');
|
||||||
|
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
if (savedMode === 'true' || (!savedMode && systemPrefersDark)) {
|
||||||
|
darkToggle.checked = true;
|
||||||
|
mainBody.classList.add('dark');
|
||||||
|
if (dot) dot.style.transform = 'translateX(16px)';
|
||||||
|
} else {
|
||||||
|
darkToggle.checked = false;
|
||||||
|
mainBody.classList.remove('dark');
|
||||||
|
if (dot) dot.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle toggle changes
|
||||||
|
darkToggle.addEventListener('change', function() {
|
||||||
|
if (darkToggle.checked) {
|
||||||
|
mainBody.classList.add('dark');
|
||||||
|
localStorage.setItem('darkMode', 'true');
|
||||||
|
if (dot) dot.style.transform = 'translateX(16px)';
|
||||||
|
} else {
|
||||||
|
mainBody.classList.remove('dark');
|
||||||
|
localStorage.setItem('darkMode', 'false');
|
||||||
|
if (dot) dot.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for system preference changes
|
||||||
|
if (window.matchMedia) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||||
|
// Only auto-switch if user hasn't manually set a preference
|
||||||
|
if (!localStorage.getItem('darkMode')) {
|
||||||
|
if (e.matches) {
|
||||||
|
darkToggle.checked = true;
|
||||||
|
mainBody.classList.add('dark');
|
||||||
|
if (dot) dot.style.transform = 'translateX(16px)';
|
||||||
|
} else {
|
||||||
|
darkToggle.checked = false;
|
||||||
|
mainBody.classList.remove('dark');
|
||||||
|
if (dot) dot.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize dark mode
|
||||||
|
setupDarkModeToggle();
|
||||||
|
});
|
||||||
BIN
tools/kanban/src/kanban.ico
Normal file
BIN
tools/kanban/src/kanban.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
162
tools/kanban/src/styles.css
Normal file
162
tools/kanban/src/styles.css
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
body.dark {
|
||||||
|
background-color: #18181b;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.kanban-column {
|
||||||
|
user-select: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
body.dark .kanban-column {
|
||||||
|
background-color: #23232a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #222;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
body.dark .task {
|
||||||
|
background-color: #262632;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.tasks::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.tasks::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
body.dark .tasks::-webkit-scrollbar-track {
|
||||||
|
background: #23232a;
|
||||||
|
}
|
||||||
|
.tasks::-webkit-scrollbar-thumb {
|
||||||
|
background: #94a3b8;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
body.dark .tasks::-webkit-scrollbar-thumb {
|
||||||
|
background: #444459;
|
||||||
|
}
|
||||||
|
.tasks::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
|
body.dark .tasks::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6366f1;
|
||||||
|
}
|
||||||
|
.tag-tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 120px;
|
||||||
|
background-color: #555;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 125%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -60px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
body.dark .tag-tooltip {
|
||||||
|
background-color: #222;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.tag-tooltip::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #555 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
body.dark .tag-tooltip::after {
|
||||||
|
border-color: #222 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.tag-circle:hover .tag-tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
#dark-mode-toggle:checked + span .dot {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
#dark-mode-toggle:not(:checked) + span .dot {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
body.dark h1,
|
||||||
|
body.dark h2,
|
||||||
|
body.dark h3,
|
||||||
|
body.dark h4,
|
||||||
|
body.dark h5,
|
||||||
|
body.dark h6,
|
||||||
|
body.dark p,
|
||||||
|
body.dark .text-gray-700,
|
||||||
|
body.dark .text-gray-900,
|
||||||
|
body.dark .text-gray-600,
|
||||||
|
body.dark .font-medium,
|
||||||
|
body.dark .font-bold {
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
body.dark .bg-white {
|
||||||
|
background-color: #23232a !important;
|
||||||
|
}
|
||||||
|
body.dark .bg-gray-50 {
|
||||||
|
background-color: #262632 !important;
|
||||||
|
}
|
||||||
|
body.dark .border-gray-400 {
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
}
|
||||||
|
body.dark select#tag-filter {
|
||||||
|
background-color: #23232a !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
}
|
||||||
|
body.dark select#tag-filter option {
|
||||||
|
background-color: #23232a !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
body.dark .modal,
|
||||||
|
body.dark #delete-modal > div,
|
||||||
|
body.dark #tag-delete-modal > div {
|
||||||
|
background-color: #23232a !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
body.dark #delete-modal h3,
|
||||||
|
body.dark #tag-delete-modal h3,
|
||||||
|
body.dark #delete-modal p,
|
||||||
|
body.dark #tag-delete-modal p {
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
body.dark #delete-modal button,
|
||||||
|
body.dark #tag-delete-modal button {
|
||||||
|
background-color: #444459 !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
}
|
||||||
|
body.dark #delete-modal button:hover,
|
||||||
|
body.dark #tag-delete-modal button:hover {
|
||||||
|
background-color: #6366f1 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
body.dark input[type="text"],
|
||||||
|
body.dark input[type="date"],
|
||||||
|
body.dark textarea {
|
||||||
|
background-color: #23232a !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
}
|
||||||
|
body.dark input[type="text"]::placeholder,
|
||||||
|
body.dark input[type="date"]::placeholder,
|
||||||
|
body.dark textarea::placeholder {
|
||||||
|
color: #a1a1aa !important;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user