From a096e19e01cca261a3d9d25f4a02ab3f6d71ea2c Mon Sep 17 00:00:00 2001 From: Andrew Tatomyr Date: Tue, 9 Dec 2025 19:45:13 +0200 Subject: [PATCH 1/3] fix(todo): better handle (sub)tasks updates; revert the stale styling --- .../purity-todo/components/subtask-item.ts | 30 ++++++++++++------- .../purity-todo/components/task-item.ts | 1 + .../purity-todo/components/task-list-style.ts | 8 ----- .../purity-todo/services/task-details.ts | 11 ++++--- src/examples/purity-todo/services/tasks.ts | 12 ++++++-- src/purity.ts | 6 ++-- 6 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/examples/purity-todo/components/subtask-item.ts b/src/examples/purity-todo/components/subtask-item.ts index 8240839..fcad0aa 100644 --- a/src/examples/purity-todo/components/subtask-item.ts +++ b/src/examples/purity-todo/components/subtask-item.ts @@ -10,12 +10,15 @@ export const SMALL_BUTTON = "small-button" const toggleSubtask = (subtaskIndex: number): EventHandler => ({target: {checked}}) => { - const {id, subtasks} = selectDetailedTask() + const {id, subtasks, completed} = selectDetailedTask() + const newSubtasks = subtasks?.map((s, i) => + i === subtaskIndex ? {...s, checked: !s.checked} : s + ) + const completedSubtasks = newSubtasks?.every(s => s.checked) patchTask({ id: id, - subtasks: subtasks?.map((subtask, i) => - i === subtaskIndex ? {...subtask, checked: !subtask.checked} : subtask - ), + subtasks: newSubtasks, + completed: completedSubtasks ?? completed, }) } @@ -23,21 +26,26 @@ const handleSubtaskChange = (subtaskIndex: number): EventHandler => ({target: {value}}) => { const task = selectDetailedTask() + const newSubtasks = task.subtasks?.map((s, i) => + i === subtaskIndex ? {...s, description: sanitize(value)} : s + ) patchTask({ id: task.id, - subtasks: task.subtasks?.map((subtask, i) => - i === subtaskIndex - ? {...subtask, description: sanitize(value)} - : subtask - ), + subtasks: newSubtasks, }) } const deleteSubtask = (subtaskIndex: number): EventHandler => () => { - const {id, subtasks} = selectDetailedTask() - patchTask({id, subtasks: subtasks?.filter((_, i) => i !== subtaskIndex)}) + const {id, subtasks, completed} = selectDetailedTask() + const newSubtasks = subtasks?.filter((_, i) => i !== subtaskIndex) + const completedSubtasks = newSubtasks?.every(s => s.checked) + patchTask({ + id, + subtasks: newSubtasks, + completed: completedSubtasks ?? completed, + }) } export const subtaskItem = ( diff --git a/src/examples/purity-todo/components/task-item.ts b/src/examples/purity-todo/components/task-item.ts index 69ea6e9..9b163cc 100644 --- a/src/examples/purity-todo/components/task-item.ts +++ b/src/examples/purity-todo/components/task-item.ts @@ -59,6 +59,7 @@ export const taskItem = ({ loading="lazy" />
render` } ol#task-list .task-item.stale { - opacity: 0.4; - filter: grayscale(0.3); - transition: opacity 0.3s ease-in-out, filter 0.3s ease-in-out; - } - - ol#task-list .task-item.stale:hover { - opacity: 0.7; - filter: grayscale(0.1); } diff --git a/src/examples/purity-todo/services/task-details.ts b/src/examples/purity-todo/services/task-details.ts index deb8898..bec34ae 100644 --- a/src/examples/purity-todo/services/task-details.ts +++ b/src/examples/purity-todo/services/task-details.ts @@ -5,10 +5,13 @@ import type {EventHandler} from "../../../purity.js" export const closeTaskDetails: EventHandler = () => { const task = selectDetailedTask() - patchTask({ - ...task, - subtasks: task.subtasks?.filter(({description}) => description), - }) + patchTask( + { + ...task, + subtasks: task.subtasks?.filter(({description}) => description), + }, + true + ) setState(() => ({taskDetailId: undefined})) } diff --git a/src/examples/purity-todo/services/tasks.ts b/src/examples/purity-todo/services/tasks.ts index 6434931..e22e402 100644 --- a/src/examples/purity-todo/services/tasks.ts +++ b/src/examples/purity-todo/services/tasks.ts @@ -25,12 +25,20 @@ export const prepareTask = (description: string): Task => { return task } -export const patchTask = (patch: Partial & Pick): void => { +export const patchTask = ( + patch: Partial & Pick, + silentUpdate = false +): void => { const now = Date.now() setState(({tasks}) => ({ tasks: tasks.map(({isImageLoading, ...task}) => task.id === patch.id - ? {...task, ...patch, updatedAt: now, tmpFlag: true} + ? { + ...task, + ...patch, + updatedAt: silentUpdate ? task.updatedAt : now, + tmpFlag: true, + } : task ), })) diff --git a/src/purity.ts b/src/purity.ts index c8c2500..2e6b272 100644 --- a/src/purity.ts +++ b/src/purity.ts @@ -122,7 +122,7 @@ export const init = >( ) } const newNodesMap = rootComponent() - console.warn("🌀") + console.warn("Updating DOM...") for (const [id, domNode] of domNodesMap) { const newNode = newNodesMap.get(id) // Since we depend on the shallow comparison, we must only care about updating changed nodes. @@ -132,9 +132,9 @@ export const init = >( updateAttributes(elementById, newNode) if (domNode.shallow.innerHTML !== newNode.shallow.innerHTML) { elementById.innerHTML = newNode.node.innerHTML - console.warn(`\t↻ #${id}`) + console.warn(`\t🔴 #${id} re-rendered`) } else { - console.warn(`\t± #${id}`) + console.warn(`\t🟡 #${id} no content change`) } } else { throw new Error(`There is no element in the DOM with id "${id}".`) From 18564ae545d638e251a8b33e1cb18c3818e61961 Mon Sep 17 00:00:00 2001 From: Andrew Tatomyr Date: Tue, 9 Dec 2025 22:14:17 +0200 Subject: [PATCH 2/3] feat(todo): update the dark theme and overall styling; modify the completion from subtasks logic --- .../purity-todo/components/app-style.ts | 52 ++++++-- src/examples/purity-todo/components/header.ts | 18 +-- .../purity-todo/components/input-form.ts | 10 +- .../purity-todo/components/modal-style.ts | 5 +- .../purity-todo/components/subtask-item.ts | 8 +- .../components/task-details-style.ts | 7 +- .../purity-todo/components/task-details.ts | 9 +- .../purity-todo/components/task-item.test.ts | 3 + .../purity-todo/components/task-list-style.ts | 13 +- src/examples/purity-todo/manifest.json | 2 +- src/examples/purity-todo/purity-todo.sw.ts | 2 +- .../purity-todo/services/task-details.test.ts | 119 ++++++++++++++++++ .../purity-todo/services/task-details.ts | 10 +- 13 files changed, 218 insertions(+), 40 deletions(-) create mode 100644 src/examples/purity-todo/services/task-details.test.ts diff --git a/src/examples/purity-todo/components/app-style.ts b/src/examples/purity-todo/components/app-style.ts index cb06b5e..adc92ba 100644 --- a/src/examples/purity-todo/components/app-style.ts +++ b/src/examples/purity-todo/components/app-style.ts @@ -8,13 +8,45 @@ export const appStyle = (): string => render` --background-color: #f0f0f0; --text-color: #555; --shadow-color: #555; + --border-color: lightgrey; + --accent-color: #4a90e2; + --completed-color: lightgrey; + --subtask-color: lightgrey; + --button-active-bg: grey; + --header-bg: lightgrey; + --header-border: none; + --input-bg: #303030; + --input-color: #eee; + --input-border: none; + --input-focus-outline: none; + --modal-overlay-bg: #50505030; + --task-item-hover-bg: transparent; + --completed-image-opacity: 1; + --control-button-bg: #555; + --control-button-opacity: 0.75; } @media (prefers-color-scheme: dark) { :root { - --background-color: #484848; - --text-color: #f0f0f0; - --shadow-color: #000; + --background-color: #1a1a2e; + --text-color: #e0e0e0; + --shadow-color: rgba(0, 0, 0, 0.5); + --border-color: #2d2d44; + --accent-color: #4a90e2; + --completed-color: #6b7280; + --subtask-color: #9ca3af; + --button-active-bg: var(--accent-color); + --header-bg: #2d2d44; + --header-border: 1px solid var(--border-color); + --input-bg: var(--header-bg); + --input-color: var(--text-color); + --input-border: 1px solid var(--border-color); + --input-focus-outline: 2px solid var(--accent-color); + --modal-overlay-bg: rgba(0, 0, 0, 0.7); + --task-item-hover-bg: rgba(74, 144, 226, 0.1); + --completed-image-opacity: 0.5; + --control-button-bg: var(--accent-color); + --control-button-opacity: 0.9; } } @@ -63,16 +95,22 @@ export const appStyle = (): string => render` } .${ACTION_BUTTON}:active { - background-color: grey; - color: white; + background-color: var(--button-active-bg); } .${ACTION_BUTTON}.hidden { display: none; } - .${ACTION_BUTTON}.chosen { - color: var(--background-color); + .header { + .${ACTION_BUTTON} { + color: var(--background-color); + } + + .${ACTION_BUTTON}:active, + .${ACTION_BUTTON}.chosen { + color: var(--text-color); + } } diff --git a/src/examples/purity-todo/components/header.ts b/src/examples/purity-todo/components/header.ts index 9760d52..c540151 100644 --- a/src/examples/purity-todo/components/header.ts +++ b/src/examples/purity-todo/components/header.ts @@ -5,25 +5,25 @@ import {ACTION_BUTTON} from "./app-style.js" const headerStyle = (): string => render` ` export const header = (): string => render` -
-
@@ -141,7 +140,7 @@ export const taskDetails = (): string => { ${task.subtasks?.map(subtaskItem)}
-