Refactor task components, add mobile-friendly enhancements, and improve settings management
- Introduced `TodoItemTouch`, a responsive task item optimized for mobile interaction with swipe actions. - Added `MobileActions` for streamlined task creation and settings access on smaller screens. - Refactored `App.vue` to adapt layouts dynamically for mobile and desktop users. - Implemented collapsible, categorized task lists with improved handling in `TodoList` and `TodoListTouch`. - Improved task swipe actions with `useSwipe` for smoother UI interactions. - Updated styling with DaisyUI theme customization and multi-theme support. - Enhanced `useSettings` with a `todayShown` toggle for quick agenda visibility.
This commit is contained in:
14
src/App.vue
14
src/App.vue
@@ -3,8 +3,8 @@ import { useMediaQuery } from '@vueuse/core'
|
|||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CreateInput from './components/CreateInput.vue'
|
import CreateInput from './components/CreateInput.vue'
|
||||||
import CreateModal from './components/CreateModal.vue'
|
|
||||||
|
|
||||||
|
import MobileActions from './components/MobileActions.vue'
|
||||||
import SettingsModal from './components/SettingsModal.vue'
|
import SettingsModal from './components/SettingsModal.vue'
|
||||||
import { useTasks } from './composables/useTasks.ts'
|
import { useTasks } from './composables/useTasks.ts'
|
||||||
|
|
||||||
@@ -18,13 +18,17 @@ onMounted(fetchTasks)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<main class="h-screen overflow-y-auto">
|
<main class="h-screen overflow-y-auto overflow-x-none w-screen max-w-screen border-t-2 border-primary">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
<template v-if="currentPath === '/'">
|
<template v-if="currentPath === '/'">
|
||||||
<CreateModal v-if="isMobile" />
|
<template v-if="isMobile">
|
||||||
<CreateInput v-else />
|
<MobileActions />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<CreateInput />
|
||||||
|
<SettingsModal />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<SettingsModal />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { Task } from '../types.ts'
|
|
||||||
import { PhPlus } from '@phosphor-icons/vue'
|
|
||||||
import { useTemplateRef } from 'vue'
|
|
||||||
import { useTasks } from '../composables/useTasks.ts'
|
|
||||||
import { router } from '../router.ts'
|
|
||||||
|
|
||||||
const { createTask } = useTasks()
|
|
||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
|
||||||
const data = new FormData(e.target as HTMLFormElement)
|
|
||||||
const task: Partial<Task> = Object.fromEntries(data)
|
|
||||||
|
|
||||||
await createTask(task)
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
const createModal = useTemplateRef('createModal')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="fab">
|
|
||||||
<button class="btn btn-xl btn-circle btn-secondary" @click="createModal?.showModal()">
|
|
||||||
<PhPlus size="24" weight="bold" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<dialog ref="createModal" class="modal">
|
|
||||||
<div class="modal-box">
|
|
||||||
<form method="dialog">
|
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form @submit.prevent="handleSubmit">
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">
|
|
||||||
What is your name?
|
|
||||||
</legend>
|
|
||||||
<input type="text" class="input" name="title" placeholder="Type here">
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">
|
|
||||||
Category
|
|
||||||
</legend>
|
|
||||||
<select class="select" name="tag">
|
|
||||||
<option disabled selected value="@uncategorized">
|
|
||||||
@uncategorized
|
|
||||||
</option>
|
|
||||||
<option value="@home">
|
|
||||||
@home
|
|
||||||
</option>
|
|
||||||
<option value="@work">
|
|
||||||
Amber
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</fieldset>
|
|
||||||
<button class="btn btn-primary">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
69
src/components/MobileActions.vue
Normal file
69
src/components/MobileActions.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhDotsNine, PhPlus, PhSliders, PhX } from '@phosphor-icons/vue'
|
||||||
|
import { ref, useTemplateRef } from 'vue'
|
||||||
|
import CreateForm from './forms/CreateForm.vue'
|
||||||
|
import EditForm from './forms/EditForm.vue'
|
||||||
|
import SettingsForm from './forms/SettingsForm.vue'
|
||||||
|
|
||||||
|
const modalComponent = useTemplateRef('modalComponent')
|
||||||
|
|
||||||
|
type ModalShown = 'create' | 'edit' | 'settings'
|
||||||
|
|
||||||
|
const modalShown = ref<ModalShown>()
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
create: CreateForm,
|
||||||
|
edit: EditForm,
|
||||||
|
settings: SettingsForm,
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal(component: ModalShown) {
|
||||||
|
modalShown.value = component
|
||||||
|
modalComponent.value?.showModal()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="fab select-none">
|
||||||
|
<!-- a focusable div with tabindex is necessary to work on all browsers. role="button" is necessary for accessibility -->
|
||||||
|
<div tabindex="0" role="button" class="btn btn-xl btn-circle bg-white border border-black border-2">
|
||||||
|
<PhDotsNine :size="30" weight="bold" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- close button should not be focusable so it can close the FAB when clicked. It's just a visual placeholder -->
|
||||||
|
<div class="fab-close">
|
||||||
|
<span class="btn btn-xl btn-circle bg-white border border-black border-2"><PhX size="30" weight="bold" /></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- buttons that show up when FAB is open -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-xl btn-circle bg-white border border-black border-2" @click="showModal('create')">
|
||||||
|
<PhPlus size="30" weight="bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-xl btn-circle bg-white border border-black border-2" @click="showModal('settings')">
|
||||||
|
<PhSliders :size="30" weight="bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dialog ref="modalComponent" class="modal">
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
<div class="modal-box">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
|
<PhX size="24" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<component :is="componentMap[modalShown]" v-if="modalShown" />
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,9 +3,7 @@ import { PhX } from '@phosphor-icons/vue'
|
|||||||
import { onKeyStroke, useMagicKeys, whenever } from '@vueuse/core'
|
import { onKeyStroke, useMagicKeys, whenever } from '@vueuse/core'
|
||||||
|
|
||||||
import { useTemplateRef } from 'vue'
|
import { useTemplateRef } from 'vue'
|
||||||
import { useSettings } from '../composables/useSettings.ts'
|
import SettingsForm from './forms/SettingsForm.vue'
|
||||||
|
|
||||||
const { settings } = useSettings()
|
|
||||||
|
|
||||||
const settingsModal = useTemplateRef('settingsModal')
|
const settingsModal = useTemplateRef('settingsModal')
|
||||||
|
|
||||||
@@ -26,26 +24,16 @@ onKeyStroke('Escape', (e) => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<dialog ref="settingsModal" class="modal">
|
<dialog ref="settingsModal" class="modal">
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
<PhX size="24" />
|
<PhX size="24" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form @submit.prevent>
|
<SettingsForm />
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">
|
|
||||||
Username
|
|
||||||
</legend>
|
|
||||||
<input v-model="settings.username" type="text" class="input" name="username">
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">
|
|
||||||
Password
|
|
||||||
</legend>
|
|
||||||
<input v-model="settings.password" type="text" class="input" name="password">
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { UseSwipeDirection } from '@vueuse/core'
|
||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue'
|
import { PhCheckSquare, PhClockCountdown, PhFlag, PhPause, PhPlay, PhSquare, PhTrash } from '@phosphor-icons/vue'
|
||||||
|
import { onClickOutside, useSwipe } from '@vueuse/core'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, shallowRef, useTemplateRef } from 'vue'
|
||||||
import { useTasks } from '../composables/useTasks.ts'
|
import useActions from '../composables/useActions.ts'
|
||||||
import { TaskStatus } from '../types.ts'
|
import { TaskStatus } from '../types.ts'
|
||||||
|
|
||||||
const { task } = defineProps<{ task: Task }>()
|
const { task } = defineProps<{ task: Task }>()
|
||||||
|
|
||||||
const { updateTask } = useTasks()
|
const { run } = useActions()
|
||||||
|
|
||||||
const dueColor = computed(() => {
|
const dueColor = computed(() => {
|
||||||
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
|
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
|
||||||
@@ -28,57 +30,90 @@ const dueColor = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusSelectVisible = ref(false)
|
const target = useTemplateRef('target')
|
||||||
|
const container = useTemplateRef('container')
|
||||||
|
const containerWidth = computed(() => container.value?.offsetWidth)
|
||||||
|
const left = shallowRef('0')
|
||||||
|
const opacity = shallowRef(1)
|
||||||
|
|
||||||
async function handleClick(update: Partial<Task>) {
|
function reset() {
|
||||||
updateTask({ ...task, ...update })
|
left.value = '0'
|
||||||
statusSelectVisible.value = false
|
opacity.value = 1
|
||||||
}
|
}
|
||||||
|
const { isSwiping, lengthX } = useSwipe(
|
||||||
|
target,
|
||||||
|
{
|
||||||
|
passive: false,
|
||||||
|
onSwipe(_e: TouchEvent) {
|
||||||
|
if (containerWidth.value) {
|
||||||
|
if (lengthX.value < 0) {
|
||||||
|
const length = Math.abs(lengthX.value)
|
||||||
|
left.value = `${length}px`
|
||||||
|
opacity.value = 1.1 - length / containerWidth.value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
left.value = '0'
|
||||||
|
opacity.value = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSwipeEnd(_e: TouchEvent, _direction: UseSwipeDirection) {
|
||||||
|
if (lengthX.value < 0 && containerWidth.value && (Math.abs(lengthX.value) / containerWidth.value) >= 0.5) {
|
||||||
|
left.value = '100%'
|
||||||
|
opacity.value = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
left.value = '0'
|
||||||
|
opacity.value = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async function handleClick(command: string) {
|
||||||
|
await run(command)
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickOutside(container, reset)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="list-row">
|
<li ref="container" class="badge badge-xl badge-neutral badge-outline w-full h-auto py-2 select-none relative overflow-hidden">
|
||||||
<div class="flex items-center justify-center">
|
<div class=" flex flex-row justify-start gap-2 w-full">
|
||||||
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
|
<template v-if="task.status !== TaskStatus.WIP">
|
||||||
<PhX v-if="statusSelectVisible" :size="20" />
|
<button class="btn btn-square btn-ghost" @click="handleClick(`check ${task.id_}`)">
|
||||||
<template v-else>
|
<PhCheckSquare v-if="task.status !== TaskStatus.DONE" :size="30" class="text-success" /><PhSquare v-else :size="30" />
|
||||||
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
|
</button>
|
||||||
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
|
<button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick(`flag ${task.id_}`)">
|
||||||
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
|
<PhFlag :size="30" weight="fill" class="text-warning" />
|
||||||
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="20" weight="fill" class="text-info" />
|
</button>
|
||||||
</template>
|
<button class="btn btn-square btn-ghost" @click="handleClick(`start ${task.id_}`)">
|
||||||
|
<PhPlay :size="30" weight="fill" class="text-info" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button v-else class="btn btn-square btn-ghost" @click="handleClick(`stop ${task.id_}`)">
|
||||||
|
<PhPause :size="30" weight="fill" class="text-info" />
|
||||||
</button>
|
</button>
|
||||||
<Transition>
|
<div class="grow flex justify-end">
|
||||||
<div v-if="statusSelectVisible" class="">
|
<button class="btn btn-square btn-ghost" @click="handleClick(`delete ${task.id_}`)">
|
||||||
<template v-if="task.status !== TaskStatus.WIP">
|
<PhTrash :size="30" weight="fill" class="text-error" />
|
||||||
<button v-if="task.status !== TaskStatus.DONE" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.DONE })">
|
</button>
|
||||||
<PhCheckSquare :size="24" weight="regular" class="text-success" />
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
|
<div ref="target" :class="{ 'transition-all': !isSwiping }" :style="{ left, opacity }" class="top-0 left-0 w-full h-full absolute rounded bg-white font-mono text-md font-semibold flex flex-row justify-start items-center gap-2 min-h-10 px-2">
|
||||||
<PhSquare :size="24" weight="regular" />
|
<div class="flex items-center justify-center">
|
||||||
</button>
|
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="30" />
|
||||||
<button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.FLAG })">
|
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="30" weight="fill" class="text-success" />
|
||||||
<PhFlag :size="24" weight="fill" class="text-warning" />
|
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="30" weight="fill" class="text-warning" />
|
||||||
</button>
|
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="30" weight="fill" class="text-info" />
|
||||||
<button class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WIP })">
|
</div>
|
||||||
<PhPlay :size="24" weight="fill" class="text-info" />
|
<div class="grow">
|
||||||
</button>
|
{{ task.title }}
|
||||||
</template>
|
</div>
|
||||||
<button v-else class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
|
<div v-if="task.dueDate" :class="dueColor" class="text-right">
|
||||||
<PhPause :size="24" weight="fill" class="text-info" />
|
<PhClockCountdown :size="30" :weight="dueColor === 'text-error' ? 'fill' : 'regular'" />
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center">
|
|
||||||
<div>{{ task.id }} {{ task.id_ }} {{ task.title }}</div>
|
|
||||||
<div v-if="task.dueDate" :class="dueColor">
|
|
||||||
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-square btn-ghost">
|
|
||||||
<PhDotsThree :size="24" weight="regular" />
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
45
src/components/TodoList.vue
Normal file
45
src/components/TodoList.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Task } from '../types.ts'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSettings } from '../composables/useSettings.ts'
|
||||||
|
import TodoItem from './TodoItem.vue'
|
||||||
|
|
||||||
|
const { categorizedTasks, today } = defineProps<{ categorizedTasks: Record<string, Task[]>, today: { today: Task[], next7days: Task[] } }>()
|
||||||
|
const { settings } = useSettings()
|
||||||
|
const collapsed = ref<string[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div v-for="(catTasks, category) in categorizedTasks" :key="category" class="m-4 ">
|
||||||
|
<div class="mb-4">
|
||||||
|
<button class="badge badge-lg badge-neutral badge-outline font-mono font-bold" @click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)">
|
||||||
|
{{ category }} [{{ catTasks.length }}]
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Transition name="fade">
|
||||||
|
<ul v-if="!collapsed.includes(category)" class="space-y-2">
|
||||||
|
<TodoItem v-for="task in catTasks" :key="task.id" :task />
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="{ 'translate-x-full': !settings.todayShown }" class=" transition-transform duration-500 border-primary border-l-2 border-t-2 fixed bottom-0 right-0 top-0 w-1/2 max-w-sm text-center text-sm bg-neutral-50">
|
||||||
|
<div class="">
|
||||||
|
today
|
||||||
|
</div><div v-for="task in today.today" :key="task.id">
|
||||||
|
{{ task.title }}
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
next 7 days
|
||||||
|
</div><div v-for="task in today.next7days" :key="task.id">
|
||||||
|
{{ task.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
45
src/components/TodoListTouch.vue
Normal file
45
src/components/TodoListTouch.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Task } from '../types.ts'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSettings } from '../composables/useSettings.ts'
|
||||||
|
import TodoItemTouch from './TodoItemTouch.vue'
|
||||||
|
|
||||||
|
const { categorizedTasks, today } = defineProps<{ categorizedTasks: Record<string, Task[]>, today: { today: Task[], next7days: Task[] } }>()
|
||||||
|
const { settings } = useSettings()
|
||||||
|
const collapsed = ref<string[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div v-for="(catTasks, category) in categorizedTasks" :key="category" class="m-4 ">
|
||||||
|
<div class="mb-4">
|
||||||
|
<button class="badge badge-xl badge-neutral badge-outline font-mono font-extrabold" @click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)">
|
||||||
|
{{ category }} [{{ catTasks.length }}]
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Transition name="fade">
|
||||||
|
<ul v-if="!collapsed.includes(category)" class="space-y-2">
|
||||||
|
<TodoItemTouch v-for="task in catTasks" :key="task.id" :task />
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="{ 'translate-x-full': !settings.todayShown }" class=" transition-transform duration-500 border-l border-t fixed bottom-0 right-0 top-0 w-1/2 max-w-sm text-center text-sm bg-neutral-50">
|
||||||
|
<div class="">
|
||||||
|
today
|
||||||
|
</div><div v-for="task in today.today" :key="task.id">
|
||||||
|
{{ task.title }}
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
next 7 days
|
||||||
|
</div><div v-for="task in today.next7days" :key="task.id">
|
||||||
|
{{ task.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
47
src/components/forms/CreateForm.vue
Normal file
47
src/components/forms/CreateForm.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Task } from '../../types.ts'
|
||||||
|
import { useTasks } from '../../composables/useTasks.ts'
|
||||||
|
|
||||||
|
const { createTask } = useTasks()
|
||||||
|
|
||||||
|
async function handleSubmit(e: Event) {
|
||||||
|
const data = new FormData(e.target as HTMLFormElement)
|
||||||
|
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'dueDate'>
|
||||||
|
|
||||||
|
await createTask(task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
What is your name?
|
||||||
|
</legend>
|
||||||
|
<input type="text" class="input" name="title" placeholder="Type here">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
Category
|
||||||
|
</legend>
|
||||||
|
<select class="select" name="tag">
|
||||||
|
<option disabled selected value="@uncategorized">
|
||||||
|
@uncategorized
|
||||||
|
</option>
|
||||||
|
<option value="@home">
|
||||||
|
@home
|
||||||
|
</option>
|
||||||
|
<option value="@work">
|
||||||
|
Amber
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
47
src/components/forms/EditForm.vue
Normal file
47
src/components/forms/EditForm.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Task } from '../../types.ts'
|
||||||
|
import { useTasks } from '../../composables/useTasks.ts'
|
||||||
|
|
||||||
|
const { createTask } = useTasks()
|
||||||
|
|
||||||
|
async function handleSubmit(e: Event) {
|
||||||
|
const data = new FormData(e.target as HTMLFormElement)
|
||||||
|
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'dueDate'>
|
||||||
|
|
||||||
|
await createTask(task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
What is your name?
|
||||||
|
</legend>
|
||||||
|
<input type="text" class="input" name="title" placeholder="Type here">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
Category
|
||||||
|
</legend>
|
||||||
|
<select class="select" name="tag">
|
||||||
|
<option disabled selected value="@uncategorized">
|
||||||
|
@uncategorized
|
||||||
|
</option>
|
||||||
|
<option value="@home">
|
||||||
|
@home
|
||||||
|
</option>
|
||||||
|
<option value="@work">
|
||||||
|
Amber
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
22
src/components/forms/SettingsForm.vue
Normal file
22
src/components/forms/SettingsForm.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useSettings } from '../../composables/useSettings.ts'
|
||||||
|
|
||||||
|
const { settings } = useSettings()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
Username
|
||||||
|
</legend>
|
||||||
|
<input v-model="settings.username" type="text" class="input" name="username">
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">
|
||||||
|
Password
|
||||||
|
</legend>
|
||||||
|
<input v-model="settings.password" type="password" class="input" name="password">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
tagRenameCommand,
|
tagRenameCommand,
|
||||||
} from '../utils/actions.ts'
|
} from '../utils/actions.ts'
|
||||||
import { parseCommand } from '../utils/parser.ts'
|
import { parseCommand } from '../utils/parser.ts'
|
||||||
|
import { useSettings } from './useSettings.ts'
|
||||||
import { useTasks } from './useTasks.ts'
|
import { useTasks } from './useTasks.ts'
|
||||||
|
|
||||||
export default function useActions() {
|
export default function useActions() {
|
||||||
const { tasks: tasksOriginal, createTask, updateTask } = useTasks()
|
const { tasks: tasksOriginal, createTask, updateTask } = useTasks()
|
||||||
const run = (value: string) => {
|
const { settings } = useSettings()
|
||||||
|
const run = async (value: string) => {
|
||||||
const cmd = parseCommand(value)
|
const cmd = parseCommand(value)
|
||||||
let tasksToUpdate: Task[] = []
|
let tasksToUpdate: Task[] = []
|
||||||
let taskToCreate: Pick<Task, 'tag' | 'title' | 'dueDate'> | null = null
|
let taskToCreate: Pick<Task, 'tag' | 'title' | 'dueDate'> | null = null
|
||||||
@@ -81,10 +83,9 @@ export default function useActions() {
|
|||||||
case 'tagrename':
|
case 'tagrename':
|
||||||
tasksToUpdate = tagRenameCommand(cmd, tasks)
|
tasksToUpdate = tagRenameCommand(cmd, tasks)
|
||||||
break
|
break
|
||||||
/* Visibility */
|
case 'today':
|
||||||
// case 'hide':
|
settings.value.todayShown = !settings.value.todayShown
|
||||||
// updateCandidate = hideCommand(updateCandidate, cmd)
|
break
|
||||||
// break
|
|
||||||
// case 'show':
|
// case 'show':
|
||||||
// updateCandidate = showCommand(updateCandidate, cmd)
|
// updateCandidate = showCommand(updateCandidate, cmd)
|
||||||
// break
|
// break
|
||||||
@@ -97,12 +98,11 @@ export default function useActions() {
|
|||||||
// break
|
// break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(tasksToUpdate, taskToCreate)
|
|
||||||
if (tasksToUpdate) {
|
if (tasksToUpdate) {
|
||||||
updateTask(tasksToUpdate)
|
await updateTask(tasksToUpdate)
|
||||||
}
|
}
|
||||||
if (taskToCreate) {
|
if (taskToCreate) {
|
||||||
createTask(taskToCreate)
|
await createTask(taskToCreate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { run }
|
return { run }
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useStore } from './useStore.ts'
|
import { useStore } from './useStore.ts'
|
||||||
|
|
||||||
|
const store = ref<string[]>([])
|
||||||
|
const historyIndex = ref(0)
|
||||||
|
|
||||||
export default function useHistory() {
|
export default function useHistory() {
|
||||||
const { getValue, setValue } = useStore()
|
const { getValue, setValue } = useStore()
|
||||||
|
|
||||||
const store = ref<string[]>([])
|
|
||||||
const history = computed<string[]>(() => store.value)
|
const history = computed<string[]>(() => store.value)
|
||||||
|
|
||||||
const historyIndex = ref(0)
|
|
||||||
|
|
||||||
const resetHistoryIndex = () => {
|
const resetHistoryIndex = () => {
|
||||||
historyIndex.value = store.value.length - 1
|
historyIndex.value = store.value.length - 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,67 @@
|
|||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue'
|
||||||
import { useStore } from './useStore.ts';
|
import { useCrypto } from './useCrypto.ts'
|
||||||
import { useCrypto } from './useCrypto.ts';
|
import { useStore } from './useStore.ts'
|
||||||
|
|
||||||
export type Settings = {
|
export interface Settings {
|
||||||
username: string,
|
username: string
|
||||||
password: string,
|
password: string
|
||||||
|
todayShown: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsDefault: Settings = {
|
const settingsDefault: Settings = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
};
|
todayShown: false,
|
||||||
|
}
|
||||||
|
const settings = ref<Settings>({ ...settingsDefault })
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const { setValue, getValue } = useStore();
|
const { setValue, getValue } = useStore()
|
||||||
const { encrypt, decrypt } = useCrypto();
|
const { encrypt, decrypt } = useCrypto()
|
||||||
const settings = ref<Settings>({ ...settingsDefault });
|
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
const readSettings = await getValue<Settings>('settings');
|
const readSettings = await getValue<Settings>('settings')
|
||||||
if (!readSettings) {
|
if (!readSettings) {
|
||||||
settings.value = { ...settingsDefault };
|
settings.value = { ...settingsDefault }
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let password = readSettings.password ?? '';
|
let password = readSettings.password ?? ''
|
||||||
if (password) {
|
if (password) {
|
||||||
try {
|
try {
|
||||||
password = decrypt(password) as string;
|
password = decrypt(password) as string
|
||||||
} catch (error) {
|
}
|
||||||
console.warn('Failed to decrypt stored password:', error);
|
catch (error) {
|
||||||
|
console.warn('Failed to decrypt stored password:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.value = {
|
settings.value = {
|
||||||
username: readSettings.username ?? '',
|
...settingsDefault,
|
||||||
|
...readSettings,
|
||||||
password,
|
password,
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
const encryptedPassword = settings.value.password
|
const encryptedPassword = settings.value.password
|
||||||
? encrypt(settings.value.password) as string
|
? encrypt(settings.value.password) as string
|
||||||
: '';
|
: ''
|
||||||
|
|
||||||
await setValue<Settings>('settings', {
|
await setValue<Settings>('settings', {
|
||||||
username: settings.value.username,
|
...settings.value,
|
||||||
password: encryptedPassword,
|
password: encryptedPassword,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(settings, () => {
|
watch(settings, () => {
|
||||||
void saveSettings();
|
void saveSettings()
|
||||||
}, { deep: true });
|
}, { deep: true })
|
||||||
|
|
||||||
void loadSettings();
|
void loadSettings()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const error = ref<string | null>(null)
|
|||||||
|
|
||||||
export function useTasks() {
|
export function useTasks() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const fetchTasks = async (force = false) => {
|
const fetchTasks = async (force = false) => {
|
||||||
if (tasks.value.length > 0 && !force)
|
if (tasks.value.length > 0 && !force)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from 'vue'
|
||||||
import App from "./App.vue";
|
import App from './App.vue'
|
||||||
import { router } from './router.ts';
|
import { router } from './router.ts'
|
||||||
|
|
||||||
createApp(App).use(router).mount("#app");
|
createApp(App).use(router).mount('#app')
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import ListScreen from './screens/ListScreen.vue'
|
import ListScreen from './screens/ListScreen.vue'
|
||||||
import SettingsScreen from './screens/SettingsScreen.vue'
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: ListScreen },
|
{ path: '/', component: ListScreen },
|
||||||
{ path: '/settings', component: SettingsScreen },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
import TodoItem from '../components/TodoItem.vue'
|
import { DateTime } from 'luxon'
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import TodoList from '../components/TodoList.vue'
|
||||||
|
import TodoListTouch from '../components/TodoListTouch.vue'
|
||||||
import { useTasks } from '../composables/useTasks.ts'
|
import { useTasks } from '../composables/useTasks.ts'
|
||||||
|
|
||||||
const { tasks, fetchTasks } = useTasks()
|
const { tasks, fetchTasks } = useTasks()
|
||||||
@@ -10,6 +13,7 @@ const { tasks, fetchTasks } = useTasks()
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchTasks()
|
await fetchTasks()
|
||||||
})
|
})
|
||||||
|
const isMobile = useMediaQuery('(pointer: coarse)')
|
||||||
|
|
||||||
const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived).sort((a, b) => a.id_ - b.id_))
|
const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived).sort((a, b) => a.id_ - b.id_))
|
||||||
|
|
||||||
@@ -29,26 +33,20 @@ const categorizedTasks = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const collapsed = ref<string[]>([])
|
const today = computed(() => {
|
||||||
|
const now = DateTime.now().startOf('day')
|
||||||
|
const tasksDue = visibleTasks.value.filter(task => task.dueDate)
|
||||||
|
const today = tasksDue.filter(task => now.diff(DateTime.fromMillis(task.dueDate ?? 0), 'days').toObject().days === 0)
|
||||||
|
const next7days = tasksDue.filter((task) => {
|
||||||
|
const diff = now.diff(DateTime.fromMillis(task.dueDate ?? 0), 'days').toObject()
|
||||||
|
return diff.days && diff.days < 0 && diff.days >= -7
|
||||||
|
})
|
||||||
|
return { today, next7days }
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<component :is="isMobile ? TodoListTouch : TodoList" :today="today" :categorized-tasks="categorizedTasks" />
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div v-for="(catTasks, category) in categorizedTasks" :key="category" class="m-4 ">
|
|
||||||
<div class="mb-4">
|
|
||||||
<button class="px-4 py-1 rounded bg-neutral-200 font-mono text-sm font-bold" @click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)">
|
|
||||||
{{ category }} [{{ catTasks.length }}]
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Transition name="fade">
|
|
||||||
<ul v-if="!collapsed.includes(category)" class="">
|
|
||||||
<TodoItem v-for="task in catTasks" :key="task.id" :task />
|
|
||||||
</ul>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -2,41 +2,80 @@
|
|||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
name: "emerald";
|
name: "lofi";
|
||||||
default: true;
|
default: true;
|
||||||
prefersdark: true;
|
prefersdark: false;
|
||||||
color-scheme: "light";
|
color-scheme: "light";
|
||||||
--color-base-100: oklch(100% 0 0);
|
--color-base-100: oklch(100% 0 0);
|
||||||
--color-base-200: oklch(93% 0 0);
|
--color-base-200: oklch(97% 0 0);
|
||||||
--color-base-300: oklch(86% 0 0);
|
--color-base-300: oklch(94% 0 0);
|
||||||
--color-base-content: oklch(35.519% 0.032 262.988);
|
--color-base-content: oklch(0% 0 0);
|
||||||
--color-primary: oklch(76.662% 0.135 153.45);
|
--color-primary: oklch(15.906% 0 0);
|
||||||
--color-primary-content: oklch(33.387% 0.04 162.24);
|
--color-primary-content: oklch(100% 0 0);
|
||||||
--color-secondary: oklch(61.302% 0.202 261.294);
|
--color-secondary: oklch(21.455% 0.001 17.278);
|
||||||
--color-secondary-content: oklch(100% 0 0);
|
--color-secondary-content: oklch(100% 0 0);
|
||||||
--color-accent: oklch(72.772% 0.149 33.2);
|
--color-accent: oklch(26.861% 0 0);
|
||||||
--color-accent-content: oklch(0% 0 0);
|
--color-accent-content: oklch(100% 0 0);
|
||||||
--color-neutral: oklch(35.519% 0.032 262.988);
|
--color-neutral: oklch(0% 0 0);
|
||||||
--color-neutral-content: oklch(98.462% 0.001 247.838);
|
--color-neutral-content: oklch(100% 0 0);
|
||||||
--color-info: oklch(72.06% 0.191 231.6);
|
--color-info: oklch(79.54% 0.103 205.9);
|
||||||
--color-info-content: oklch(0% 0 0);
|
--color-info-content: oklch(15.908% 0.02 205.9);
|
||||||
--color-success: oklch(64.8% 0.15 160);
|
--color-success: oklch(90.13% 0.153 164.14);
|
||||||
--color-success-content: oklch(0% 0 0);
|
--color-success-content: oklch(18.026% 0.03 164.14);
|
||||||
--color-warning: oklch(84.71% 0.199 83.87);
|
--color-warning: oklch(88.37% 0.135 79.94);
|
||||||
--color-warning-content: oklch(0% 0 0);
|
--color-warning-content: oklch(17.674% 0.027 79.94);
|
||||||
--color-error: oklch(71.76% 0.221 22.18);
|
--color-error: oklch(78.66% 0.15 28.47);
|
||||||
--color-error-content: oklch(0% 0 0);
|
--color-error-content: oklch(15.732% 0.03 28.47);
|
||||||
--radius-selector: 1rem;
|
--radius-selector: 0rem;
|
||||||
--radius-field: 0.5rem;
|
--radius-field: 0rem;
|
||||||
--radius-box: 1rem;
|
--radius-box: 0rem;
|
||||||
--size-selector: 0.25rem;
|
--size-selector: 0.25rem;
|
||||||
--size-field: 0.25rem;
|
--size-field: 0.25rem;
|
||||||
--border: 1px;
|
--border: 2px;
|
||||||
--depth: 0;
|
--depth: 1;
|
||||||
--noise: 0;
|
--noise: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "black";
|
||||||
|
default: false;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(0% 0 0);
|
||||||
|
--color-base-200: oklch(19% 0 0);
|
||||||
|
--color-base-300: oklch(22% 0 0);
|
||||||
|
--color-base-content: oklch(87.609% 0 0);
|
||||||
|
--color-primary: oklch(35% 0 0);
|
||||||
|
--color-primary-content: oklch(100% 0 0);
|
||||||
|
--color-secondary: oklch(35% 0 0);
|
||||||
|
--color-secondary-content: oklch(100% 0 0);
|
||||||
|
--color-accent: oklch(35% 0 0);
|
||||||
|
--color-accent-content: oklch(100% 0 0);
|
||||||
|
--color-neutral: oklch(35% 0 0);
|
||||||
|
--color-neutral-content: oklch(100% 0 0);
|
||||||
|
--color-info: oklch(45.201% 0.313 264.052);
|
||||||
|
--color-info-content: oklch(89.04% 0.062 264.052);
|
||||||
|
--color-success: oklch(51.975% 0.176 142.495);
|
||||||
|
--color-success-content: oklch(90.395% 0.035 142.495);
|
||||||
|
--color-warning: oklch(96.798% 0.211 109.769);
|
||||||
|
--color-warning-content: oklch(19.359% 0.042 109.769);
|
||||||
|
--color-error: oklch(62.795% 0.257 29.233);
|
||||||
|
--color-error-content: oklch(12.559% 0.051 29.233);
|
||||||
|
--radius-selector: 0rem;
|
||||||
|
--radius-field: 0rem;
|
||||||
|
--radius-box: 0rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 2px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export function flagCommand(tasks: Task[], ids: Task['id_'][]) {
|
|||||||
t.status
|
t.status
|
||||||
= t.status === TaskStatus.FLAG ? TaskStatus.WAIT : TaskStatus.FLAG
|
= t.status === TaskStatus.FLAG ? TaskStatus.WAIT : TaskStatus.FLAG
|
||||||
t = stopWorkLogging(t)
|
t = stopWorkLogging(t)
|
||||||
|
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user