From 6a03b702418faed7ecdc122d46aaeef09c4f2910 Mon Sep 17 00:00:00 2001
From: gigamaster <1905497+gigamaster@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:36:00 +0100
Subject: [PATCH] Web Apps Launcher
---
app/asset/app.js | 2 +-
app/asset/launcher.json | 132 ++++++
app/asset/lib/component.min.js | 2 +
app/asset/modal-launcher.html | 53 +++
app/tools/tasks/index.html | 53 +++
app/tools/tasks/scripts/AppCollapsible.js | 34 ++
app/tools/tasks/scripts/AppDatePicker.js | 163 +++++++
app/tools/tasks/scripts/AppDraggable.js | 353 +++++++++++++++
app/tools/tasks/scripts/AppFlip.js | 196 ++++++++
app/tools/tasks/scripts/AppIcon.js | 26 ++
app/tools/tasks/scripts/AppSortable.js | 145 ++++++
app/tools/tasks/scripts/TodoApp.js | 192 ++++++++
app/tools/tasks/scripts/TodoController.js | 93 ++++
app/tools/tasks/scripts/TodoCustomList.js | 161 +++++++
app/tools/tasks/scripts/TodoDay.js | 52 +++
app/tools/tasks/scripts/TodoFrameCustom.js | 133 ++++++
app/tools/tasks/scripts/TodoFrameDays.js | 121 +++++
app/tools/tasks/scripts/TodoItem.js | 160 +++++++
app/tools/tasks/scripts/TodoItemInput.js | 67 +++
app/tools/tasks/scripts/TodoList.js | 68 +++
app/tools/tasks/scripts/TodoLogic.js | 357 +++++++++++++++
app/tools/tasks/scripts/util.js | 99 +++++
app/tools/tasks/scripts/uuid.js | 8 +
app/tools/tasks/styles/app-tasks.css | 212 +++++++++
app/tools/tasks/styles/base.css | 491 +++++++++++++++++++++
app/web-app/graphite.webp | Bin 0 -> 29336 bytes
app/web-app/livecodes.webp | Bin 0 -> 33144 bytes
app/web-app/livecodes/readme.md | 0
app/web-app/tldraw.webp | Bin 0 -> 31246 bytes
src/template/foot.html | 4 +-
30 files changed, 3374 insertions(+), 3 deletions(-)
create mode 100644 app/asset/launcher.json
create mode 100644 app/asset/lib/component.min.js
create mode 100644 app/asset/modal-launcher.html
create mode 100644 app/tools/tasks/index.html
create mode 100644 app/tools/tasks/scripts/AppCollapsible.js
create mode 100644 app/tools/tasks/scripts/AppDatePicker.js
create mode 100644 app/tools/tasks/scripts/AppDraggable.js
create mode 100644 app/tools/tasks/scripts/AppFlip.js
create mode 100644 app/tools/tasks/scripts/AppIcon.js
create mode 100644 app/tools/tasks/scripts/AppSortable.js
create mode 100644 app/tools/tasks/scripts/TodoApp.js
create mode 100644 app/tools/tasks/scripts/TodoController.js
create mode 100644 app/tools/tasks/scripts/TodoCustomList.js
create mode 100644 app/tools/tasks/scripts/TodoDay.js
create mode 100644 app/tools/tasks/scripts/TodoFrameCustom.js
create mode 100644 app/tools/tasks/scripts/TodoFrameDays.js
create mode 100644 app/tools/tasks/scripts/TodoItem.js
create mode 100644 app/tools/tasks/scripts/TodoItemInput.js
create mode 100644 app/tools/tasks/scripts/TodoList.js
create mode 100644 app/tools/tasks/scripts/TodoLogic.js
create mode 100644 app/tools/tasks/scripts/util.js
create mode 100644 app/tools/tasks/scripts/uuid.js
create mode 100644 app/tools/tasks/styles/app-tasks.css
create mode 100644 app/tools/tasks/styles/base.css
create mode 100644 app/web-app/graphite.webp
create mode 100644 app/web-app/livecodes.webp
create mode 100644 app/web-app/livecodes/readme.md
create mode 100644 app/web-app/tldraw.webp
diff --git a/app/asset/app.js b/app/asset/app.js
index 69034ca9..26305876 100644
--- a/app/asset/app.js
+++ b/app/asset/app.js
@@ -92,7 +92,7 @@ document.addEventListener('alpine:init', () => {
currentIndex: -1,
init() {
// Initialize data
- fetch('./search.json')
+ fetch('https://gigamaster.github.io/codemo/asset/search.json')
.then(res => res.json())
.then(data => this.data = data.data);
},
diff --git a/app/asset/launcher.json b/app/asset/launcher.json
new file mode 100644
index 00000000..cf1c6ac8
--- /dev/null
+++ b/app/asset/launcher.json
@@ -0,0 +1,132 @@
+[
+ {
+ "id": 1,
+ "url": "https://gigamaster.github.io/codemo/web-app/dpaint/",
+ "name": "DPaint",
+ "author": "Author",
+ "desc": "Web app graphic image editor modeled after the legendary Deluxe Paint",
+ "cat": "Graphics",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/dpaint.webp"
+ },
+ {
+ "id": 2,
+ "url": "https://gigamaster.github.io/codemo/web-app/drawio/",
+ "name": "Draw.io",
+ "author": "Author",
+ "desc": "Web app to make flowcharts, process and network diagrams, org charts, UML, ER.",
+ "cat": "Diagram",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/drawio.webp"
+ },
+ {
+ "id": 3,
+ "url": "https://gigamaster.github.io/codemo/web-app/encrypt/",
+ "name": "Encrypt",
+ "author": "Author",
+ "desc": "Static HTML Encryption for public host",
+ "cat": "Security Privacy",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/encrypt.webp"
+ },
+ {
+ "id": 4,
+ "url": "https://gigamaster.github.io/codemo/web-app/erd-editor/",
+ "name": "ERD-editor",
+ "author": "Author",
+ "desc": "Entity-Relationship Diagram Editor",
+ "cat": "Database",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/erp-editor.webp"
+ },
+ {
+ "id": 5,
+ "url": "https://gigamaster.github.io/codemo/web-app/fuxa-hmi-scada/",
+ "name": "Fuxa",
+ "author": "Author",
+ "desc": "Web app SCADA/HMI/Dashboard to create modern process visualizations with individual designs",
+ "cat": "Dashboard",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/fuxa.webp"
+ },
+ {
+ "id": 6,
+ "url": "https://gigamaster.github.io/codemo/web-app/grapesjs/",
+ "name": "Grapesjs",
+ "author": "Author",
+ "desc": "Web app drag and drop website builder",
+ "cat": "Web Design",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/grapesjs.webp"
+ },
+ {
+ "id": 7,
+ "url": "https://gigamaster.github.io/codemo/web-app/graphite/",
+ "name": "Graphite",
+ "author": "Author",
+ "desc": "Web app to create, compose and edit vector images and graphics",
+ "cat": "Graphics",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/graphite.webp"
+ },
+ {
+ "id": 8,
+ "url": "https://gigamaster.github.io/codemo/web-app/livecodes/",
+ "name": "Livecodes",
+ "author": "Author",
+ "desc": "Web app code editor with support for 90+ languages & frameworks, embedded playgrounds and AI code assistant",
+ "cat": "Code Editor",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/livecodes.webp"
+ },
+ {
+ "id": 9,
+ "url": "https://gigamaster.github.io/codemo/web-app/mermaid-editor/",
+ "name": "Mermaid editor",
+ "author": "Author",
+ "desc": "Web app editor to create and modify complex charts and diagrams",
+ "cat": "Diagram",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/mermaid.webp"
+ },
+ {
+ "id": 10,
+ "url": "https://gigamaster.github.io/codemo/web-app/studio/",
+ "name": "Studio",
+ "author": "Author",
+ "desc": "Web app to create designs and templates for social media",
+ "cat": "Graphics",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/studio.webp"
+ },
+ {
+ "id": 11,
+ "url": "https://gigamaster.github.io/codemo/web-app/tldraw/",
+ "name": "Tldraw",
+ "author": "Author",
+ "desc": "Web app to draw and write on the canvas, add images and video",
+ "cat": "Drawing 2D",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/tldraw.webp"
+ },
+ {
+ "id": 12,
+ "url": "https://gigamaster.github.io/codemo/web-app/voxel-builder/",
+ "name": "Voxel-builder",
+ "author": "Author",
+ "desc": "Web app to create 3D voxel art, modeling art game graphics",
+ "cat": "3D graphics",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/voxel-builder.webp"
+ },
+ {
+ "id": 13,
+ "url": "https://gigamaster.github.io/codemo/web-app/vvvebjs/",
+ "name": "Vvvebjs",
+ "author": "Author",
+ "desc": "Drag and drop web page builder",
+ "cat": "Web Design",
+ "icon": "icon-apps",
+ "image": "https://gigamaster.github.io/codemo/web-app/vvveb.webp"
+ }
+]
diff --git a/app/asset/lib/component.min.js b/app/asset/lib/component.min.js
new file mode 100644
index 00000000..e914a43f
--- /dev/null
+++ b/app/asset/lib/component.min.js
@@ -0,0 +1,2 @@
+/* alpinejs-component@1.2.7 */
+(()=>{function p(e,n){let s=(n.includes("global")?[...document.styleSheets]:[...document.styleSheets].filter(({title:t})=>n.includes(t))).flatMap(({cssRules:t})=>[...t]),i=new CSSStyleSheet;for(let t of s)t instanceof CSSStyleRule&&t.selectorText===":root"||i.insertRule(t.cssText);e.adoptedStyleSheets=[i]}async function u(e,n,o){function l(i){let t=document.getElementById(i),a=new DOMParser().parseFromString(t.innerHTML,"text/html").body.firstChild;return Promise.resolve(a)}let s=await l(n);o.appendChild(s),e.initTree(o)}async function h(e,n,o){let s=await(await fetch(n)).text(),i=new DOMParser().parseFromString(s,"text/html").body.firstChild;o.appendChild(i),e.initTree(o)}function d(e){class n extends HTMLElement{connectedCallback(){if(this._hasInit)return;let s=this.attachShadow({mode:"open"}),i=this.hasAttribute(":template"),t=this.hasAttribute(":url");(i||t)&&e.initTree(this);let{template:a={value:""},url:f={value:""},styles:S={value:""}}=this.attributes,m=a.value,c=f.value,r=S.value.split(",");m.length&&u(e,m,s),c.length&&h(e,c,s),r.length&&p(s,r),this._hasInit=!0}}let{name:o}=window?.xComponent||{name:"x-component"};window.customElements.get(o)||(customElements.define(o,n),new n)}document.addEventListener("alpine:init",()=>window.Alpine.plugin(d));})();
diff --git a/app/asset/modal-launcher.html b/app/asset/modal-launcher.html
new file mode 100644
index 00000000..54da8017
--- /dev/null
+++ b/app/asset/modal-launcher.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/tools/tasks/index.html b/app/tools/tasks/index.html
new file mode 100644
index 00000000..b3293b8a
--- /dev/null
+++ b/app/tools/tasks/index.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+ Codemo Task Manager
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/tools/tasks/scripts/AppCollapsible.js b/app/tools/tasks/scripts/AppCollapsible.js
new file mode 100644
index 00000000..6001435f
--- /dev/null
+++ b/app/tools/tasks/scripts/AppCollapsible.js
@@ -0,0 +1,34 @@
+/**
+ * @param {HTMLElement} el
+ */
+export function AppCollapsible(el) {
+ let show = true;
+
+ setTimeout(() => el.classList.add('-animated'), 200);
+
+ el.addEventListener('collapse', (e) => {
+ if (typeof e.detail === 'boolean') {
+ show = e.detail;
+ }
+
+ update();
+ });
+
+ el.querySelector('.bar > .toggle').addEventListener('click', () => {
+ show = !show;
+ update();
+ });
+
+ update();
+
+ function update() {
+ el.querySelector('.bar > .toggle > .app-icon').classList.toggle(
+ '-r180',
+ show,
+ );
+
+ el.querySelectorAll('.body').forEach((el) => {
+ el.style.height = show ? `${el.children[0].offsetHeight}px` : '0';
+ });
+ }
+}
diff --git a/app/tools/tasks/scripts/AppDatePicker.js b/app/tools/tasks/scripts/AppDatePicker.js
new file mode 100644
index 00000000..bc54c24e
--- /dev/null
+++ b/app/tools/tasks/scripts/AppDatePicker.js
@@ -0,0 +1,163 @@
+import { AppIcon } from './AppIcon.js';
+import { formatMonth } from './util.js';
+
+const datesCell = /* html */ `
+ |
+`;
+
+const datesRow = /* html */ `
+
+ ${datesCell}
+ ${datesCell}
+ ${datesCell}
+ ${datesCell}
+ ${datesCell}
+ ${datesCell}
+ ${datesCell}
+
+`;
+
+/**
+ * @param {HTMLElement} el
+ */
+export function AppDatePicker(el) {
+ const now = new Date();
+ let at = {
+ year: now.getFullYear(),
+ month: now.getMonth() + 1,
+ };
+ let show = false;
+
+ el.innerHTML = /* html */ `
+
+
+
+
+ Su |
+ Mo |
+ Tu |
+ We |
+ Th |
+ Fr |
+ Sa |
+
+
+
+ ${datesRow}
+ ${datesRow}
+ ${datesRow}
+ ${datesRow}
+ ${datesRow}
+
+
+ `;
+
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ el.addEventListener('toggleDatePicker', (e) => {
+ show = e.detail ?? !show;
+ update();
+ });
+
+ el.querySelector('.previousmonth').addEventListener('click', previousMonth);
+ el.querySelector('.nextmonth').addEventListener('click', nextMonth);
+
+ el.addEventListener('click', (e) => {
+ if (!e.target.matches('.pick')) return;
+
+ show = false;
+ update();
+
+ el.dispatchEvent(
+ new CustomEvent('pickDate', {
+ detail: new Date(
+ e.target.dataset.year,
+ e.target.dataset.month - 1,
+ e.target.dataset.day,
+ ),
+ bubbles: true,
+ }),
+ );
+ });
+
+ function previousMonth() {
+ if (at.month > 1) {
+ at = {
+ year: at.year,
+ month: at.month - 1,
+ };
+ } else {
+ at = {
+ year: at.year - 1,
+ month: 12,
+ };
+ }
+
+ update();
+ }
+
+ function nextMonth() {
+ if (at.month < 12) {
+ at = {
+ year: at.year,
+ month: at.month + 1,
+ };
+ } else {
+ at = {
+ year: at.year + 1,
+ month: 1,
+ };
+ }
+
+ update();
+ }
+
+ function update() {
+ el.classList.toggle('-show', show);
+
+ const now = new Date();
+ const first = new Date(at.year, at.month - 1, 1);
+
+ el.querySelector('.month').innerHTML = `${formatMonth(
+ first,
+ )} ${first.getFullYear()}`;
+
+ let current = new Date(first);
+ current.setDate(1 - current.getDay());
+
+ const datesBody = el.querySelector('.dates > tbody');
+
+ for (let i = 0; i < 35; ++i) {
+ const row = Math.floor(i / 7);
+ const column = i % 7;
+
+ const cell = datesBody.children[row].children[column];
+ const button = cell.children[0];
+
+ button.innerHTML = current.getDate();
+
+ button.dataset.year = current.getFullYear();
+ button.dataset.month = current.getMonth() + 1;
+ button.dataset.day = current.getDate();
+
+ button.classList.toggle(
+ '-highlight',
+ current.getFullYear() === now.getFullYear() &&
+ current.getMonth() === now.getMonth() &&
+ current.getDate() === now.getDate(),
+ );
+
+ current.setDate(current.getDate() + 1);
+ }
+ }
+
+ update();
+}
diff --git a/app/tools/tasks/scripts/AppDraggable.js b/app/tools/tasks/scripts/AppDraggable.js
new file mode 100644
index 00000000..7dc6159d
--- /dev/null
+++ b/app/tools/tasks/scripts/AppDraggable.js
@@ -0,0 +1,353 @@
+/**
+ * @param {HTMLElement} el
+ * @param {{
+ * dropSelector: string;
+ * dragThreshold?: number;
+ * dropRange?: number;
+ * }} options
+ */
+export function AppDraggable(el, options) {
+ const dragThreshold = options.dragThreshold ?? 5;
+ const dropRange = options.dropRange ?? 50;
+ const dropRangeSquared = dropRange * dropRange;
+
+ let originX, originY;
+ let clientX, clientY;
+ let startTime;
+ let dragging = false;
+ let clicked = false;
+ let data;
+ let image;
+ let imageSource;
+ let imageX, imageY;
+ let currentTarget;
+
+ el.addEventListener('touchstart', start);
+ el.addEventListener('mousedown', start);
+
+ // Maybe prevent click
+ el.addEventListener(
+ 'click',
+ (e) => {
+ if (dragging || clicked) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ }
+ },
+ true,
+ );
+
+ function start(e) {
+ if (el.classList.contains('_nodrag')) return;
+ if (e.type === 'mousedown' && e.button !== 0) return;
+ if (e.touches && e.touches.length > 1) return;
+
+ e.preventDefault();
+
+ const p = getPositionHost(e);
+ clientX = originX = p.clientX ?? p.pageX;
+ clientY = originY = p.clientY ?? p.pageY;
+ startTime = Date.now();
+
+ startListening();
+ }
+
+ function move(e, ) {
+ e.preventDefault();
+ const p = getPositionHost(e);
+ clientX = p.clientX ?? p.pageX;
+ clientY = p.clientY ?? p.pageY;
+
+ if (dragging) {
+ dispatchDrag();
+ dispatchTarget();
+ return;
+ }
+
+ const deltaX = clientX - originX;
+ const deltaY = clientY - originY;
+
+ if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) {
+ return;
+ }
+
+ // prevent unintentional dragging on touch devices
+ if (e.touches && Date.now() - startTime < 50) {
+ stopListening();
+ return;
+ }
+
+ dragging = true;
+ data = {};
+
+ dispatchStart();
+ dispatchDrag();
+ dispatchTarget();
+ dispatchOverContinuously();
+ }
+
+ function end(e) {
+ e.preventDefault();
+
+ if (!dragging) {
+ e.target.click();
+ clicked = true;
+ }
+
+ stopListening();
+
+ requestAnimationFrame(() => {
+ clicked = false;
+
+ if (dragging) {
+ dispatchTarget();
+ dispatchEnd();
+ }
+ });
+ }
+
+ function startListening() {
+ el.addEventListener('touchmove', move);
+ el.addEventListener('touchend', end);
+ window.addEventListener('mousemove', move);
+ window.addEventListener('mouseup', end);
+ }
+
+ function stopListening() {
+ el.removeEventListener('touchmove', move);
+ el.removeEventListener('touchend', end);
+ window.removeEventListener('mousemove', move);
+ window.removeEventListener('mouseup', end);
+ }
+
+ //
+
+ function dispatchStart() {
+ setImage(el);
+
+ el.dispatchEvent(
+ new CustomEvent('draggableStart', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+
+ function dispatchDrag() {
+ image.dispatchEvent(
+ new CustomEvent('draggableDrag', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+
+ function dispatchTarget() {
+ if (!dragging) return;
+
+ const nextTarget = getTarget();
+
+ if (nextTarget === currentTarget) return;
+
+ if (currentTarget) {
+ currentTarget.addEventListener('draggableLeave', removeDropClassOnce);
+ currentTarget.dispatchEvent(
+ new CustomEvent('draggableLeave', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+
+ if (nextTarget) {
+ nextTarget.addEventListener('draggableEnter', addDropClassOnce);
+ nextTarget.dispatchEvent(
+ new CustomEvent('draggableEnter', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+
+ currentTarget = nextTarget;
+ }
+
+ function dispatchOverContinuously() {
+ if (!dragging) return;
+
+ dispatchOver();
+ setTimeout(dispatchOver, 50);
+ }
+
+ function dispatchOver() {
+ if (currentTarget) {
+ currentTarget.dispatchEvent(
+ new CustomEvent('draggableOver', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+
+ setTimeout(dispatchOver, 50);
+ }
+
+ function dispatchEnd() {
+ if (currentTarget) {
+ currentTarget.addEventListener('draggableDrop', cleanUpOnce);
+ currentTarget.dispatchEvent(
+ new CustomEvent('draggableDrop', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ } else {
+ image.dispatchEvent(
+ new CustomEvent('draggableCancel', {
+ detail: buildDetail(),
+ bubbles: true,
+ }),
+ );
+ }
+ }
+
+ //
+
+ function buildDetail() {
+ const detail = {
+ el,
+ data,
+ image,
+ imageSource,
+ originX,
+ originY,
+ clientX,
+ clientY,
+ imageX,
+ imageY,
+ setImage: (source) => {
+ setImage(source);
+ detail.image = image;
+ },
+ };
+
+ return detail;
+ }
+
+ function setImage(source) {
+ if (imageSource === source) return;
+ imageSource = source;
+
+ removeImage();
+
+ image = imageSource.cloneNode(true);
+ image.style.position = 'fixed';
+ image.style.left = '0';
+ image.style.top = '0';
+ image.style.width = `${imageSource.offsetWidth}px`;
+ image.style.height = `${imageSource.offsetHeight}px`;
+ image.style.margin = '0';
+ image.style.zIndex = 9999;
+ image.classList.add('-dragging');
+
+ const rect = source.getBoundingClientRect();
+ imageX = originX - rect.left;
+ imageY = originY - rect.top;
+
+ image.addEventListener('draggableDrag', (e) => {
+ const x = e.detail.clientX - e.detail.imageX;
+ const y = e.detail.clientY - e.detail.imageY;
+ image.style.transition = 'none';
+ image.style.transform = `translate(${x}px, ${y}px)`;
+ });
+
+ image.addEventListener('draggableCancel', cleanUp);
+
+ document.body.appendChild(image);
+ }
+
+ function addDropClassOnce(e) {
+ e.target.removeEventListener(e.type, addDropClassOnce);
+ e.target.classList.add('-drop');
+ }
+
+ function removeDropClassOnce(e) {
+ e.target.removeEventListener(e.type, addDropClassOnce);
+ e.target.classList.remove('-drop');
+ }
+
+ function cleanUpOnce(e) {
+ e.target.removeEventListener(e.type, cleanUpOnce);
+ cleanUp();
+ }
+
+ function cleanUp() {
+ currentTarget?.classList.remove('-drop');
+
+ removeImage();
+
+ data = null;
+ image = null;
+ imageSource = null;
+ currentTarget = null;
+ }
+
+ function removeImage() {
+ image?.parentNode?.removeChild(image);
+ }
+
+ function getTarget() {
+ const candidates = [];
+
+ document.querySelectorAll(options.dropSelector).forEach((el) => {
+ const rect = el.getBoundingClientRect();
+ const distanceSquared = pointDistanceToRectSquared(
+ clientX,
+ clientY,
+ rect,
+ );
+
+ if (distanceSquared > dropRangeSquared) return;
+
+ candidates.push({
+ el,
+ distance2: distanceSquared,
+ });
+ });
+
+ candidates.sort((a, b) => {
+ if (a.distance2 === 0 && b.distance2 === 0) {
+ // in this case, the client position is inside both rectangles
+ // if A contains B, B is the correct target and vice versa
+ // TODO sort by z-index somehow?
+ return a.el.contains(b.el) ? -1 : b.el.contains(a.el) ? 1 : 0;
+ }
+
+ // sort by distance, ascending
+ return a.distance2 - b.distance2;
+ });
+
+ return candidates.length > 0 ? candidates[0].el : null;
+ }
+
+ function pointDistanceToRectSquared(x, y, rect) {
+ const dx =
+ x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0;
+ const dy =
+ y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0;
+
+ return dx * dx + dy * dy;
+ }
+
+ function getPositionHost(e) {
+ if (e.targetTouches && e.targetTouches.length > 0) {
+ return e.targetTouches[0];
+ }
+
+ if (e.changedTouches && e.changedTouches.length > 0) {
+ return e.changedTouches[0];
+ }
+
+ return e;
+ }
+}
diff --git a/app/tools/tasks/scripts/AppFlip.js b/app/tools/tasks/scripts/AppFlip.js
new file mode 100644
index 00000000..4ebf49be
--- /dev/null
+++ b/app/tools/tasks/scripts/AppFlip.js
@@ -0,0 +1,196 @@
+/**
+ * @param {HTMLElement} el
+ * @param {{
+ * initialDelay?: number;
+ * removeTimeout: number;
+ * selector: string;
+ * }} options
+ */
+export function AppFlip(el, options) {
+ let enabled = options.initialDelay === 0;
+ let first;
+ let level = 0;
+
+ // Enable animations only after an initial delay.
+ setTimeout(() => {
+ enabled = true;
+ }, options.initialDelay ?? 100);
+
+ // Take a snapshot before any HTML changes.
+ // Do this only for the first beforeFlip event in the current cycle.
+ el.addEventListener('beforeFlip', () => {
+ if (!enabled) return;
+ if (++level > 1) return;
+
+ first = snapshot();
+ });
+
+ // Take a snapshot after HTML changes, calculate and play animations.
+ // Do this only for the last flip event in the current cycle.
+ el.addEventListener('flip', () => {
+ if (!enabled) return;
+ if (--level > 0) return;
+
+ const last = snapshot();
+ const toRemove = invertForRemoval(first, last);
+ const toAnimate = invertForAnimation(first, last);
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ remove(toRemove);
+ animate(toAnimate);
+
+ first = null;
+ });
+ });
+ });
+
+ // Build a snapshot of the current HTML's client rectangles,
+ // including original transforms and hierarchy.
+ function snapshot() {
+ const map = new Map();
+
+ el.querySelectorAll(options.selector).forEach((el) => {
+ const key = el.dataset.key ?? el;
+
+ // Parse original transform,
+ // i.e. strip inverse transform using "scale(1)" marker.
+ const transform = el.style.transform
+ ? el.style.transform.replace(/^.*scale\(1\)/, '')
+ : '';
+
+ map.set(key, {
+ key,
+ el,
+ rect: el.getBoundingClientRect(),
+ ancestor: null,
+ transform,
+ });
+ });
+
+ resolveAncestors(map);
+
+ return map;
+ }
+
+ function resolveAncestors(map) {
+ map.forEach((entry) => {
+ let current = entry.el.parentNode;
+
+ while (current && current !== el) {
+ const ancestor = map.get(current.dataset.key ?? current);
+
+ if (ancestor) {
+ entry.ancestor = ancestor;
+ return;
+ }
+
+ current = current.parentNode;
+ }
+ });
+ }
+
+ // Reinsert removed elements at their original position.
+ function invertForRemoval(first, last) {
+ const toRemove = [];
+
+ first.forEach((entry) => {
+ if (entry.el.classList.contains('_noflip')) return;
+ if (!needsRemoval(entry)) return;
+
+ entry.el.style.position = 'fixed';
+ entry.el.style.left = `${entry.rect.left}px`;
+ entry.el.style.top = `${entry.rect.top}px`;
+ entry.el.style.width = `${entry.rect.right - entry.rect.left}px`;
+ entry.el.style.transition = 'none';
+ entry.el.style.transform = '';
+
+ el.appendChild(entry.el);
+ toRemove.push(entry);
+ });
+
+ return toRemove;
+
+ function needsRemoval(entry) {
+ if (entry.ancestor && needsRemoval(entry.ancestor)) {
+ return false;
+ }
+
+ return !last.has(entry.key);
+ }
+ }
+
+ // Set position of moved elements to their original position,
+ // or set opacity to zero for new elements to appear nicely.
+ function invertForAnimation(first, last) {
+ const toAnimate = [];
+
+ last.forEach((entry) => {
+ if (entry.el.classList.contains('_noflip')) return;
+
+ calculate(entry);
+
+ if (entry.appear) {
+ entry.el.style.transition = 'none';
+ entry.el.style.opacity = '0';
+ toAnimate.push(entry);
+ } else if (entry.deltaX !== 0 || entry.deltaY !== 0) {
+ // Set inverted transform with "scale(1)" marker, see above.
+ entry.el.style.transition = 'none';
+ entry.el.style.transform = `translate(${entry.deltaX}px, ${entry.deltaY}px) scale(1) ${entry.transform}`;
+ toAnimate.push(entry);
+ }
+ });
+
+ return toAnimate;
+
+ // Calculate inverse transform relative to any animated ancestors.
+ function calculate(entry) {
+ if (entry.calculated) return;
+ entry.calculated = true;
+
+ const b = first.get(entry.key);
+
+ if (b) {
+ entry.deltaX = b.rect.left - entry.rect.left;
+ entry.deltaY = b.rect.top - entry.rect.top;
+
+ if (entry.ancestor) {
+ calculate(entry.ancestor);
+
+ entry.deltaX -= entry.ancestor.deltaX;
+ entry.deltaY -= entry.ancestor.deltaY;
+ }
+ } else {
+ entry.appear = true;
+ entry.deltaX = 0;
+ entry.deltaY = 0;
+ }
+ }
+ }
+
+ // Play remove animations and remove elements after timeout.
+ function remove(entries) {
+ entries.forEach((entry) => {
+ entry.el.style.transition = '';
+ entry.el.style.opacity = '0';
+ });
+
+ setTimeout(() => {
+ entries.forEach((entry) => {
+ if (entry.el.parentNode) {
+ entry.el.parentNode.removeChild(entry.el);
+ }
+ });
+ }, options.removeTimeout);
+ }
+
+ // Play move/appear animations.
+ function animate(entries) {
+ entries.forEach((entry) => {
+ entry.el.style.transition = '';
+ entry.el.style.transform = entry.transform;
+ entry.el.style.opacity = '';
+ });
+ }
+}
diff --git a/app/tools/tasks/scripts/AppIcon.js b/app/tools/tasks/scripts/AppIcon.js
new file mode 100644
index 00000000..523843b5
--- /dev/null
+++ b/app/tools/tasks/scripts/AppIcon.js
@@ -0,0 +1,26 @@
+// export const BASE_URL =
+ export const BASE_URL ='https://api.iconify.design';
+ const iconset = 'tabler';
+ const iconH = '16';
+ const cache = {};
+/**
+ * iconify options
+ * https://iconify.design/docs/usage/svg/no-code/
+ */
+/**
+ * @param {HTMLElement} el
+ */
+export function AppIcon(el) {
+ if (el.children.length > 0) return;
+
+ const id = el.dataset.id;
+ let promise = cache[id];
+
+ if (!promise) {
+ promise = cache[id] = fetch(`${BASE_URL}/${iconset}/${id}.svg?height=${iconH}`).then((r) => r.text());
+ }
+
+ promise.then((svg) => {
+ el.innerHTML = el.classList.contains('-double') ? svg + svg : svg;
+ });
+}
diff --git a/app/tools/tasks/scripts/AppSortable.js b/app/tools/tasks/scripts/AppSortable.js
new file mode 100644
index 00000000..cc3a60e7
--- /dev/null
+++ b/app/tools/tasks/scripts/AppSortable.js
@@ -0,0 +1,145 @@
+/**
+ * @param {HTMLElement} el
+ * @param {{
+ * direction?: 'horizontal' | 'vertical';
+ * }} options
+ */
+export function AppSortable(el, options) {
+ let placeholder;
+ let placeholderSource;
+ let currentIndex = -1;
+
+ const isBefore = options.direction === 'horizontal' ? isLeft : isAbove;
+
+ el.addEventListener('draggableStart', (e) =>
+ e.detail.image.addEventListener('draggableCancel', cleanUp),
+ );
+
+ el.addEventListener('draggableOver', (e) =>
+ maybeDispatchUpdate(calculateIndex(e.detail.image), e),
+ );
+
+ el.addEventListener('draggableLeave', (e) => maybeDispatchUpdate(-1, e));
+
+ el.addEventListener('draggableDrop', (e) =>
+ el.dispatchEvent(
+ new CustomEvent('sortableDrop', {
+ detail: buildDetail(e),
+ bubbles: true,
+ }),
+ ),
+ );
+
+ el.addEventListener('sortableUpdate', (e) => {
+ if (!placeholder) {
+ e.detail.setPlaceholder(e.detail.originalEvent.detail.imageSource);
+ }
+
+ if (e.detail.index >= 0) {
+ insertPlaceholder(e.detail.index);
+ } else {
+ removePlaceholder();
+ }
+
+ removeByKey(e.detail.data.key);
+ });
+
+ el.addEventListener('sortableDrop', cleanUp);
+
+ function maybeDispatchUpdate(index, originalEvent) {
+ if (index !== currentIndex) {
+ currentIndex = index;
+
+ el.dispatchEvent(
+ new CustomEvent('sortableUpdate', {
+ detail: buildDetail(originalEvent),
+ bubbles: true,
+ }),
+ );
+ }
+ }
+
+ function cleanUp() {
+ removePlaceholder();
+ placeholder = null;
+ placeholderSource = null;
+ currentIndex = -1;
+ }
+
+ function buildDetail(e) {
+ const detail = {
+ data: e.detail.data,
+ index: currentIndex,
+ placeholder,
+ setPlaceholder: (source) => {
+ setPlaceholder(source);
+ detail.placeholder = placeholder;
+ },
+ originalEvent: e,
+ };
+
+ return detail;
+ }
+
+ function setPlaceholder(source) {
+ if (placeholderSource === source) return;
+ placeholderSource = source;
+
+ removePlaceholder();
+
+ placeholder = placeholderSource.cloneNode(true);
+ placeholder.classList.add('-placeholder');
+ placeholder.removeAttribute('data-key');
+ }
+
+ function insertPlaceholder(index) {
+ if (placeholder && el.children[index] !== placeholder) {
+ if (placeholder.parentNode === el) el.removeChild(placeholder);
+ el.insertBefore(placeholder, el.children[index]);
+ }
+ }
+
+ function removePlaceholder() {
+ placeholder?.parentNode?.removeChild(placeholder);
+ }
+
+ function removeByKey(key) {
+ for (let i = 0, l = el.children.length; i < l; ++i) {
+ const child = el.children[i];
+
+ if (child && child.dataset.key === key) {
+ el.removeChild(child);
+ }
+ }
+ }
+
+ function calculateIndex(image) {
+ if (el.children.length === 0) return 0;
+
+ const rect = image.getBoundingClientRect();
+ let p = 0;
+
+ for (let i = 0, l = el.children.length; i < l; ++i) {
+ const child = el.children[i];
+
+ if (isBefore(rect, child.getBoundingClientRect())) return i - p;
+ if (child === placeholder) p = 1;
+ }
+
+ return el.children.length - p;
+ }
+
+ function isAbove(rectA, rectB) {
+ return (
+ rectA.top + (rectA.bottom - rectA.top) / 2 <=
+ rectB.top + (rectB.bottom - rectB.top) / 2
+ );
+ }
+
+ function isLeft(rectA, rectB) {
+ return (
+ rectA.left + (rectA.right - rectA.left) / 2 <=
+ rectB.left + (rectB.right - rectB.left) / 2
+ );
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoApp.js b/app/tools/tasks/scripts/TodoApp.js
new file mode 100644
index 00000000..b0697af3
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoApp.js
@@ -0,0 +1,192 @@
+import { AppCollapsible } from './AppCollapsible.js';
+import { AppFlip } from './AppFlip.js';
+import { AppIcon } from './AppIcon.js';
+import { AppDatePicker } from './AppDatePicker.js';
+import { TodoController } from './TodoController.js';
+import { TodoFrameCustom } from './TodoFrameCustom.js';
+import { TodoFrameDays } from './TodoFrameDays.js';
+import { TodoLogic } from './TodoLogic.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoApp(el) {
+ let todoData = TodoLogic.initTodoData();
+
+ el.innerHTML = /* html */ `
+
+
+
+
+ `;
+
+ AppFlip(el, {
+ selector: '.todo-item, .todo-item-input, .todo-day, .todo-custom-list',
+ removeTimeout: 200,
+ });
+
+ TodoController(el);
+
+ el.querySelectorAll('.app-collapsible').forEach(AppCollapsible);
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ el.querySelector('.home').addEventListener('click', () =>
+ el.dispatchEvent(new CustomEvent('seekToToday', { bubbles: true })),
+ );
+
+ el.querySelectorAll('.app-date-picker').forEach(AppDatePicker);
+
+ el.querySelector('.pickdate').addEventListener('click', () =>
+ el
+ .querySelector('.datepicker')
+ .dispatchEvent(new CustomEvent('toggleDatePicker')),
+ );
+
+ el.querySelector('.datepicker').addEventListener('pickDate', (e) =>
+ el.dispatchEvent(
+ new CustomEvent('seekToDate', { detail: e.detail, bubbles: true }),
+ ),
+ );
+
+ TodoFrameDays(el.querySelector('.todo-frame.-days'));
+ TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
+
+ el.querySelector('[name=importFile]').addEventListener('change', (e) => {
+ const f = e.target.files[0];
+ if (!f) return;
+
+ const reader = new FileReader();
+
+ reader.addEventListener('load', (e) => {
+ try {
+ const todoData = JSON.parse(e.target.result);
+
+ el.dispatchEvent(
+ new CustomEvent('importTodoData', {
+ detail: todoData,
+ bubbles: true,
+ }),
+ );
+ } catch (err) {
+ alert(`Could not import data (${err.message})`);
+ }
+ });
+
+ reader.readAsText(f);
+ });
+
+ el.querySelector('.app-header > .actions > .export').addEventListener(
+ 'click',
+ () => {
+ el.dispatchEvent(new CustomEvent('exportTodoData', { bubbles: true }));
+ },
+ );
+
+ // Each of these events make changes to the HTML to be animated using FLIP.
+ // Listening to them using "capture" dispatches "beforeFlip" before any changes.
+ el.addEventListener('todoData', beforeFlip, true);
+ el.addEventListener('sortableUpdate', beforeFlip, true);
+ el.addEventListener('draggableCancel', beforeFlip, true);
+ el.addEventListener('draggableDrop', beforeFlip, true);
+
+ // Some necessary work to orchestrate drag & drop with FLIP animations
+ el.addEventListener('draggableStart', (e) => {
+ e.detail.image.classList.add('_noflip');
+ el.appendChild(e.detail.image);
+ });
+
+ el.addEventListener('draggableCancel', (e) => {
+ e.detail.image.classList.remove('_noflip');
+ update();
+ });
+
+ el.addEventListener('draggableDrop', (e) => {
+ e.detail.image.classList.remove('_noflip');
+ });
+
+ el.addEventListener('sortableUpdate', (e) => {
+ e.detail.placeholder.classList.add('_noflip');
+ });
+
+ // Dispatch "focusOther" on .use-focus-other inputs if they are not active.
+ // Ensures only one edit input is active.
+ el.addEventListener('focusin', (e) => {
+ if (!e.target.classList.contains('use-focus-other')) return;
+
+ document.querySelectorAll('.use-focus-other').forEach((el) => {
+ if (el === e.target) return;
+ el.dispatchEvent(new CustomEvent('focusOther'));
+ });
+ });
+
+ // Listen to the TodoController's data.
+ // This is the main update.
+ // Everything else is related to drag & drop or FLIP animations.
+ el.addEventListener('todoData', (e) => {
+ todoData = e.detail;
+ update();
+ });
+
+ // Dispatch "flip" after HTML changes from the following events.
+ // This plays the FLIP animations.
+ el.addEventListener('todoData', flip);
+ el.addEventListener('sortableUpdate', flip);
+ el.addEventListener('draggableCancel', flip);
+ el.addEventListener('draggableDrop', flip);
+
+ el.dispatchEvent(new CustomEvent('loadTodoData'));
+
+ function update() {
+ el.querySelectorAll('.todo-frame').forEach((el) =>
+ el.dispatchEvent(new CustomEvent('todoData', { detail: todoData })),
+ );
+
+ el.querySelectorAll('.app-collapsible').forEach((el) =>
+ el.dispatchEvent(new CustomEvent('collapse')),
+ );
+ }
+
+ function beforeFlip(e) {
+ if (e.type === 'todoData' && e.target !== el) return;
+
+ el.dispatchEvent(new CustomEvent('beforeFlip'));
+ }
+
+ function flip() {
+ el.dispatchEvent(new CustomEvent('flip'));
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoController.js b/app/tools/tasks/scripts/TodoController.js
new file mode 100644
index 00000000..61f55026
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoController.js
@@ -0,0 +1,93 @@
+import { TodoLogic } from './TodoLogic.js';
+import { toDataURL } from './util.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoController(el) {
+ let todoData = TodoLogic.initTodoData();
+ let saveTimeout;
+
+ el.addEventListener('loadTodoData', load);
+ el.addEventListener('importTodoData', (e) => importTodoData(e.detail));
+ el.addEventListener('exportTodoData', exportTodoData);
+
+ for (const action of [
+ 'addTodoItem',
+ 'checkTodoItem',
+ 'editTodoItem',
+ 'moveTodoItem',
+ 'deleteTodoItem',
+ 'addCustomTodoList',
+ 'editCustomTodoList',
+ 'moveCustomTodoList',
+ 'deleteCustomTodoList',
+ 'seekDays',
+ 'seekToToday',
+ 'seekToDate',
+ 'seekCustomTodoLists',
+ ]) {
+ el.addEventListener(action, (e) => {
+ todoData = TodoLogic[action](todoData, e.detail);
+ update();
+ });
+ }
+
+ function update() {
+ save();
+
+ el.dispatchEvent(
+ new CustomEvent('todoData', {
+ detail: todoData,
+ bubbles: false,
+ }),
+ );
+ }
+
+ function load() {
+ try {
+ if (localStorage?.todo) {
+ todoData = TodoLogic.movePastTodoItems({
+ ...todoData,
+ ...JSON.parse(localStorage.todo),
+ });
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.warn(err);
+ }
+
+ update();
+ }
+
+ function save() {
+ clearTimeout(saveTimeout);
+
+ saveTimeout = setTimeout(() => {
+ try {
+ localStorage.todo = JSON.stringify(todoData);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.warn(err);
+ }
+ }, 100);
+ }
+
+ function importTodoData(input) {
+ // TODO validate?
+ todoData = input;
+
+ update();
+ }
+
+ async function exportTodoData() {
+ const json = JSON.stringify(todoData, null, 2);
+ const href = await toDataURL(json);
+ const link = document.createElement('a');
+ link.setAttribute('download', 'todo.json');
+ link.setAttribute('href', href);
+ document.querySelector('body').appendChild(link);
+ link.click();
+ link.remove();
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoCustomList.js b/app/tools/tasks/scripts/TodoCustomList.js
new file mode 100644
index 00000000..a46e8ddb
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoCustomList.js
@@ -0,0 +1,161 @@
+import { AppDraggable } from './AppDraggable.js';
+import { AppIcon } from './AppIcon.js';
+import { TodoList } from './TodoList.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoCustomList(el) {
+ let list;
+ let editing = false;
+ let startEditing = false;
+ let saveOnBlur = true;
+
+ el.innerHTML = /* html */ `
+
+
+ `;
+
+ const titleEl = el.querySelector('.title');
+ const inputEl = el.querySelector('.input');
+ const deleteEl = el.querySelector('.delete');
+
+ AppDraggable(titleEl, {
+ dropSelector: '.todo-frame.-custom .container',
+ });
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+ TodoList(el.querySelector('.todo-list'));
+
+ titleEl.addEventListener('click', () => {
+ startEditing = true;
+ editing = true;
+ update();
+ });
+
+ deleteEl.addEventListener(
+ 'touchstart',
+ () => {
+ saveOnBlur = false;
+ },
+ { passive: true },
+ );
+
+ deleteEl.addEventListener('mousedown', () => {
+ saveOnBlur = false;
+ });
+
+ inputEl.addEventListener('blur', () => {
+ if (saveOnBlur) save();
+ saveOnBlur = true;
+ });
+
+ inputEl.addEventListener('focusOther', () => {
+ if (editing) save();
+ });
+
+ inputEl.addEventListener('keyup', (e) => {
+ switch (e.keyCode) {
+ case 13: // enter
+ save();
+ break;
+ case 27: // escape
+ cancelEdit();
+ break;
+ }
+ });
+
+ deleteEl.addEventListener('click', () => {
+ if (list.items.length > 0) {
+ if (
+ !confirm(
+ 'Deleting this list will delete its items as well. Are you sure?',
+ )
+ ) {
+ return;
+ }
+ }
+
+ el.dispatchEvent(
+ new CustomEvent('deleteCustomTodoList', {
+ detail: list,
+ bubbles: true,
+ }),
+ );
+ });
+
+ el.addEventListener('draggableStart', (e) => {
+ if (e.target !== titleEl) return;
+
+ e.detail.data.list = list;
+ e.detail.data.key = list.id;
+
+ // Update image (default would only be title element).
+ e.detail.setImage(el);
+
+ // Override for horizontal dragging only.
+ e.detail.image.addEventListener('draggableDrag', (e) => {
+ const x = e.detail.clientX - e.detail.imageX;
+ const y = e.detail.originY - e.detail.imageY;
+ e.detail.image.style.transform = `translate(${x}px, ${y}px)`;
+ });
+ });
+
+ el.addEventListener('addTodoItem', (e) => {
+ e.detail.listId = list.id;
+ });
+
+ el.addEventListener('moveTodoItem', (e) => {
+ e.detail.listId = list.id;
+ e.detail.index = e.detail.index ?? 0;
+ });
+
+ el.addEventListener('customTodoList', (e) => {
+ list = e.detail;
+ update();
+ });
+
+ function save() {
+ el.dispatchEvent(
+ new CustomEvent('editCustomTodoList', {
+ detail: { ...list, title: inputEl.value.trim() },
+ bubbles: true,
+ }),
+ );
+ editing = false;
+ update();
+ }
+
+ function cancelEdit() {
+ saveOnBlur = false;
+ editing = false;
+ update();
+ }
+
+ function update() {
+ titleEl.innerText = list.title || '...';
+
+ el.querySelector('.todo-list').dispatchEvent(
+ new CustomEvent('todoItems', { detail: list.items }),
+ );
+
+ el.querySelector('.todo-list > .todo-item-input').dataset.key =
+ `todo-item-input${list.id}`;
+
+ el.classList.toggle('-editing', editing);
+
+ if (editing && startEditing) {
+ inputEl.value = list.title;
+ inputEl.focus();
+ inputEl.select();
+ startEditing = false;
+ }
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoDay.js b/app/tools/tasks/scripts/TodoDay.js
new file mode 100644
index 00000000..1f3c16de
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoDay.js
@@ -0,0 +1,52 @@
+import { TodoList } from './TodoList.js';
+import { formatDate, formatDayOfWeek } from './util.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoDay(el) {
+ const dateId = el.dataset.key;
+ let items = [];
+
+ el.innerHTML = /* html */ `
+
+
+ `;
+
+ TodoList(el.querySelector('.todo-list'));
+
+ el.addEventListener('addTodoItem', (e) => {
+ e.detail.listId = dateId;
+ });
+
+ el.addEventListener('moveTodoItem', (e) => {
+ e.detail.listId = dateId;
+ e.detail.index = e.detail.index ?? 0;
+ });
+
+ el.addEventListener('todoDay', (e) => {
+ items = e.detail.items;
+ update();
+ });
+
+ function update() {
+ const date = new Date(`${dateId}T00:00:00`);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const tomorrow = new Date(today);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+
+ el.classList.toggle('-past', date < today);
+ el.classList.toggle('-today', date >= today && date < tomorrow);
+ el.classList.toggle('-future', date >= tomorrow);
+
+ el.querySelector('.header > .dayofweek').innerText = formatDayOfWeek(date);
+ el.querySelector('.header > .date').innerText = formatDate(date);
+ el.querySelector('.todo-list').dispatchEvent(
+ new CustomEvent('todoItems', { detail: items }),
+ );
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoFrameCustom.js b/app/tools/tasks/scripts/TodoFrameCustom.js
new file mode 100644
index 00000000..76165d57
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoFrameCustom.js
@@ -0,0 +1,133 @@
+import { AppIcon } from './AppIcon.js';
+import { AppSortable } from './AppSortable.js';
+import { TodoCustomList } from './TodoCustomList.js';
+import { TodoLogic } from './TodoLogic.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoFrameCustom(el) {
+ let todoData = TodoLogic.initTodoData();
+
+ el.innerHTML = /* html */ `
+
+
+
+ `;
+
+ AppSortable(el.querySelector('.container'), { direction: 'horizontal' });
+
+ setTimeout(() => el.classList.add('-animated'), 200);
+
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ el.querySelector('.back').addEventListener('click', () =>
+ el.dispatchEvent(
+ new CustomEvent('seekCustomTodoLists', { detail: -1, bubbles: true }),
+ ),
+ );
+
+ el.querySelector('.forward').addEventListener('click', () =>
+ el.dispatchEvent(
+ new CustomEvent('seekCustomTodoLists', { detail: 1, bubbles: true }),
+ ),
+ );
+
+ el.querySelector('.add').addEventListener('click', () => {
+ el.dispatchEvent(new CustomEvent('addCustomTodoList', { bubbles: true }));
+ // TODO seek if not at end
+ });
+
+ el.addEventListener('sortableDrop', (e) => {
+ if (!e.detail.data.list) return;
+
+ el.dispatchEvent(
+ new CustomEvent('moveCustomTodoList', {
+ detail: {
+ ...e.detail.data.list,
+ index: e.detail.index,
+ },
+ bubbles: true,
+ }),
+ );
+ });
+
+ el.addEventListener('draggableOver', (e) => {
+ if (e.detail.data.list) updatePositions();
+ });
+
+ el.addEventListener('todoData', (e) => {
+ todoData = e.detail;
+ update();
+ });
+
+ function update() {
+ const customLists = TodoLogic.getCustomTodoLists(todoData);
+
+ const container = el.querySelector('.container');
+ const obsolete = new Set(container.children);
+ const childrenByKey = new Map();
+
+ obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
+
+ const children = customLists.map((list) => {
+ let child = childrenByKey.get(list.id);
+
+ if (child) {
+ obsolete.delete(child);
+ } else {
+ child = document.createElement('div');
+ child.className = 'card todo-custom-list';
+ child.dataset.key = list.id;
+ TodoCustomList(child);
+ }
+
+ child.dispatchEvent(new CustomEvent('customTodoList', { detail: list }));
+
+ return child;
+ });
+
+ obsolete.forEach((child) => container.removeChild(child));
+
+ children.forEach((child, index) => {
+ if (child !== container.children[index]) {
+ container.insertBefore(child, container.children[index]);
+ }
+ });
+
+ updatePositions();
+ updateHeight();
+ }
+
+ function updatePositions() {
+ el.querySelectorAll('.container > *').forEach((child, index) => {
+ child.style.transform = `translateX(${
+ (index - todoData.customAt) * 100
+ }%)`;
+ });
+ }
+
+ function updateHeight() {
+ let height = 280;
+ const container = el.querySelector('.container');
+
+ for (let i = 0, l = container.children.length; i < l; ++i) {
+ container.children[i].style.height = `auto`;
+ height = Math.max(container.children[i].offsetHeight, height);
+ }
+
+ el.style.height = `${height + 80}px`;
+
+ for (let i = 0, l = container.children.length; i < l; ++i) {
+ container.children[i].style.height = `${height + 30}px`;
+ }
+
+ // Update collapsible on changing heights
+ el.dispatchEvent(new CustomEvent('collapse', { bubbles: true }));
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoFrameDays.js b/app/tools/tasks/scripts/TodoFrameDays.js
new file mode 100644
index 00000000..0b611b17
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoFrameDays.js
@@ -0,0 +1,121 @@
+import { AppIcon } from './AppIcon.js';
+import { TodoDay } from './TodoDay.js';
+import { TodoLogic } from './TodoLogic.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoFrameDays(el) {
+ const RANGE = 14;
+ let todoData = TodoLogic.initTodoData();
+
+ el.innerHTML = /* html */ `
+
+
+
+ `;
+
+ setTimeout(() => el.classList.add('-animated'), 200);
+
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ el.querySelector('.backward').addEventListener('click', () =>
+ el.dispatchEvent(
+ new CustomEvent('seekDays', { detail: -1, bubbles: true }),
+ ),
+ );
+
+ el.querySelector('.forward').addEventListener('click', () =>
+ el.dispatchEvent(new CustomEvent('seekDays', { detail: 1, bubbles: true })),
+ );
+
+ el.querySelector('.fastbackward').addEventListener('click', () =>
+ el.dispatchEvent(
+ new CustomEvent('seekDays', { detail: -5, bubbles: true }),
+ ),
+ );
+
+ el.querySelector('.fastforward').addEventListener('click', () =>
+ el.dispatchEvent(new CustomEvent('seekDays', { detail: 5, bubbles: true })),
+ );
+
+
+
+ el.addEventListener('todoData', (e) => {
+ todoData = e.detail;
+ update();
+ });
+
+ function update() {
+ const listsByDay = TodoLogic.getTodoListsByDay(todoData, RANGE);
+
+ const container = el.querySelector('.container');
+ const obsolete = new Set(container.children);
+ const childrenByKey = new Map();
+
+ obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
+
+ const children = listsByDay.map((day) => {
+ let child = childrenByKey.get(day.id);
+
+ if (child) {
+ obsolete.delete(child);
+ } else {
+ child = document.createElement('div');
+ child.className = 'card todo-day';
+ child.dataset.key = day.id;
+ TodoDay(child);
+ }
+
+ child.dispatchEvent(new CustomEvent('todoDay', { detail: day }));
+ child.style.transform = `translateX(${day.position * 100}%)`;
+
+ return child;
+ });
+
+ obsolete.forEach((child) => container.removeChild(child));
+
+ children.forEach((child, index) => {
+ if (child !== container.children[index]) {
+ container.insertBefore(child, container.children[index]);
+ }
+ });
+
+ updateHeight();
+ }
+
+ function updateHeight() {
+ let height = 280;
+ const container = el.querySelector('.container');
+
+ for (let i = 0, l = container.children.length; i < l; ++i) {
+ height = Math.max(container.children[i].offsetHeight, height);
+ }
+
+ el.style.height = `${height + 50}px`;
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoItem.js b/app/tools/tasks/scripts/TodoItem.js
new file mode 100644
index 00000000..6efddf8d
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoItem.js
@@ -0,0 +1,160 @@
+import { AppDraggable } from './AppDraggable.js';
+import { AppIcon } from './AppIcon.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoItem(el) {
+ let item;
+ let editing = false;
+ let startEditing = false;
+ let saveOnBlur = true;
+
+ el.innerHTML = /* html */ `
+
+
+
+
+
+
+
+
+ `;
+
+ const checkboxEl = el.querySelector('.checkbox');
+ const labelEl = el.querySelector('.label');
+ const inputEl = el.querySelector('.input');
+ const saveEl = el.querySelector('.save');
+
+ AppDraggable(el, {
+ dropSelector: '.todo-list > .items',
+ });
+
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ checkboxEl.addEventListener(
+ 'touchstart',
+ () => {
+ saveOnBlur = false;
+ },
+ { passive: true },
+ );
+
+ checkboxEl.addEventListener('mousedown', () => {
+ saveOnBlur = false;
+ });
+
+ checkboxEl.addEventListener('click', () => {
+ if (editing) save();
+
+ el.dispatchEvent(
+ new CustomEvent('checkTodoItem', {
+ detail: {
+ ...item,
+ done: !item.done,
+ },
+ bubbles: true,
+ }),
+ );
+ });
+
+ labelEl.addEventListener('click', () => {
+ startEditing = true;
+ editing = true;
+ update();
+ });
+
+ inputEl.addEventListener('keyup', (e) => {
+ switch (e.keyCode) {
+ case 13: // Enter
+ save();
+ break;
+ case 27: // Escape
+ cancelEdit();
+ break;
+ }
+ });
+
+ inputEl.addEventListener('blur', () => {
+ if (saveOnBlur) save();
+ saveOnBlur = true;
+ });
+
+ inputEl.addEventListener('focusOther', () => {
+ if (editing) save();
+ });
+
+ saveEl.addEventListener('mousedown', () => {
+ saveOnBlur = false;
+ });
+
+ saveEl.addEventListener('click', save);
+
+ el.addEventListener('draggableStart', (e) => {
+ e.detail.data.item = item;
+ e.detail.data.key = item.id;
+ });
+
+ el.addEventListener('todoItem', (e) => {
+ item = e.detail;
+ update();
+ });
+
+ function save() {
+ const label = inputEl.value.trim();
+
+ if (label === '') {
+ // Deferred deletion prevents a bug at reconciliation in TodoList:
+ // Failed to execute 'removeChild' on 'Node': The node to be removed is
+ // no longer a child of this node. Perhaps it was moved in a 'blur'
+ // event handler?
+ requestAnimationFrame(() => {
+ el.dispatchEvent(
+ new CustomEvent('deleteTodoItem', {
+ detail: item,
+ bubbles: true,
+ }),
+ );
+ });
+
+ return;
+ }
+
+ el.dispatchEvent(
+ new CustomEvent('editTodoItem', {
+ detail: {
+ ...item,
+ label,
+ },
+ bubbles: true,
+ }),
+ );
+
+ editing = false;
+ update();
+ }
+
+ function cancelEdit() {
+ saveOnBlur = false;
+ editing = false;
+ update();
+ }
+
+ function update() {
+ el.classList.toggle('-done', item.done);
+ checkboxEl.querySelector('input').checked = item.done;
+ labelEl.innerText = item.label;
+
+ el.classList.toggle('-editing', editing);
+ el.classList.toggle('_nodrag', editing);
+
+ if (editing && startEditing) {
+ inputEl.value = item.label;
+ inputEl.focus();
+ inputEl.select();
+ startEditing = false;
+ }
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoItemInput.js b/app/tools/tasks/scripts/TodoItemInput.js
new file mode 100644
index 00000000..efbaa18b
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoItemInput.js
@@ -0,0 +1,67 @@
+import { AppIcon } from './AppIcon.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoItemInput(el) {
+ let saveOnBlur = true;
+
+ el.innerHTML = /* html */ `
+
+
+ `;
+
+ const inputEl = el.querySelector('.input');
+ const saveEl = el.querySelector('.save');
+
+ el.querySelectorAll('.app-icon').forEach(AppIcon);
+
+ inputEl.addEventListener('keyup', (e) => {
+ switch (e.keyCode) {
+ case 13: // Enter
+ save();
+ break;
+ case 27: // Escape
+ clear();
+ break;
+ }
+ });
+
+ inputEl.addEventListener('blur', () => {
+ if (saveOnBlur) save();
+ saveOnBlur = true;
+ });
+
+ inputEl.addEventListener('focusOther', save);
+
+ saveEl.addEventListener('mousedown', () => {
+ saveOnBlur = false;
+ });
+
+ saveEl.addEventListener('click', () => {
+ save();
+ inputEl.focus();
+ });
+
+ function save() {
+ const label = inputEl.value.trim();
+
+ if (label === '') return;
+
+ inputEl.value = '';
+
+ el.dispatchEvent(
+ new CustomEvent('addTodoItem', {
+ detail: { label },
+ bubbles: true,
+ }),
+ );
+ }
+
+ function clear() {
+ inputEl.value = '';
+ inputEl.blur();
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoList.js b/app/tools/tasks/scripts/TodoList.js
new file mode 100644
index 00000000..0de5ed0d
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoList.js
@@ -0,0 +1,68 @@
+import { AppSortable } from './AppSortable.js';
+import { TodoItem } from './TodoItem.js';
+import { TodoItemInput } from './TodoItemInput.js';
+
+/**
+ * @param {HTMLElement} el
+ */
+export function TodoList(el) {
+ let items = [];
+
+ el.innerHTML = /* html */ `
+
+
+ `;
+
+ AppSortable(el.querySelector('.items'), {});
+ TodoItemInput(el.querySelector('.todo-item-input'));
+
+ el.addEventListener('sortableDrop', (e) =>
+ el.dispatchEvent(
+ new CustomEvent('moveTodoItem', {
+ detail: {
+ ...e.detail.data.item,
+ index: e.detail.index,
+ },
+ bubbles: true,
+ }),
+ ),
+ );
+
+ el.addEventListener('todoItems', (e) => {
+ items = e.detail;
+ update();
+ });
+
+ function update() {
+ const container = el.querySelector('.items');
+ const obsolete = new Set(container.children);
+ const childrenByKey = new Map();
+
+ obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
+
+ const children = items.map((item) => {
+ let child = childrenByKey.get(item.id);
+
+ if (child) {
+ obsolete.delete(child);
+ } else {
+ child = document.createElement('div');
+ child.classList.add('todo-item');
+ child.dataset.key = item.id;
+ TodoItem(child);
+ }
+
+ child.dispatchEvent(new CustomEvent('todoItem', { detail: item }));
+
+ return child;
+ });
+
+ obsolete.forEach((child) => container.removeChild(child));
+
+ children.forEach((child, index) => {
+ if (child !== container.children[index]) {
+ container.insertBefore(child, container.children[index]);
+ }
+ });
+ }
+}
diff --git a/app/tools/tasks/scripts/TodoLogic.js b/app/tools/tasks/scripts/TodoLogic.js
new file mode 100644
index 00000000..7bcb9d3a
--- /dev/null
+++ b/app/tools/tasks/scripts/TodoLogic.js
@@ -0,0 +1,357 @@
+import { formatDateId } from './util.js';
+import { uuid } from './uuid.js';
+
+/**
+ * @typedef {{
+ * id: string;
+ * listId: string;
+ * index: number;
+ * label: string;
+ * done: boolean;
+ * fixed: boolean;
+ * }} TodoDataItem
+ */
+
+/**
+ * @typedef {{
+ * id: string;
+ * index: number;
+ * title: string;
+ * }} TodoDataCustomList
+ */
+
+/**
+ * @typedef {{
+ * items: TodoDataItem[];
+ * customLists: TodoDataCustomList[];
+ * at: string;
+ * customAt: number;
+ * }} TodoData
+ */
+
+export class TodoLogic {
+ /**
+ * @param {Date} now
+ * @returns {TodoData}
+ */
+ static initTodoData(now = new Date()) {
+ return {
+ items: [],
+ customLists: [],
+ at: formatDateId(now),
+ customAt: 0,
+ };
+ }
+
+ static getTodoListsByDay(data, range) {
+ const listsByDay = [];
+
+ for (let i = 0; i < 2 * range; ++i) {
+ const t = new Date(`${data.at}T00:00:00`);
+ t.setDate(t.getDate() - range + i);
+ const id = formatDateId(t);
+
+ listsByDay.push({
+ id,
+ items: TodoLogic.getTodoItemsForList(data, id),
+ position: -range + i,
+ });
+ }
+
+ return listsByDay;
+ }
+
+ static getTodoItemsForList(data, listId) {
+ return data.items
+ .filter((item) => item.listId === listId)
+ .sort((a, b) => a.index - b.index);
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{listId: string, label: string}} input
+ * @returns {TodoData}
+ */
+ static addTodoItem(data, input, now = new Date()) {
+ let index = 0;
+
+ for (const item of data.items) {
+ if (item.listId === input.listId) {
+ index = Math.max(index, item.index + 1);
+ }
+ }
+
+ return {
+ ...data,
+ items: [
+ ...data.items,
+ {
+ ...input,
+ id: uuid(),
+ index,
+ done: false,
+ fixed: this.isListInThePast(input.listId, now),
+ },
+ ],
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string, done: boolean}} input
+ * @returns {TodoData}
+ */
+ static checkTodoItem(data, input) {
+ return {
+ ...data,
+ items: data.items.map((item) =>
+ item.id === input.id ? { ...item, done: input.done } : item,
+ ),
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string, label: string}} input
+ * @returns {TodoData}
+ */
+ static editTodoItem(data, input) {
+ return {
+ ...data,
+ items: data.items.map((item) =>
+ item.id === input.id ? { ...item, label: input.label } : item,
+ ),
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string, listId: string, index: number}} input
+ * @returns {TodoData}
+ */
+ static moveTodoItem(data, input, now = new Date()) {
+ const itemToMove = data.items.find((item) => item.id === input.id);
+
+ if (!itemToMove) return data;
+
+ // Reinsert item at target list and index
+ let list = data.items.filter(
+ (item) => item.listId === input.listId && item.id !== input.id,
+ );
+ list.splice(input.index, 0, {
+ ...itemToMove,
+ listId: input.listId,
+ fixed: this.isListInThePast(input.listId, now),
+ });
+ list = TodoLogic.setIndexes(list);
+
+ // Reinsert updated list
+ let items = data.items.filter(
+ (item) => item.listId !== input.listId && item.id !== input.id,
+ );
+ items = [...items, ...list];
+
+ return {
+ ...data,
+ items,
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string}} input
+ * @returns {TodoData}
+ */
+ static deleteTodoItem(data, input) {
+ return {
+ ...data,
+ items: data.items.filter((item) => item.id !== input.id),
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {Date} now
+ * @returns {TodoData}
+ */
+ static movePastTodoItems(data, now = new Date()) {
+ const todayListId = formatDateId(now);
+
+ let targetIndex = 0;
+
+ for (const item of data.items) {
+ if (item.listId === todayListId && item.index > targetIndex) {
+ targetIndex = item.index;
+ }
+ }
+
+ return {
+ ...data,
+ items: data.items.map((item) => {
+ if (
+ !item.done &&
+ !item.fixed &&
+ this.isListInThePast(item.listId, now)
+ ) {
+ return { ...item, listId: todayListId, index: targetIndex++ };
+ }
+
+ return item;
+ }),
+ };
+ }
+
+ static isListInThePast(listId, now = new Date()) {
+ const todayListId = formatDateId(now);
+
+ return listId.match(/\d\d\d\d-\d\d-\d\d/) && listId < todayListId;
+ }
+
+ //
+
+ static getCustomTodoLists(data) {
+ return data.customLists
+ .map((list) => ({
+ id: list.id,
+ index: list.index,
+ title: list.title,
+ items: TodoLogic.getTodoItemsForList(data, list.id),
+ }))
+ .sort((a, b) => a.index - b.index);
+ }
+
+ /**
+ * @param {TodoData} data
+ * @returns {TodoData}
+ */
+ static addCustomTodoList(data) {
+ let index = 0;
+
+ for (const customList of data.customLists) {
+ index = Math.max(index, customList.index + 1);
+ }
+
+ return {
+ ...data,
+ customLists: [
+ ...data.customLists,
+ {
+ id: uuid(),
+ index,
+ title: '',
+ },
+ ],
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string, title: string}} input
+ * @returns {TodoData}
+ */
+ static editCustomTodoList(data, input) {
+ return {
+ ...data,
+ customLists: data.customLists.map((customList) =>
+ customList.id === input.id
+ ? { ...customList, title: input.title }
+ : customList,
+ ),
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string, index: number}} input
+ * @returns {TodoData}
+ */
+ static moveCustomTodoList(data, input) {
+ const customListToMove = data.customLists.find(
+ (customList) => customList.id === input.id,
+ );
+
+ let customLists = data.customLists
+ .filter((customList) => customList.id !== input.id)
+ .sort((a, b) => a.index - b.index);
+ customLists.splice(input.index, 0, customListToMove);
+ customLists = TodoLogic.setIndexes(customLists);
+
+ return {
+ ...data,
+ customLists,
+ };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {{id: string}} input
+ * @returns {TodoData}
+ */
+ static deleteCustomTodoList(data, input) {
+ return {
+ ...data,
+ customLists: data.customLists.filter(
+ (customList) => customList.id !== input.id,
+ ),
+ };
+ }
+
+ //
+
+ /**
+ * @param {TodoData} data
+ * @param {number} delta
+ * @returns {TodoData}
+ */
+ static seekDays(data, delta) {
+ const t = new Date(`${data.at}T00:00:00`);
+ t.setDate(t.getDate() + delta);
+
+ return { ...data, at: formatDateId(t) };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @returns {TodoData}
+ */
+ static seekToToday(data) {
+ return { ...data, at: formatDateId(new Date()) };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {Date} date
+ * @returns {TodoData}
+ */
+ static seekToDate(data, date) {
+ return { ...data, at: formatDateId(date) };
+ }
+
+ /**
+ * @param {TodoData} data
+ * @param {number} delta
+ * @returns {TodoData}
+ */
+ static seekCustomTodoLists(data, delta) {
+ return {
+ ...data,
+ customAt: Math.max(
+ 0,
+ Math.min(data.customLists.length - 1, data.customAt + delta),
+ ),
+ };
+ }
+
+ //
+
+ /**
+ * @template {{index?: number}} T
+ * @param {T[]} array
+ * @returns {T[]}
+ */
+ static setIndexes(array) {
+ return array.map((item, index) =>
+ item.index === index ? item : { ...item, index },
+ );
+ }
+}
diff --git a/app/tools/tasks/scripts/util.js b/app/tools/tasks/scripts/util.js
new file mode 100644
index 00000000..2bda634e
--- /dev/null
+++ b/app/tools/tasks/scripts/util.js
@@ -0,0 +1,99 @@
+/**
+ * @param {Date} date
+ * @returns {string}
+ */
+export function formatDateId(date) {
+ const y = date.getFullYear();
+ const m = date.getMonth() + 1;
+ const d = date.getDate();
+ const ys = y.toString().padStart(4, '0');
+ const ms = m.toString().padStart(2, '0');
+ const ds = d.toString().padStart(2, '0');
+
+ return `${ys}-${ms}-${ds}`;
+}
+
+/**
+ * @param {Date} date
+ * @returns {string}
+ */
+export function formatDate(date) {
+ const m = formatMonth(date);
+ const d = formatDayOfMonth(date);
+ const y = date.getFullYear().toString().padStart(4, '0');
+
+ return `${m} ${d} ${y}`;
+}
+
+/**
+ * @param {Date} date
+ * @returns {string}
+ */
+export function formatDayOfMonth(date) {
+ const d = date.getDate();
+ const t = d % 10;
+
+ return d === 11 || d === 12 || d === 13
+ ? `${d}th`
+ : t === 1
+ ? `${d}st`
+ : t === 2
+ ? `${d}nd`
+ : t === 3
+ ? `${d}rd`
+ : `${d}th`;
+}
+
+export const DAY_NAMES = [
+ 'Sunday',
+ 'Monday',
+ 'Tuesday',
+ 'Wednesday',
+ 'Thursday',
+ 'Friday',
+ 'Saturday',
+];
+
+/**
+ * @param {Date} date
+ * @returns {string}
+ */
+export function formatDayOfWeek(date) {
+ return DAY_NAMES[date.getDay()];
+}
+
+export const MONTH_NAMES = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+];
+
+/**
+ * @param {Date} date
+ * @returns {string}
+ */
+export function formatMonth(date) {
+ return MONTH_NAMES[date.getMonth()];
+}
+
+/**
+ * https://developer.mozilla.org/en-US/docs/Glossary/Base64
+ * @param {BlobPart} input
+ */
+export async function toDataURL(input, type) {
+ return await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.addEventListener('load', () => resolve(reader.result));
+ reader.addEventListener('error', () => reject(reader.error));
+ reader.readAsDataURL(new File([input], '', { type }));
+ });
+}
diff --git a/app/tools/tasks/scripts/uuid.js b/app/tools/tasks/scripts/uuid.js
new file mode 100644
index 00000000..facb3697
--- /dev/null
+++ b/app/tools/tasks/scripts/uuid.js
@@ -0,0 +1,8 @@
+export function uuid() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0,
+ v = c === 'x' ? r : (r & 0x3) | 0x8;
+
+ return v.toString(16);
+ });
+}
diff --git a/app/tools/tasks/styles/app-tasks.css b/app/tools/tasks/styles/app-tasks.css
new file mode 100644
index 00000000..9dd9361f
--- /dev/null
+++ b/app/tools/tasks/styles/app-tasks.css
@@ -0,0 +1,212 @@
+/* app header */
+
+.app-header {
+ background: var(--header-bg);
+ padding: 10px 20px;
+ position: relative;
+ user-select: none;
+ }
+
+ .app-header > .title {
+ color: var(--header-text);
+ font-size: 1.25rem;
+ font-weight: 400;
+ line-height: 1.25em;
+ margin: 0;
+ }
+
+ .app-header > .actions {
+ position: absolute;
+ right: 1.5rem;
+ top: .5rem;
+ margin: 0;
+ }
+
+/* app collapsible */
+.app-collapsible > .bar {
+ height: 32px;
+ line-height: 29px;
+ margin: 0;
+ padding: 0 0.75em;
+ background: var(--bar-active-bg);
+ }
+
+ .app-collapsible.-animated > .body {
+ transition: height 0.2s ease-out;
+ overflow: hidden;
+ }
+/* app date-picket */
+.app-date-picker {
+ position: absolute;
+ top: 3rem;
+ right: 1.5rem;
+ display:none;
+ width: 260px;
+ background: #263239; /* var(--main-bg); */
+ border-radius: 4px;
+ box-shadow: rgb(0 0 0 / 10%) 0 4px 12px;
+ padding: 8px;
+ transform: translate(0, -110%);
+ transition: all 0.2s ease-in-out;
+ text-align: center;
+ user-select: none;
+ z-index: 99;
+ }
+
+ .app-date-picker.-show {
+ display:block;
+ transform: translate(0, 0);
+ }
+
+ .app-date-picker > .header {
+ display: flex;
+ font-size: 1em;
+ margin: 0 0 1em;
+ line-height: 1.5em;
+ }
+
+ .app-date-picker > .header > .month {
+ flex-grow: 1;
+ font-weight: bold;
+ text-transform: uppercase;
+ }
+
+ .app-date-picker > .dates {
+ width: 100%;
+ }
+
+ /* stylelint-disable-next-line rscss/class-format */
+ .app-date-picker > .dates > thead > tr > th {
+ font-weight: normal;
+ text-transform: uppercase;
+ padding-bottom: 0.4em;
+ }
+
+ /* stylelint-disable-next-line rscss/class-format */
+ .app-date-picker > .dates > tbody > tr > td {
+ padding: 0;
+ }
+
+ /* stylelint-disable-next-line rscss/class-format */
+ .app-date-picker > .dates > tbody > tr > td > button {
+ width: 100%;
+ height: 1.9em;
+ }
+
+ @media (width >= 320px) {
+ .app-date-picker {
+ width: 300px;
+ }
+ }
+
+/* app buttons */
+.app-button,
+label.app-button {
+ background: transparent;
+ border: 0;
+ border-radius: 4px;
+ color: var(--button-text);
+ cursor: pointer;
+ display: inline-block;
+ font-size: 1em;
+ line-height: 1em;
+ margin: 0;
+ padding: 0.25em;
+ outline: 0;
+ transition: all 0.1s ease-out;
+ vertical-align: middle;
+}
+
+.app-button:hover {
+ color: var(--button-active-text);
+}
+
+.app-button:active {
+ transform: translate(0, 1px);
+ color: var(--button-active-text);
+ background: var(--button-active-bg);
+}
+
+.app-button:focus {
+ color: var(--button-active-text);
+}
+
+.app-button.rounded-md {
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: .5rem;
+}
+
+.app-button.-xl {
+ font-size: 1.5em;
+}
+
+.app-button.-highlight {
+ color: var(--button-highlight-text);
+ font-weight: bold;
+}
+
+.app-button.-highlight:hover {
+ color: var(--button-active-text);
+}
+
+@media (width >= 600px) {
+ .app-button.-xl {
+ font-size: 2em;
+ }
+}
+
+/* app icons */
+.app-icon {
+ display: inline-block;
+ vertical-align: baseline;
+ height: 1em;
+ }
+
+ .app-icon > svg {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ fill: currentcolor;
+ transition: transform 0.1s ease-out;
+ position: relative;
+ top: -0.1em;
+ }
+
+ .icon-codemo {
+ background-color:currentColor;
+ display:inline-block;
+ width:1em;
+ height:1em;
+ mask-image:var(--svg);
+ mask-repeat:no-repeat;
+ mask-size:100% 100%;
+ vertical-align:-.175em;
+ --svg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M12 0h-.079c-1.66 0-3.239.349-4.667.978l.074-.029A12.269 12.269 0 0 0 3.52 3.523A12.219 12.219 0 0 0 .978 7.251l-.031.079A11.398 11.398 0 0 0 0 11.919v.086v-.004v.079c0 1.66.349 3.239.978 4.667l-.029-.074a12.276 12.276 0 0 0 2.572 3.807a12.224 12.224 0 0 0 3.729 2.542l.079.031c1.354.6 2.933.949 4.593.949h.083h-.004h.079c1.66 0 3.239-.349 4.667-.978l-.074.029a12.276 12.276 0 0 0 3.809-2.573a12.219 12.219 0 0 0 2.542-3.728l.031-.079c.6-1.354.949-2.932.949-4.593v-.158c0-1.66-.349-3.239-.978-4.667l.029.074a12.286 12.286 0 0 0-2.573-3.806A12.219 12.219 0 0 0 16.754.981L16.675.95C15.321.35 13.741 0 12.08 0h-.083zm.64 22.79v-2.087l5.193-2.633a.421.421 0 0 0 .154-.129l.001-.001a.335.335 0 0 0 .059-.191v-.011v.001v-1.186c.225-.082.412-.226.543-.412l.002-.004c.133-.179.214-.404.214-.648v-.118a1.146 1.146 0 0 0-.366-.746l-.001-.001a1.09 1.09 0 0 0-.75-.297h-.022h.001h-.02c-.308 0-.587.127-.786.332c-.205.2-.332.478-.332.787v.021v-.001v.059c.002.03.011.057.024.081v-.001c.013.222.096.423.227.583l-.001-.002c.133.163.304.289.501.364l.008.003v.949l-4.649 2.372v-1.942l2.039-.949a.533.533 0 0 0 .166-.13l.001-.001a.3.3 0 0 0 .071-.194v-3.233l3.793-2.134a.318.318 0 0 0 .142-.141l.001-.002a.407.407 0 0 0 .047-.189v-.901c.223-.079.409-.218.543-.397l.002-.003c.133-.177.214-.401.214-.644v-.04c0-.308-.127-.587-.332-.786a1.066 1.066 0 0 0-.775-.332H18.4a1.082 1.082 0 0 0-.71.366l-.001.001a1.113 1.113 0 0 0-.285.746v.025v-.001v.02c0 .243.08.466.216.646l-.002-.003c.137.182.322.321.538.397l.008.003v.688l-3.818 2.134a.51.51 0 0 0-.129.129l-.001.002a.338.338 0 0 0-.055.184v.017v-.001v3.2l-1.52.711v-6.972l3.2-1.566a.379.379 0 0 0 .153-.141l.001-.002a.347.347 0 0 0 .059-.189V7.04c.223-.079.409-.218.543-.397l.002-.003c.133-.177.214-.401.214-.644v-.021v.001v-.061a.078.078 0 0 0-.024-.057a1.127 1.127 0 0 0-.366-.719l-.001-.001a1.06 1.06 0 0 0-.738-.297h-.128a1.108 1.108 0 0 0-.719.378l-.001.001a1.103 1.103 0 0 0-.297.754v.123c.032.227.12.428.251.596l-.002-.003c.127.167.301.291.502.354l.007.002v.972l-2.656 1.304V5.288c.225-.082.412-.226.543-.412l.002-.004c.133-.179.214-.404.214-.648V4.2c0-.308-.127-.587-.332-.786a1.095 1.095 0 0 0-.787-.332h-.021h.001h-.02c-.308 0-.587.127-.786.332c-.205.2-.332.478-.332.787v.021v-.001v.118c.03.223.119.421.25.583l-.002-.002c.133.163.304.289.501.364l.008.003v6.569l-1.874-.996V7.865a.292.292 0 0 0-.048-.16l.001.001a.87.87 0 0 0-.095-.119l-1.306-.998a.825.825 0 0 0 .07-.196l.001-.006c.015-.067.024-.143.024-.222v-.024c0-.308-.127-.587-.332-.786a1.097 1.097 0 0 0-.786-.331H7.52h.001h-.02c-.308 0-.587.127-.786.332c-.205.2-.332.478-.332.787v.021v-.001v.024c0 .305.125.581.326.78c.2.205.478.332.787.332h.021h-.001h.015a.858.858 0 0 0 .288-.049l-.006.002c.11-.041.2-.081.287-.125l-.015.007l1.162.925v3.035a.29.29 0 0 0 .06.178l-.001-.001a.42.42 0 0 0 .152.129l.002.001l2.419 1.28V15.1l-4.055-1.874l.071-1.47v-.024a.29.29 0 0 0-.06-.178l.001.001a.42.42 0 0 0-.152-.129l-.002-.001l-1.851-.97a.522.522 0 0 0 .024-.157v-.2a1.127 1.127 0 0 0-.366-.719l-.001-.001a1.093 1.093 0 0 0-.752-.299h-.018h.001h-.007c-.305 0-.58.127-.775.332c-.208.2-.338.481-.338.792v.015v-.001v.118c.029.285.164.534.366.71l.001.001c.193.177.451.285.735.285h.131a1.45 1.45 0 0 0 .33-.083l-.01.003c.104-.04.195-.092.275-.156l-.002.002l1.707.88l-.047 1.47v.008c0 .075.017.145.048.208l-.001-.003a.305.305 0 0 0 .164.142l.002.001l4.577 2.134v6.869q-.308 0-.605-.024l-.605-.047l.071-4.364a.349.349 0 0 0-.06-.191l.001.001a.364.364 0 0 0-.175-.142l-.002-.001l-2.87-1.328v-.125c0-.305-.127-.58-.332-.775a1.095 1.095 0 0 0-.787-.332H6.49h.001h-.114a1.082 1.082 0 0 0-.71.366l-.001.001a1.113 1.113 0 0 0-.285.746v.025v-.001v.007c0 .305.127.58.332.775c.195.205.47.332.775.332h.039c.156 0 .305-.033.439-.094l-.007.003c.14-.067.261-.147.369-.242l-.002.002l2.656 1.21v4.008a10.611 10.611 0 0 1-3.534-1.343l.048.027a10.85 10.85 0 0 1-2.773-2.354l-.014-.017a11.1 11.1 0 0 1-1.824-3.112l-.026-.076a10.267 10.267 0 0 1-.676-3.699v-.111c0-1.494.314-2.915.88-4.201l-.026.067a11.06 11.06 0 0 1 2.324-3.44A11.019 11.019 0 0 1 7.73 2.065l.071-.028a10.269 10.269 0 0 1 4.139-.856h.061h-.003h.064c1.494 0 2.915.314 4.201.88l-.067-.026a11.078 11.078 0 0 1 3.44 2.32a11 11 0 0 1 2.296 3.369l.028.071c.54 1.218.854 2.639.854 4.134v.067v-.003v.064c0 1.444-.292 2.82-.82 4.072l.026-.069a11.1 11.1 0 0 1-2.175 3.373l.005-.006a10.852 10.852 0 0 1-3.172 2.32l-.065.028c-1.16.568-2.516.932-3.948 1.009l-.026.001z'%3E%3C/path%3E%3C/svg%3E")
+ }
+
+ .app-icon.-r180 > svg {
+ transform: rotate(180deg);
+ }
+
+ .app-icon.-double > svg:nth-child(2) {
+ margin-left: -0.5em;
+ }
+
+/* app footer */
+.app-footer {
+ padding: 2em;
+ font-size: 0.8em;
+ color: var(--aside-text);
+ }
+
+ .app-footer > p {
+ margin: 0;
+ }
+
+ /* stylelint-disable-next-line rscss/no-descendant-combinator */
+ .app-footer a {
+ color: var(--aside-text);
+ }
+
\ No newline at end of file
diff --git a/app/tools/tasks/styles/base.css b/app/tools/tasks/styles/base.css
new file mode 100644
index 00000000..fa3e5a6c
--- /dev/null
+++ b/app/tools/tasks/styles/base.css
@@ -0,0 +1,491 @@
+:root {
+ /* Named color scheme */
+ --white: #fefefe;
+ --black: #0a0a0b;
+ --grey1: #545e75;
+ --grey2: #b0b0b8;
+ --grey3: #607D8B;
+ --grey4: #e4ecf0;
+ --blue: #2597f4;
+ --red: #ce2d4f;
+ --bar: #263238;
+
+ /* UI color mapping */
+ --scrollbar-color-thumb : var(--grey1);
+ --scrollbar-color-track : transparent;
+ --scrollbar-width : thin;
+ --scrollbar-width-legacy : 8px;
+
+ --main-text: var(--white);
+ --main-bg: var(--black);
+ --main-border: var(--grey1);
+ --main-border-light: var(--grey3);
+ --header-text: var(--grey2);
+ --header-bg: var(--bar);
+ --highlight-text: var(--blue);
+ --disabled-text: var(--grey2);
+ --aside-text: var(--grey3);
+ --button-text: var(--grey3);
+ --button-bg: transparent;
+ --button-active-text: var(--white);
+ --button-active-bg: #1b2429; /* var(--grey2); */
+ --button-highlight-text: var(--blue);
+ --bar-active-bg: var(--bar);
+}
+
+html {
+ box-sizing: border-box;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+/* Modern browsers with `scrollbar-*` support */
+@supports (scrollbar-width: auto) {
+ * {
+ scrollbar-color: var(--scrollbar-color-thumb) var(--scrollbar-color-track);
+ scrollbar-width: var(--scrollbar-width);
+ }
+}
+/* Legacy browsers with `::-webkit-scrollbar-*` support */
+@supports selector(::-webkit-scrollbar) {
+ * ::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-color-thumb);
+ }
+ * ::-webkit-scrollbar-track {
+ background: var(--scrollbar-color-track);
+ }
+ * ::-webkit-scrollbar {
+ max-width: var(--scrollbar-width-legacy);
+ max-height: var(--scrollbar-width-legacy);
+ }
+}
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ background: var(--main-bg);
+ color: var(--main-text);
+}
+
+body {
+ font: normal 16px/1.5 system-ui, sans-serif;
+}
+
+
+
+/* Todo app */
+
+.todo-app {
+ overflow: hidden;
+}
+/* Todo day */
+
+.todo-day {
+ padding: 0 0.5em;
+}
+
+.todo-day > .header {
+ text-align: center;
+ padding: 2em 0;
+}
+
+.todo-day > .header > .dayofweek {
+ text-transform: uppercase;
+ margin: 0 0 0.25em;
+ font-size: 1.5em;
+}
+
+.todo-day > .header > .date {
+ text-transform: uppercase;
+ font-weight: normal;
+ margin: 0.25em 0 0;
+ font-size: 0.8em;
+ color: var(--aside-text);
+}
+
+.todo-day.-past {
+ color: var(--disabled-text);
+}
+
+.todo-day.-past > .header > .date {
+ color: var(--disabled-text);
+}
+
+.todo-day.-today > .header > .dayofweek {
+ color: var(--highlight-text);
+}
+
+.todo-day.-today > .header > .date {
+ color: var(--highlight-text);
+}
+
+
+/* Todo frame */
+
+.todo-frame {
+ position: relative;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.todo-frame > .leftcontrols,
+.todo-frame > .rightcontrols {
+ position: absolute;
+ top: 0;
+ width: 52px;
+ padding: 1.5em 0;
+ text-align: center;
+}
+
+.todo-frame > .leftcontrols {
+ left: 0;
+}
+
+.todo-frame > .rightcontrols {
+ right: 0;
+}
+
+.todo-frame > .leftcontrols > p,
+.todo-frame > .rightcontrols > p {
+ margin: 0 0 0.5em;
+}
+
+.todo-frame > .rightcontrols > .datepicker {
+ position: absolute;
+ right: 10px;
+}
+
+.todo-frame > .container {
+ position: absolute;
+ overflow: hidden;
+ inset: 0 52px;
+}
+
+.todo-frame > .container > .card {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ transition:
+ transform 0.2s ease-out,
+ opacity 0.2s ease-out;
+}
+
+.todo-frame.-animated {
+ transition: height 0.2s ease-out;
+}
+
+@media (width >= 600px) {
+ .todo-frame > .container {
+ right: 70px;
+ left: 70px;
+ }
+
+ .todo-frame > .leftcontrols,
+ .todo-frame > .rightcontrols {
+ width: 70px;
+ }
+
+ .todo-frame > .container > .card {
+ width: 50%;
+ }
+}
+
+@media (width >= 768px) {
+ .todo-frame > .container > .card {
+ width: 33.333%;
+ }
+}
+
+@media (width >= 1024px) {
+ .todo-frame > .container > .card {
+ width: 25%;
+ }
+}
+
+@media (width >= 1280px) {
+ .todo-frame > .container > .card {
+ width: 20%;
+ }
+}
+
+
+/* Todo item */
+
+.todo-list {
+ border:1px dotted var(--grey1);
+ border-radius: 5px;
+ max-height: 25vh;
+ overflow: hidden;
+ overflow-y: auto;
+ padding: .5em 0;
+ scrollbar-gutter: stable;
+}
+.todo-item {
+ position: relative;
+ font-size: 0.8em;
+ line-height: 1.5em;
+ margin: 0;
+ padding: 0.25em 0;
+ background: var(--main-bg);
+ transition:
+ transform 0.2s ease-out,
+ opacity 0.2s ease-out;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+}
+.todo-item .label {
+ cursor:
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-linecap='round' stroke-linejoin='round' d='m13.478 11.832l-.46-2.757a2.573 2.573 0 0 0-2.961-2.114l-2.171.362l-.683-4.09a1.194 1.194 0 0 0-1.374-.98v0c-.65.108-1.09.723-.98 1.374l.894 5.36l-.363.133a1.715 1.715 0 0 0-.643 2.803l.184.19l.954.988M1.75.5L.5 1.75L1.75 3M.5 1.75h3M10.25.5l1.25 1.25L10.25 3m1.25-1.25h-3'/%3E%3C/svg%3E")
+ 16 16, pointer;
+}
+
+.todo-item > .checkbox {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 30px;
+ height: 2em;
+ line-height: 2em;
+}
+
+.todo-item > .checkbox > input {
+ vertical-align: middle;
+ cursor: pointer;
+ appearance: none;
+ border: 1px solid #1b2429;
+ margin: 0;
+ font: inherit;
+ color: currentColor;
+ width: 1.15em;
+ height: 1.15em;
+ border: 1px solid #607D8B;
+ border-radius: 0.15em;
+ /* transform: translateY(0.5em); */
+ margin: .5em 0 0 .5em;
+ display: grid;
+ place-content: center;
+}
+.todo-item > .checkbox > input::before {
+ content: "";
+ width: 0.65em;
+ height: 0.65em;
+ transform: scale(0);
+ transition: 120ms transform ease-in-out;
+ box-shadow: inset 1em 1em var(--form-control-color);transform-origin: bottom left;
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+}
+.todo-item.-done > .checkbox > input::before {
+ background: var(--blue);
+ border-color:var(--blue);
+ transform: translateY(1px) scale(1);
+ transform-origin: bottom left;
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+}
+.todo-item > .checkbox > input:hover {
+}
+
+.todo-item > .label {
+ margin: 0 0 0 30px;
+ padding-bottom: 0.25em;
+ border-bottom: 1px solid var(--main-border-light);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.todo-item > .form {
+ display: none;
+ margin: 0 0 0 30px;
+ padding-right: 24px;
+ border-bottom: 1px solid var(--main-border);
+}
+
+.todo-item > .form > .input {
+ border: 0;
+ border-radius: 0;
+ outline: 0;
+ padding: 0 0 0.25em;
+ width: 100%;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: 1.5em;
+ background: transparent;
+}
+
+.todo-item > .form > .save {
+ position: absolute;
+ top: 0.5em;
+ right: 0;
+}
+
+.todo-item.-done > .label {
+ color: var(--disabled-text);
+ text-decoration: line-through;
+}
+
+.todo-item.-editing > .label {
+ display: none;
+}
+
+.todo-item.-editing > .form {
+ display: block;
+ background:var(--grey1);
+ color: var(--highlight-text);
+}
+
+
+
+.todo-item.-dragging {
+ opacity: 0.7;
+ background:var(--blue);
+ color: var(--highlight-text);
+}
+
+.todo-item.-placeholder {
+ transition: none !important;
+ visibility: hidden;
+}
+
+/* Todo item input */
+
+.todo-item-input {
+ position: relative;
+ margin: 0 0 0 30px;
+ padding: 0 30px 0 0;
+ border-bottom: 1px solid var(--main-border-light);
+ font-size: 0.8em;
+ line-height: 1.5em;
+ transition: transform 0.2s ease-out;
+}
+
+.todo-item-input > .input {
+ border: 0;
+ border-radius: 3px;
+ outline: 0;
+ padding: 0.25em 0;
+ width: 100%;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: 1.5em;
+ background: transparent;
+ color: var(--grey1);
+}
+.todo-item-input:focus-within{
+ background: #263238;
+ /* box-shadow: 0px 1px 4px 0px #607D8B, 1px 0px 4px 0px #607D8B; */
+}
+.todo-item-input:focus-within > .input:focus-visible {
+ background: #37474f;
+ color: #ccc;
+ outline-offset: 0px;
+
+}
+
+.todo-item-input > .save {
+ position: absolute;
+ top: 0.25em;
+ right: 0;
+}
+.todo-item.-editing._nodrag > .form > input[type="text"]:focus {
+ background: #263238; /* var(--grey1); */
+ color: var(--white);
+}
+
+
+/* Todo custom list */
+
+.todo-custom-list {
+ padding: 0 0.5em;
+ transition:
+ transform 0.2s ease-out,
+ opacity 0.2s ease-out,
+ box-shadow 0.2s ease-out;
+}
+
+.todo-custom-list > .header {
+ text-align: center;
+ padding: 2em 0;
+}
+
+.todo-custom-list > .header > .title {
+ margin: 0 0 10px;
+ font-size: 1.25em;
+ font-weight: 300;
+ line-height: normal;
+ cursor:
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 14 14'%3E%3Cpath fill='none' stroke='white' stroke-linecap='round' stroke-linejoin='round' d='m13.478 11.832l-.46-2.757a2.573 2.573 0 0 0-2.961-2.114l-2.171.362l-.683-4.09a1.194 1.194 0 0 0-1.374-.98v0c-.65.108-1.09.723-.98 1.374l.894 5.36l-.363.133a1.715 1.715 0 0 0-.643 2.803l.184.19l.954.988M1.75.5L.5 1.75L1.75 3M.5 1.75h3M10.25.5l1.25 1.25L10.25 3m1.25-1.25h-3'/%3E%3C/svg%3E")
+ 16 16, pointer;
+}
+
+.todo-custom-list > .header > .form {
+ display: none;
+ margin: 0 0 10px;
+ font-size: 1em;
+ line-height: 1em;
+}
+
+.todo-custom-list > .header > .form > .input {
+ border: 0;
+ border-radius: 5px;
+ font-family: inherit;
+ font-size: 1.5em;
+ font-weight: bold;
+ line-height: normal;
+ outline: 0;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 70%;
+}
+
+.todo-custom-list > .header > .form > .delete {
+ position: absolute;
+ right: 0.5em;
+ top: 2.5em;
+}
+
+.todo-custom-list.-editing > .header > .title {
+ display: none;
+}
+
+.todo-custom-list.-editing > .header > .form {
+ display: block;
+}
+.todo-custom-list.-editing > .header > .form > input[type="text"]:focus {
+ background: #263238; /* var(--bar-active-bg); */
+ color: var(--main-text);
+}
+
+.todo-custom-list.-editing input[type=text]:focus {
+ background: var(--bar-active-bg);
+ color: var(--main-text);
+}
+
+
+.todo-custom-list.-dragging {
+ box-shadow:
+ 10px 0 12px -14px rgb(0 0 0 / 30%),
+ -10px 0 12px -14px rgb(0 0 0 / 30%);
+ background: var(--blue);
+ opacity: 0.8;
+}
+
+.todo-custom-list.-placeholder {
+ transition: none !important;
+ visibility: hidden;
+}
+
+
diff --git a/app/web-app/graphite.webp b/app/web-app/graphite.webp
new file mode 100644
index 0000000000000000000000000000000000000000..ca8a3789ba1a544528d4c9c96a32eca1d267c56d
GIT binary patch
literal 29336
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
G0000wo=uGa
literal 0
HcmV?d00001
diff --git a/app/web-app/livecodes.webp b/app/web-app/livecodes.webp
new file mode 100644
index 0000000000000000000000000000000000000000..aa642bb8a19b959fdd732adfdaaabe0610e6ac9e
GIT binary patch
literal 33144
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