diff --git a/opal-web/src/app.css b/opal-web/src/app.css
index 1eefb54..bfc2afa 100644
--- a/opal-web/src/app.css
+++ b/opal-web/src/app.css
@@ -68,6 +68,8 @@
--color-overdue-text: #f85149;
--color-tag-bg: rgba(139, 148, 158, 0.1);
--color-tag-text: #8b949e;
+ --color-active-bg: rgba(57, 208, 186, 0.15);
+ --color-active-text: #39d0ba;
color-scheme: dark;
}
@@ -118,6 +120,8 @@
--color-overdue-text: #be123c;
--color-tag-bg: #f5f5f4;
--color-tag-text: #78716c;
+ --color-active-bg: rgba(99, 102, 241, 0.12);
+ --color-active-text: #4f46e5;
color-scheme: light;
}
@@ -168,6 +172,8 @@
--color-overdue-text: #ef4444;
--color-tag-bg: rgba(148, 163, 184, 0.1);
--color-tag-text: #94a3b8;
+ --color-active-bg: rgba(139, 92, 246, 0.15);
+ --color-active-text: #8b5cf6;
color-scheme: dark;
}
diff --git a/opal-web/src/lib/components/BottomSheet.svelte b/opal-web/src/lib/components/BottomSheet.svelte
new file mode 100644
index 0000000..7731871
--- /dev/null
+++ b/opal-web/src/lib/components/BottomSheet.svelte
@@ -0,0 +1,248 @@
+
+
+
+
+
+
diff --git a/opal-web/src/lib/components/ConfirmDialog.svelte b/opal-web/src/lib/components/ConfirmDialog.svelte
new file mode 100644
index 0000000..b498c53
--- /dev/null
+++ b/opal-web/src/lib/components/ConfirmDialog.svelte
@@ -0,0 +1,178 @@
+
+
+
+
+
+
diff --git a/opal-web/src/lib/components/Toast.svelte b/opal-web/src/lib/components/Toast.svelte
new file mode 100644
index 0000000..d19dd93
--- /dev/null
+++ b/opal-web/src/lib/components/Toast.svelte
@@ -0,0 +1,105 @@
+
+
+
+ {message}
+ {#if action}
+
+ {/if}
+
+
+
diff --git a/opal-web/src/lib/stores/tasks.js b/opal-web/src/lib/stores/tasks.js
index 894ae3c..e26684d 100644
--- a/opal-web/src/lib/stores/tasks.js
+++ b/opal-web/src/lib/stores/tasks.js
@@ -140,6 +140,55 @@ function createTasksStore() {
}
},
+ /**
+ * Start task timer (optimistic)
+ * @param {string} uuid
+ */
+ async startTask(uuid) {
+ const now = Math.floor(Date.now() / 1000);
+ update(tasks => tasks.map(t =>
+ t.uuid === uuid ? { ...t, start: now } : t
+ ));
+ try {
+ const updated = await tasksAPI.start(uuid);
+ update(tasks => tasks.map(t =>
+ t.uuid === uuid ? updated : t
+ ));
+ } catch (error) {
+ update(tasks => tasks.map(t =>
+ t.uuid === uuid ? { ...t, start: null } : t
+ ));
+ throw error;
+ }
+ },
+
+ /**
+ * Stop task timer (optimistic)
+ * @param {string} uuid
+ */
+ async stopTask(uuid) {
+ /** @type {number|null} */
+ let prevStart = null;
+ update(tasks => tasks.map(t => {
+ if (t.uuid === uuid) {
+ prevStart = t.start;
+ return { ...t, start: null };
+ }
+ return t;
+ }));
+ try {
+ const updated = await tasksAPI.stop(uuid);
+ update(tasks => tasks.map(t =>
+ t.uuid === uuid ? updated : t
+ ));
+ } catch (error) {
+ update(tasks => tasks.map(t =>
+ t.uuid === uuid ? { ...t, start: prevStart } : t
+ ));
+ throw error;
+ }
+ },
+
/**
* Complete task
* @param {string} uuid