commit fcc11fdc18d3b6b1275cb5c5b193641745b397a2 Author: Interitio Date: Sat Nov 1 11:12:02 2025 +1000 Initial commit of hyperfocus scrolling list. diff --git a/configs.js b/configs.js new file mode 100644 index 0000000..1e36b67 --- /dev/null +++ b/configs.js @@ -0,0 +1,383 @@ +const configs = (function () { + "use strict"; + + // settings + const port = 6968; // must be a 4 digit number + const showDoneTasks = true; // true or false + const showTasksNumber = true; // true or false + const crossTasksOnDone = true; // true or false + const showCheckBox = true; // true or false + const reverseOrder = false; // true or false + + // TOGGLE TRUE WILL RESET TASKS + const enableTests = false; // true or false + // if dummy tasks are still visible, do !clearall to clear all tasks + + // fonts + const headerFontFamily = "Fredoka One"; // supports all google fonts - https://fonts.google.com/ + const bodyFontFamily = "Nunito"; // supports all google fonts - https://fonts.google.com/ + + // scroll + const pixelsPerSecond = 40; // must be a number + + const gapBetweenScrolls = 10; // integer only + + // task list + const taskListBackgroundColor = "#ff0000"; // hex only + const taskListBackgroundOpacity = 0; // must be between 0 and 1 + + const taskListBorderColor = "#AF22A1"; // hex or name + const taskListBorderWidth = "0px"; // must have px at the end + const taskListBorderRadius = "5px"; // must have px at the end + + const taskListPadding = "0px"; // must have px at the end + + // header + const headerHeight = "60px"; // must have px at the end + const headerBackgroundColor = "#321"; // hex only + const headerBackgroundOpacity = 0.9; // must be between 0 and 1 + + const headerBorderColor = "brown"; // hex or name + const headerBorderWidth = "2px"; // must have px at the end + const headerBorderRadius = "10px"; // must have px at the end + + const headerFontSize = "30px"; // must have px at the end + const headerFontColor = "white"; // hex or name + + const headerPadding = "10px"; // must have px at the end + const tasksNumberFontSize = "30px"; // must have px at the end + + // body + const bodyBackgroundColor = "#00ff00"; // hex only + const bodyBackgroundOpacity = 0; // must be between 0 and 1 + + const bodyBorderColor = "white"; // hex or name + const bodyBorderWidth = "0px"; // must have px at the end + const bodyBorderRadius = "0px"; // must have px at the end + + const bodyVerticalPadding = "5px"; // must have px at the end + const bodyHorizontalPadding = "5px"; // must have px at the end + + // task (individual tasks) + const numberOfLines = 1; // number of lines for the task + const usernameColor = "white"; // hex or name, "" for twitch username color + const taskDirection = "row"; // row or column + + const usernameMaxWidth = "100%"; // must have px or % at the end + + const taskBackgroundColor = "#000"; // hex only + const taskBackgroundOpacity = 0.8; // must be between 0 and 1 + + const taskFontSize = "25px"; // must have px at the end + const taskFontColor = "white"; // hex or name + + const taskBorderColor = "black"; // hex or name + const taskBorderWidth = "0px"; // must have px at the end + const taskBorderRadius = "5px"; // must have px at the end + + const taskMarginLeft = "0px"; // must have px at the end + const taskMarginBottom = "5px"; // must have px at the end + const taskPadding = "10px"; // must have px at the end + + const taskMaxWidth = "100%"; // must have px or % at the end + + // done task + const doneTaskBackgroundColor = "#000"; // hex only + const doneTaskBackgroundOpacity = 0.5; // must be between 0 and 1 + + const doneTaskFontColor = "grey"; // hex or name + + // checkbox - if enabled + const checkBoxSize = "20px"; // must have px at the end + const checkBoxBackgroundColor = "#000"; // hex only + const checkBoxBackgroundOpacity = 0; // must be between 0 and 1 + + const checkBoxBorderColor = "white"; // hex or name + const checkBoxBorderWidth = "1px"; // must have px at the end + const checkBoxBorderRadius = "3px"; // must have px at the end + + const checkBoxMarginTop = "6px"; // must have px at the end + const checkBoxMarginLeft = "2px"; // must have px at the end + const checkBoxMarginRight = "2px"; // must have px at the end + + const tickCharacter = "'✔'"; // any character, must be in single quotes + const tickSize = "18px"; // must have px at the end + const tickColor = "white"; // hex or name + const tickTranslateY = "4px"; // must have px at the end + + // bullet point - if enabled + const bulletPointCharacter = "•"; // any character + const bulletPointSize = "15px"; // must have px at the end + const bulletPointColor = "white"; // hex or name + + const bulletPointMarginTop = "0px"; // must have px at the end + const bulletPointMarginLeft = "5px"; // must have px at the end + const bulletPointMarginRight = "5px"; // must have px at the end + + // colon + const colonMarginLeft = "0px"; // must have px at the end + const colonMarginRight = "5px"; // must have px at the end + + // Add task commands - please add commands in the exact format + const addTaskCommands = [ + "!task", + "!add", + "!todo", + "!taska", + "!taskadd", + "!addtask", + "!atask", + "!a", + ]; + + // Delete task commands - please add commands in the exact format + const deleteTaskCommands = [ + "!remove", + "!delete", + "!taskd", + "!taskdel", + "!taskdelete", + "!deltask", + "!deletetask", + "!taskr", + "!taskremove", + "!rtask", + "!removetask", + "!r", + ]; + + // Edit task commands - please add commands in the exact format + const editTaskCommands = [ + "!edit", + "!rename", + "!taskedit", + "!edittask", + "!taske", + "!etask", + "!e", + ]; + + // Finish task commands - please add commands in the exact format + const finishTaskCommands = [ + "!done", + "!donetask", + "!taskdone", + "!finished", + "!taskf", + "!taskfinish", + "!ftask", + "!finishtask", + "!taskd", + "!dtask", + "!finish", + "!f", + ]; + + // Next task commands - please add commands in the exact format + const nextTaskCommands = ["!next", "!nexttask", "!taskn", "!n"]; + + // Check task commands - please add commands in the exact format + const checkCommands = [ + "!mytask", + "!check", + "!taskc", + "!taskcheck", + "!ctask", + "!checktask", + "!c", + ]; + + // Help commands - please add commands in the exact format + const helpCommands = [ + "!taskhelp", + "!tasks", + "!taskh", + "!htask", + "!helptask", + ]; + + // Admin delete - please add commands following the exact format + const adminDeleteCommands = [ + "!adel", + "!admindelete", + "!taskadel", + "!adelete", + ]; + + // Admin clear done - please add commands following the exact format + const adminClearDoneCommands = [ + "!cleardone", + "!acleardone", + "!admincleardone", + ]; + + const adminClearAllCommands = [ + "!clearall", + "!allclear", + "!adminclearall", + "!adminallclear", + "!aclearall", + "!aclear", + ]; + + // Responses + const taskAdded = 'The task "{task}" has been added, {user}!'; + const noTaskAdded = + "Looks like you already have a task up there {user}, use !check to check your last task!"; + const noTaskContent = "Try using !add the-task-you-are-working-on {user}"; + const noTaskToEdit = "No task to edit {user}"; + const taskEdited = 'Task edited to "{task}" {user}'; + const taskDeleted = 'Task "{task}" has been deleted, {user}'; + const taskNext = + "Good job on finishing the task '{oldTask}'! Now moving onto '{newTask}', {user}!"; + const adminDeleteTasks = "All of the user's tasks have been deleted"; + const taskFinished = 'Good job on finishing "{task}" {user}!'; + const taskCheck = '{user} your current task is: "{task}"'; + const taskCheckUser = `{user} {user2}'s current task is: "{task}"`; + const noTask = "Looks like you don't have a task up there {user}"; + const noTaskA = "Looks like there is no task from that user there {user}"; + const notMod = "Permission denied, {user}; Mods only"; + const clearedAll = "All tasks have been cleared"; + const clearedDone = "All finished tasks have been cleared"; + const nextNoContent = "Try using !next the-task-you-want-to-do-next {user}"; + const help = `{user} Use the following commands to help you out - !task !remove !edit !done. For more commands, click here: https://github.com/liyunze-coding/Rython-Task-Streamer-Bot#commands`; + + const additionalCommands = { + "!botcred": + "{user} Ryan is the creator of this bot, check out his Twitch at https://www.twitch.tv/RythonDev", + }; + + const titles = [ + "!botcred", + "!taskadd", + "!taskdone", + "!taskedit", + "!taskdel", + "!taskhelp", + ]; + + // Other + const styles = { + headerFontFamily, + bodyFontFamily, + pixelsPerSecond, + taskListBackgroundColor, + taskListBackgroundOpacity, + taskListBorderColor, + taskListBorderWidth, + taskListBorderRadius, + taskListPadding, + gapBetweenScrolls, + numberOfLines, + headerFontColor, + headerBorderColor, + headerBorderWidth, + headerBorderRadius, + headerHeight, + headerFontSize, + headerBackgroundColor, + headerBackgroundOpacity, + headerPadding, + tasksNumberFontSize, + bodyBorderColor, + bodyBorderWidth, + bodyBorderRadius, + bodyBackgroundColor, + bodyBackgroundOpacity, + bodyVerticalPadding, + bodyHorizontalPadding, + usernameColor, + usernameMaxWidth, + taskFontSize, + taskFontColor, + taskBackgroundColor, + taskBackgroundOpacity, + taskBorderRadius, + taskBorderColor, + taskBorderWidth, + taskMarginLeft, + taskMarginBottom, + taskPadding, + taskDirection, + taskMaxWidth, + doneTaskBackgroundColor, + doneTaskBackgroundOpacity, + doneTaskFontColor, + checkBoxSize, + checkBoxBorderColor, + checkBoxBorderRadius, + checkBoxBorderWidth, + checkBoxMarginTop, + checkBoxMarginLeft, + checkBoxMarginRight, + checkBoxBackgroundColor, + checkBoxBackgroundOpacity, + tickCharacter, + tickColor, + tickSize, + tickTranslateY, + bulletPointCharacter, + bulletPointColor, + bulletPointSize, + bulletPointMarginRight, + bulletPointMarginLeft, + bulletPointMarginTop, + colonMarginRight, + colonMarginLeft, + }; + + const commands = { + addTaskCommands, + deleteTaskCommands, + editTaskCommands, + finishTaskCommands, + nextTaskCommands, + helpCommands, + checkCommands, + adminDeleteCommands, + adminClearDoneCommands, + adminClearAllCommands, + additionalCommands, + }; + + const responses = { + taskAdded, + noTaskAdded, + noTaskContent, + taskDeleted, + taskEdited, + noTaskToEdit, + taskFinished, + taskNext, + taskCheck, + taskCheckUser, + noTask, + noTaskA, + notMod, + help, + adminDeleteTasks, + clearedAll, + clearedDone, + nextNoContent, + }; + + const settings = { + port, + enableTests, + showDoneTasks, + showTasksNumber, + crossTasksOnDone, + showCheckBox, + reverseOrder, + }; + + let module = { + styles, + commands, + responses, + settings, + titles, + }; + + return module; +})(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..6e5c5cd --- /dev/null +++ b/index.html @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + Current Tasks + + +
+
+
!hyperfocused
+ +
+
+
+ + + + + +
+
+
+
+ + diff --git a/main.js b/main.js new file mode 100644 index 0000000..ff1ec81 --- /dev/null +++ b/main.js @@ -0,0 +1,467 @@ +let scrolling = false; +let primaryAnimation, secondaryAnimation; +let focusing = {}; + +window.addEventListener("DOMContentLoaded", () => { + importStyles(); + + const websocket = new ReconnectingWebSocket("ws://adamwalsh.name:9525"); + setInterval(updateTimers, 1000); + communicate(websocket); +}); + + + +class ReconnectingWebSocket { + constructor(url) { + this.url = url; + this.ws = null; + this.attempt = 0; + this.connect(); + + this.closing = false; + this.onopen = null; + this.onmessage = null; + this.onclose = null; + this.onerror = null; + } + + connect() { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.attempt = 0; + if (this.onopen != null) { + this.onopen(); + } + }; + + this.ws.onmessage = (msg) => { + console.log('Received:', msg.data); + if (this.onmessage != null) { + this.onmessage(msg); + } + }; + + this.ws.onclose = () => { + if (this.closing) { + console.log('Closing Websocket.') + } { + console.warn('WebSocket closed. Reconnecting...'); + this.reconnect(); + } + }; + + this.ws.onerror = (err) => { + console.error('WebSocket error:', err); + if (this.onerror != null) { + this.onerror(); + } + this.close(); + }; + } + + reconnect() { + const delay = this.getBackoffDelay(this.attempt); + console.log(`Reconnecting in ${delay}ms`); + + setTimeout(() => { + this.attempt++; + this.connect(); + }, delay); + } + + close() { + this.closing = true; + this.ws.close(); + if (this.onclose != null) { + this.onclose(); + } + } + + getBackoffDelay(attempt) { + const base = 500; // 0.5 second + const max = 30000; // 30 seconds + const jitter = Math.random() * 1000; + return Math.min(base * 2 ** attempt + jitter, max); + } + + send(data) { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(data); + } else { + console.warn('WebSocket not connected'); + } + } +} + + + + +function communicate(websocket) { + console.log("Communicating"); + const params = new URLSearchParams(window.location.search); + + websocket.onopen = () => { + websocket.send( + JSON.stringify( + { + type: "init", + channel: "HyperFocus", + community: params.get('community') + } + ) + ); + }; + + websocket.onmessage = ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "DO": + let args = event.args; + + // Call the specified method + switch (event.method) { + case "patchFocus": + setFocus(focusing, args.userid, args.user_name, true, args.ends_at); + break; + case "delFocus": + delFocus(focusing, args.userid); + break; + case "putFocus": + clearFocus(focusing); + args.forEach(focuser => { + setFocus(focusing, focuser.userid, focuser.user_name, true, focuser.ends_at); + }); + break; + default: + throw new Error(`Unsupported method requested: ${event.method}.`) + } + if (!scrolling) { + renderList(); + } + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }; +} + + +function setFocus(focusing, userid, name, hyper, end_at) { + console.log(`Setting Focus ${userid}, ${name}, ${hyper}, ${end_at}`); + focusing[userid] = { + name: name, + hyper: hyper, + end_at: new Date(end_at) + }; +} + +function delFocus(focusing, userid) { + console.log(`Del Focus ${userid}`); + if (focusing.hasOwnProperty(userid)) { + delete focusing[userid]; + } +} + +function clearFocus(tasks) { + for (var prop in focusing) { + delete focusing[prop]; + } +} + +function renderList() { + let containers = document.querySelectorAll(".task-container") + let now = Date.now(); + let infos = Object.values(focusing); + infos.sort((info1, info2) => (info2.end_at - info1.end_at)); + console.log(infos); + console.log(focusing); + + containers.forEach(function (tasklist) { + tasklist.innerHTML = ""; + + for (const info of infos) { + if (info.end_at < now) { + continue; + } + + let newTask = document.createElement("div"); + newTask.className = "task-div"; + + let newFocus = document.createElement("div"); + newFocus.className = "task-div"; + + let usernameTask = document.createElement("div"); + usernameTask.className = "username-div"; + + let usernameDiv = document.createElement("div"); + usernameDiv.className = "username"; + usernameDiv.innerText = info.name; + usernameDiv.style.color = "pink"; + usernameTask.appendChild(usernameDiv); + + newTask.appendChild(usernameTask); + + let colon = document.createElement("div"); + colon.className = "colon"; + colon.innerText = ":"; + usernameTask.appendChild(colon); + + let timerDiv = document.createElement("div"); + timerDiv.dataset.endat = info.end_at.toISOString(); + timerDiv.className = "task-timer-running"; + // timerDiv.style.color = 'grey'; + timerDiv.innerText = formatDur(info.end_at - Date.now()); + + newTask.appendChild(timerDiv); + tasklist.appendChild(newTask); + + } + + }); + animate(); + +} + +function formatDur(duration) { + var dur_seconds = Math.floor(duration / 1000); + var seconds = dur_seconds % 60; + dur_seconds = dur_seconds - seconds; + var minutes = Math.floor(dur_seconds / 60); + var hours = Math.floor(minutes / 60); + minutes = minutes - 60 * hours; + + if (hours > 0) { + return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + } else { + return String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + } +} + +function updateTimers() { + let timers = document.querySelectorAll('.task-timer-running'); + timers.forEach(function (timer) { + if (new Date(timer.dataset.endat) < Date.now()) { + if (!scrolling) { + renderList(); + } + } else { + timer.innerText = formatDur(new Date(timer.dataset.endat) - Date.now()); + } + }); +} + +/** + * This function animates the task list by scrolling it up and down once. + * It first calculates the heights of the task container and task wrapper. + * If the task container height is greater than the task wrapper height and the task list is not currently scrolling, it calculates the final height and duration of the animation, + * creates the keyframes and options for the animation, and then creates and plays the animation. + * If the task list is currently scrolling, it hides the secondary task list and cancels the animation. + */ +async function animate() { + // task container height + let taskContainer = document.querySelector(".task-container"); + let taskContainerHeight = taskContainer.scrollHeight; + + let taskWrapper = document.querySelector(".task-wrapper"); + let taskWrapperHeight = taskWrapper.clientHeight; + + // scroll task wrapper up and down once + if (taskContainerHeight > taskWrapperHeight && !scrolling) { + let secondaryElement = document.querySelector(".secondary"); + secondaryElement.style.display = "flex"; + + let finalHeight = + taskContainerHeight + configs.styles.gapBetweenScrolls; + let duration = (finalHeight / configs.styles.pixelsPerSecond) * 1000; + + // keyframes object in css scroll + let primaryKeyFrames = [ + { transform: `translateY(0)` }, + { transform: `translateY(-${finalHeight}px)` }, + ]; + + let secondaryKeyFrames = [ + { transform: `translateY(${finalHeight}px)` }, + { transform: `translateY(0)` }, + ]; + + let options = { + duration: duration, + iterations: 1, + easing: "linear", + }; + + // create animation object and play it + primaryAnimation = document + .querySelector(".primary") + .animate(primaryKeyFrames, options); + + secondaryAnimation = document + .querySelector(".secondary") + .animate(secondaryKeyFrames, options); + + primaryAnimation.play(); + secondaryAnimation.play(); + + // wait for animation to finish + scrolling = true; + + addAnimationListeners(); + } else if (!scrolling) { + document.querySelector(".secondary").style.display = "none"; + + // cancel animations + cancelAnimation(); + } +} + +/** + * This function adds event listeners to the primary animation. + * If the primary animation exists, it adds 'finish' and 'cancel' event listeners that both trigger the animationFinished function. + */ +function addAnimationListeners() { + if (primaryAnimation) { + primaryAnimation.addEventListener("finish", animationFinished); + primaryAnimation.addEventListener("cancel", animationFinished); + } +} + +/** + * This function is triggered when the primary animation finishes or is cancelled. + * It sets the scrolling flag to false, re-renders the task list, and then starts the animation again. + */ +function animationFinished() { + scrolling = false; + renderList(); + animate(); +} + +/** + * This function cancels the primary and secondary animations if they exist. + * It also sets the scrolling flag to false. + */ +function cancelAnimation() { + if (primaryAnimation) { + primaryAnimation.cancel(); + } + if (secondaryAnimation) { + secondaryAnimation.cancel(); + } + scrolling = false; +} + +/** + * This function converts a hex color code to an RGB color code. + * It first checks if the hex code includes the '#' character and removes it if present. + * Then, it checks the length of the hex code. + * If it's 3, it duplicates each character to create a 6-digit hex code. + * If it's 6, it uses the hex code as is. + * Finally, it converts each pair of hex digits to decimal to get the RGB values and returns them as a string. + * + * @param {string} hex - The hex color code to be converted. + * @returns {string} The RGB color code. + */ +function hexToRgb(hex) { + // remove # if present + if (hex[0] === "#") { + hex = hex.slice(1); + } + + let r = 0, + g = 0, + b = 0; + + if (hex.length == 3) { + // 3 digits + r = "0x" + hex[0] + hex[0]; + g = "0x" + hex[1] + hex[1]; + b = "0x" + hex[2] + hex[2]; + } else if (hex.length == 6) { + // 6 digits + r = "0x" + hex[0] + hex[1]; + g = "0x" + hex[2] + hex[3]; + b = "0x" + hex[4] + hex[5]; + } + + // interger value of rgb + r = +r; + g = +g; + b = +b; + + return `${r}, ${g}, ${b}`; +} + +/** + * + * @param {string} font + */ +function loadGoogleFont(font) { + WebFont.load({ + google: { + families: [font], + }, + }); +} + +/** + * convert taskListBorderColor to task-list-border-color + */ +function convertToCSSVar(name) { + let cssVar = name.replace(/([A-Z])/g, "-$1").toLowerCase(); + return `--${cssVar}`; +} + +/** + * This function imports styles from the configs object and applies them to the document. + * It loads the Google fonts specified in the configs, applies all styles that do not include 'Background' in their keys, + * and applies background colors and opacities for specific elements. + * If the 'showTasksNumber' setting in configs is false, it hides the task count element. + */ +function importStyles() { + const styles = configs.styles; + + loadGoogleFont(styles.headerFontFamily); + loadGoogleFont(styles.bodyFontFamily); + + const stylesToImport = Object.keys(styles).filter((style) => { + return !style.includes("Background"); + }); + + const backgroundStyles = [ + "taskList", + "header", + "body", + "task", + "checkBox", + "doneTask", + ]; + + stylesToImport.forEach((style) => { + document.documentElement.style.setProperty( + convertToCSSVar(style), + styles[style] + ); + }); + + // loop through backgroundstyles + backgroundStyles.forEach((style) => { + // get background color and opacity + let backgroundColor = styles[`${style}BackgroundColor`]; + let backgroundOpacity = styles[`${style}BackgroundOpacity`]; + + let cssStyle = convertToCSSVar(style); + + // set background color + document.documentElement.style.setProperty( + `${cssStyle}-background-color`, + `rgba(${hexToRgb(backgroundColor)}, ${backgroundOpacity})` + ); + }); + + if (!configs.settings.showTasksNumber) { + document.getElementById("task-count").style.display = "none"; + } +} diff --git a/tasklist.css b/tasklist.css new file mode 100644 index 0000000..46bf2c8 --- /dev/null +++ b/tasklist.css @@ -0,0 +1,339 @@ +/* + +CUSTOMIZATIONS TO ADD: + +- add a way to change the borders +- add a way to change the scroll speed + +*/ + +:root { + /* fonts */ + --header-font-family: sans-serif; + --body-font-family: sans-serif; + + /* ENTIRE task list */ + --task-list-width: 100%; + --task-list-height: 100vh; + --task-list-background-color: rgba(0, 0, 0, 0.5); + + --task-list-border-color: black; + --task-list-border-width: 1px; + --task-list-border-radius: 10px; + + --task-list-padding: 10px; + + /* header */ + --header-border-color: black; + --header-border-width: 1px; + --header-border-radius: 0px; + --header-height: 30px; + --header-font-size: 20px; + --header-font-color: white; + --header-background-color: rgba(0, 0, 0, 0.5); + --header-padding: 10px; + + /* tasks number */ + --tasks-number-font-size: 20px; + + /* body */ + --body-border-color: black; + --body-border-width: 1px; + --body-border-radius: 0px; + --body-background-color: rgba(0, 0, 0, 0.5); + --body-vertical-padding: 10px; + --body-horizontal-padding: 10px; + + /* task */ + --task-font-size: 16px; + --task-font-color: white; + --task-background-color: rgba(0, 0, 0, 0.5); + --task-border-color: black; + --task-border-width: 0px; + --task-border-radius: 0px; + --task-margin-bottom: 10px; + --task-padding: 10px; + --task-max-width: 70%; + --task-direction: row; + + --number-of-lines: 1; /* number of lines to show */ + + /* check box */ + --check-box-size: 15px; + --check-box-color: white; + --check-box-margin-top: 3px; + --check-box-margin-left: 2px; + --check-box-margin-right: 2px; + --check-box-border-width: 1px; + --check-box-border-radius: 0px; + --check-box-border-color: white; + --tick-character: "✓"; + --tick-color: white; + --tick-size: 10px; + --tick-translate-y: 2px; + + /* bullet point */ + --bullet-point-color: white; + --bullet-point-size: 15px; + --bullet-point-margin-top: 3px; + --bullet-point-margin-left: 2px; + --bullet-point-margin-right: 2px; + + /* done task */ + --done-task-font-color: gray; + --done-task-background-color: rgba(0, 0, 0, 0.5); + + /* username */ + --username-color: white; + --username-max-width: 10px; + + /* colon */ + --colon-margin-right: 2px; + --colon-margin-left: 2px; +} + +*, +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-size: 16px; + + background-color: gray; + color: white; +} + +#main-container { + display: flex; + flex-direction: column; + width: var(--task-list-width); + height: var(--task-list-height); + border: solid; + border-color: var(--task-list-border-color); + border-width: var(--task-list-border-width); + border-radius: var(--task-list-border-radius); + padding: var(--task-list-padding); + + background: var(--task-list-background-color); +} + +.header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + width: 100%; + height: var(--header-height); + + font-family: var(--header-font-family); + + border: solid; + border-color: var(--header-border-color); + border-width: var(--header-border-width); + border-radius: var(--header-border-radius); + + padding: var(--header-padding); + + background: var(--header-background-color); + color: var(--header-font-color); +} + +#title { + font-weight: normal; + font-size: var(--header-font-size); +} + +#task-count { + font-size: var(--tasks-number-font-size); +} + +.task-wrapper { + background: var(--body-background-color); + font-family: var(--body-font-family); + + border: solid; + border-color: var(--body-border-color); + border-width: var(--body-border-width); + border-radius: var(--body-border-radius); + + padding: var(--body-vertical-padding) var(--body-horizontal-padding); + + display: flex; + width: 100%; + height: 100%; + z-index: 1; + position: relative; + + overflow: hidden; +} + +.task-container { + display: flex; + flex-direction: column; + width: 100%; + + position: absolute; + top: 0; + left: 0; + + z-index: -1; +} + +.task-timer-done { + float: right; + display: flex; + position: relative; + right: 5px; +} + +.task-timer-running { + float: right; + display: flex; + position: relative; + right: 5px; +} + +/* INDIVIDUAL TASKS */ +.done { + color: var(--done-task-font-color); +} + +.crossed { + text-decoration-line: line-through; +} + +#title { + transition: opacity 500ms; +} + +.fade { + opacity: 0; +} + +.username-div { + display: flex; + flex-direction: row; +} + +.task-and-username { + display: flex; + overflow: hidden; + text-overflow: ellipsis; +} + +.task-div { + display: flex; + overflow: hidden; + text-overflow: ellipsis; + justify-content: space-between; + flex-direction: var(--task-direction); + + font-size: var(--task-font-size); + color: var(--task-font-color); + + border: solid; + border-color: var(--task-border-color); + border-width: var(--task-border-width); + border-radius: var(--task-border-radius); + margin-bottom: var(--task-margin-bottom); + padding: var(--task-padding); +} + +.task-div:not(.done-task-div) { + background-color: var(--task-background-color); +} + +.task-div.done-task-div { + background-color: var(--done-task-background-color); +} + +.secondary { + display: none; +} + +.username { + color: var(--username-color); + font-weight: bold; + width: fit-content; + max-width: var(--username-max-width); + white-space: nowrap; + + overflow: hidden; + text-overflow: ellipsis; +} + +.task-div:not(:last-child) { + margin-bottom: var(--task-margin-bottom); +} + +.checkbox { + display: flex; + justify-content: center; + position: relative; + align-items: start; + margin-top: var(--check-box-margin-top); + margin-left: var(--check-box-margin-left); + margin-right: var(--check-box-margin-right); +} + +input[type="checkbox"] { + display: none; +} + +input[type="checkbox"] + label { + display: inline-block; + position: relative; + + border: solid; + border-color: var(--check-box-border-color); + border-width: var(--check-box-border-width); + border-radius: var(--check-box-border-radius); + + background: var(--check-box-background-color); + width: var(--check-box-size); + height: var(--check-box-size); + margin-right: 5px; + + font-size: var(--font-size); +} + +input[type="checkbox"]:checked + label:after { + content: var(--tick-character); + color: var(--tick-color); + font-size: var(--tick-size); + transform: translateY(calc(var(--tick-translate-y) * -1)); + + display: flex; + justify-content: center; +} + +.bullet-point { + display: flex; + justify-content: start; + align-items: start; + margin-top: var(--bullet-point-margin-top); + margin-left: var(--bullet-point-margin-left); + margin-right: var(--bullet-point-margin-right); +} + +.colon { + margin-right: var(--colon-margin-right); + margin-left: var(--colon-margin-left); +} + +.task { + display: -webkit-box; + -webkit-line-clamp: var(--number-of-lines); + -webkit-box-orient: vertical; + overflow: hidden; + width: max-content; + max-width: var(--task-max-width); + margin-left: var(--task-margin-left); +}