diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4b331ee
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/Marketing/PSD/game_cover.psd b/src/Marketing/PSD/game_cover.psd
new file mode 100644
index 0000000..505e917
--- /dev/null
+++ b/src/Marketing/PSD/game_cover.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:743124bc57aa865b85a8eef158b37bf6be3cb3b618542539ea074fa37d2727ca
+size 1889040
diff --git a/src/Marketing/game_cover.png b/src/Marketing/game_cover.png
new file mode 100644
index 0000000..399d897
--- /dev/null
+++ b/src/Marketing/game_cover.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d45edae4c905c1a02d630e93f860a7f7aabb68cfcb1ad493b646a64b0ea5e87d
+size 464392
diff --git a/src/UI/gem.psd b/src/UI/gem.psd
new file mode 100644
index 0000000..8a170ca
--- /dev/null
+++ b/src/UI/gem.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c6920b37ed4a03c5d529de815c74383b9190acb7367f433f25766e2006327ebc
+size 4396
diff --git a/src/UI/item_pickaxe.psd b/src/UI/item_pickaxe.psd
new file mode 100644
index 0000000..dcf095f
--- /dev/null
+++ b/src/UI/item_pickaxe.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84d580342c8cc0d72e470dadf43e2d4d352dcbd3f49c8832a71899ceeb69add3
+size 4176
diff --git a/src/UI/item_wand_01.psd b/src/UI/item_wand_01.psd
new file mode 100644
index 0000000..8b5b43d
--- /dev/null
+++ b/src/UI/item_wand_01.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1bb20f9dfdcbb5f95899f10ab456d0144022b2dcbe7c5a7a13772c2c6315a282
+size 5048
diff --git a/src/UI/moon.psd b/src/UI/moon.psd
new file mode 100644
index 0000000..8c9cfbf
--- /dev/null
+++ b/src/UI/moon.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c3c83ffcc23dbaa86220f93b27a33a8c4b6f24977ce37d9739d4d18dc12b9286
+size 4724
diff --git a/src/UI/resource_wood.aseprite b/src/UI/resource_wood.aseprite
new file mode 100644
index 0000000..7c91070
Binary files /dev/null and b/src/UI/resource_wood.aseprite differ
diff --git a/src/UI/terrain_forest.psd b/src/UI/terrain_forest.psd
new file mode 100644
index 0000000..6498dd4
--- /dev/null
+++ b/src/UI/terrain_forest.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df28dc2aa3b48c40ed8ac8bd556763c97c8fbc6e690fd2806c9cbf098f01ef34
+size 9151
diff --git a/src/UI/terrain_mountain.psd b/src/UI/terrain_mountain.psd
new file mode 100644
index 0000000..6b745b0
--- /dev/null
+++ b/src/UI/terrain_mountain.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0dc980378268a66ef1c0f0ccff728984b6066d29bc3652311821d4fa94107771
+size 7488
diff --git a/src/WorldView/campfire_unlit.vox b/src/WorldView/campfire_unlit.vox
new file mode 100644
index 0000000..6a176fc
Binary files /dev/null and b/src/WorldView/campfire_unlit.vox differ
diff --git a/src/WorldView/forest.vox b/src/WorldView/forest.vox
new file mode 100644
index 0000000..d87a9f3
Binary files /dev/null and b/src/WorldView/forest.vox differ
diff --git a/src/WorldView/graveyard.vox b/src/WorldView/graveyard.vox
new file mode 100644
index 0000000..bf86ce5
Binary files /dev/null and b/src/WorldView/graveyard.vox differ
diff --git a/src/WorldView/nine-tile-track.vox b/src/WorldView/nine-tile-track.vox
new file mode 100644
index 0000000..cb7803d
Binary files /dev/null and b/src/WorldView/nine-tile-track.vox differ
diff --git a/tools/kanban/kanban.html b/tools/kanban/kanban.html
new file mode 100644
index 0000000..a01432a
--- /dev/null
+++ b/tools/kanban/kanban.html
@@ -0,0 +1,217 @@
+
+
+
+
+
+ Simple Kanban Board
+
+
+
+
+
+
+
+
+
+
+
+
+ Kanban Board
+ Drag and drop tasks to organize your workflow.
+
+
+
+
+
+ Add Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Add New Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Deletion
+
Are you sure you want to delete this task?
+
+
+
+
+
+
+
+
+
+
+
+
Task Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/kanban/src/SimpleKanban.js b/tools/kanban/src/SimpleKanban.js
new file mode 100644
index 0000000..db5df30
--- /dev/null
+++ b/tools/kanban/src/SimpleKanban.js
@@ -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 = `
+
+
Delete Tag
+
Are you sure you want to delete the tag ? This will remove the tag from all tasks.
+
+
+
+
+
+ `;
+ 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 = `
+
+
Delete Tag
+
Are you sure you want to delete the tag ? This will remove the tag from all tasks.
+
+
+
+
+
+ `;
+ 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 = '×';
+ 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, '$1');
+ // Linkify URLs
+ text = text.replace(/(https?:\/\/[^\s]+)/g, '$1');
+ text = text.replace(/(www\.[^\s]+)/g, '$1');
+ 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 = ``;
+ 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();
+});
diff --git a/tools/kanban/src/kanban.ico b/tools/kanban/src/kanban.ico
new file mode 100644
index 0000000..3ea33f8
Binary files /dev/null and b/tools/kanban/src/kanban.ico differ
diff --git a/tools/kanban/src/styles.css b/tools/kanban/src/styles.css
new file mode 100644
index 0000000..64a8fdb
--- /dev/null
+++ b/tools/kanban/src/styles.css
@@ -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;
+}