Added some WIP assets for the Local/World/UI views and the cover art.

This commit is contained in:
2026-01-07 05:45:05 -05:00
parent 8186f2466c
commit 357189e12e
18 changed files with 1339 additions and 0 deletions

20
package.json Normal file
View 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

Binary file not shown.

BIN
src/Marketing/game_cover.png LFS Normal file

Binary file not shown.

BIN
src/UI/gem.psd LFS Normal file

Binary file not shown.

BIN
src/UI/item_pickaxe.psd LFS Normal file

Binary file not shown.

BIN
src/UI/item_wand_01.psd LFS Normal file

Binary file not shown.

BIN
src/UI/moon.psd LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/UI/terrain_forest.psd LFS Normal file

Binary file not shown.

BIN
src/UI/terrain_mountain.psd LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
src/WorldView/forest.vox Normal file

Binary file not shown.

BIN
src/WorldView/graveyard.vox Normal file

Binary file not shown.

Binary file not shown.

217
tools/kanban/kanban.html Normal file
View 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">&times;</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>

View 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>&times;</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

162
tools/kanban/src/styles.css Normal file
View 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;
}