Compare commits
2 Commits
72dee337b7
...
03e7185abe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e7185abe | ||
|
|
5de84f135f |
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onClickOutside } from "@vueuse/core"
|
||||||
import { computed, nextTick, ref, useTemplateRef, watch } from "vue"
|
import { computed, nextTick, ref, useTemplateRef, watch } from "vue"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -23,8 +24,8 @@ const segments = computed(() => segmentBody(props.task.body))
|
|||||||
|
|
||||||
const PRIORITIES = ["A", "B", "C", "D"] as const
|
const PRIORITIES = ["A", "B", "C", "D"] as const
|
||||||
|
|
||||||
const priorityBadgeClass = computed(() => {
|
const priorityBadgeClassFor = (priority?: string): string => {
|
||||||
switch (props.task.priority) {
|
switch (priority) {
|
||||||
case "A":
|
case "A":
|
||||||
return "badge badge-soft badge-error"
|
return "badge badge-soft badge-error"
|
||||||
case "B":
|
case "B":
|
||||||
@@ -34,17 +35,32 @@ const priorityBadgeClass = computed(() => {
|
|||||||
default:
|
default:
|
||||||
return "badge badge-soft badge-ghost"
|
return "badge badge-soft badge-ghost"
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const priorityBadgeClass = computed(() =>
|
||||||
|
priorityBadgeClassFor(props.task.priority)
|
||||||
|
)
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (!props.canEdit) return
|
if (!props.canEdit) return
|
||||||
emit("update", toggleCompleted(props.task))
|
emit("update", toggleCompleted(props.task))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const priorityOpen = ref(false)
|
||||||
|
const priorityDropdown = useTemplateRef<HTMLDivElement>("priorityDropdown")
|
||||||
|
onClickOutside(priorityDropdown, () => {
|
||||||
|
priorityOpen.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const togglePriorityMenu = () => {
|
||||||
|
if (!props.canEdit) return
|
||||||
|
priorityOpen.value = !priorityOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
const setPriority = (priority: string | undefined) => {
|
const setPriority = (priority: string | undefined) => {
|
||||||
if (!props.canEdit) return
|
if (!props.canEdit) return
|
||||||
emit("update", { ...props.task, priority })
|
emit("update", { ...props.task, priority })
|
||||||
closeDropdown()
|
priorityOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeToken = (kind: "project" | "context", value: string) => {
|
const removeToken = (kind: "project" | "context", value: string) => {
|
||||||
@@ -174,11 +190,6 @@ watch(
|
|||||||
if (editing.value) editing.value = false
|
if (editing.value) editing.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const dropdownRef = useTemplateRef<HTMLDetailsElement>("priorityDropdown")
|
|
||||||
const closeDropdown = () => {
|
|
||||||
if (dropdownRef.value) dropdownRef.value.open = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -194,26 +205,44 @@ const closeDropdown = () => {
|
|||||||
@change="toggle"
|
@change="toggle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<details ref="priorityDropdown" class="dropdown dropdown-bottom">
|
<div
|
||||||
<summary
|
ref="priorityDropdown"
|
||||||
|
class="todo-priority"
|
||||||
|
:class="{ 'todo-priority--open': priorityOpen }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
:class="priorityBadgeClass"
|
:class="priorityBadgeClass"
|
||||||
class="cursor-pointer list-none w-7 justify-center mt-0.5"
|
class="todo-priority-trigger cursor-pointer w-7 justify-center mt-0.5"
|
||||||
:tabindex="canEdit ? 0 : -1"
|
:disabled="!canEdit"
|
||||||
|
@click="togglePriorityMenu"
|
||||||
>
|
>
|
||||||
{{ task.priority ?? "—" }}
|
{{ task.priority ?? "—" }}
|
||||||
</summary>
|
</button>
|
||||||
<ul
|
<div
|
||||||
v-if="canEdit"
|
v-if="canEdit && priorityOpen"
|
||||||
class="dropdown-content menu bg-base-200 rounded-box z-10 shadow p-1 mt-1"
|
class="todo-priority-menu bg-base-200 rounded-box shadow p-2"
|
||||||
>
|
>
|
||||||
<li v-for="p in PRIORITIES" :key="p">
|
<button
|
||||||
<a @click="setPriority(p)">({{ p }})</a>
|
v-for="p in PRIORITIES"
|
||||||
</li>
|
:key="p"
|
||||||
<li>
|
type="button"
|
||||||
<a @click="setPriority(undefined)">none</a>
|
:class="priorityBadgeClassFor(p)"
|
||||||
</li>
|
class="todo-priority-option w-10 justify-center cursor-pointer"
|
||||||
</ul>
|
@click="setPriority(p)"
|
||||||
</details>
|
>
|
||||||
|
({{ p }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="priorityBadgeClassFor(undefined)"
|
||||||
|
class="todo-priority-option w-10 justify-center cursor-pointer"
|
||||||
|
@click="setPriority(undefined)"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<input
|
<input
|
||||||
@@ -445,6 +474,51 @@ const closeDropdown = () => {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todo-priority {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lift the whole priority widget above sibling rows while its menu is open,
|
||||||
|
// otherwise the absolutely-positioned menu paints behind row N+1's content.
|
||||||
|
// 1 is enough: no other element in this view sets a non-auto z-index.
|
||||||
|
.todo-priority--open {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-priority-trigger {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-priority-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-priority-option {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
transition:
|
||||||
|
transform 120ms ease,
|
||||||
|
filter 120ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.todo-chip {
|
.todo-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -89,8 +89,23 @@ const allContexts = computed(() => {
|
|||||||
return Array.from(set).sort()
|
return Array.from(set).sort()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// "none" represents tasks without a priority.
|
||||||
|
const allPriorities = computed<Array<string | "none">>(() => {
|
||||||
|
const set = new Set<string | "none">()
|
||||||
|
items.value.forEach((line) => {
|
||||||
|
if (isBlank(line)) return
|
||||||
|
set.add(line.priority ?? "none")
|
||||||
|
})
|
||||||
|
return Array.from(set).sort((a, b) => {
|
||||||
|
if (a === "none") return 1
|
||||||
|
if (b === "none") return -1
|
||||||
|
return a.localeCompare(b)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const activeProjects = ref<Set<string>>(new Set())
|
const activeProjects = ref<Set<string>>(new Set())
|
||||||
const activeContexts = ref<Set<string>>(new Set())
|
const activeContexts = ref<Set<string>>(new Set())
|
||||||
|
const activePriorities = ref<Set<string | "none">>(new Set())
|
||||||
|
|
||||||
const toggleProject = (p: string) => {
|
const toggleProject = (p: string) => {
|
||||||
const next = new Set(activeProjects.value)
|
const next = new Set(activeProjects.value)
|
||||||
@@ -106,15 +121,39 @@ const toggleContext = (c: string) => {
|
|||||||
activeContexts.value = next
|
activeContexts.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePriority = (p: string | "none") => {
|
||||||
|
const next = new Set(activePriorities.value)
|
||||||
|
if (next.has(p)) next.delete(p)
|
||||||
|
else next.add(p)
|
||||||
|
activePriorities.value = next
|
||||||
|
}
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
activeProjects.value = new Set()
|
activeProjects.value = new Set()
|
||||||
activeContexts.value = new Set()
|
activeContexts.value = new Set()
|
||||||
|
activePriorities.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilters = computed(
|
const hasFilters = computed(
|
||||||
() => activeProjects.value.size > 0 || activeContexts.value.size > 0
|
() =>
|
||||||
|
activeProjects.value.size > 0 ||
|
||||||
|
activeContexts.value.size > 0 ||
|
||||||
|
activePriorities.value.size > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const priorityBadgeFilterClass = (p: string | "none"): string => {
|
||||||
|
switch (p) {
|
||||||
|
case "A":
|
||||||
|
return "badge-error"
|
||||||
|
case "B":
|
||||||
|
return "badge-warning"
|
||||||
|
case "C":
|
||||||
|
return "badge-info"
|
||||||
|
default:
|
||||||
|
return "badge-ghost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filteredEntries = computed(() => {
|
const filteredEntries = computed(() => {
|
||||||
if (!hasFilters.value) return taskEntries.value
|
if (!hasFilters.value) return taskEntries.value
|
||||||
return taskEntries.value.filter(({ line }) => {
|
return taskEntries.value.filter(({ line }) => {
|
||||||
@@ -126,7 +165,10 @@ const filteredEntries = computed(() => {
|
|||||||
const contextsOk =
|
const contextsOk =
|
||||||
activeContexts.value.size === 0 ||
|
activeContexts.value.size === 0 ||
|
||||||
taskContexts.some((c) => activeContexts.value.has(c))
|
taskContexts.some((c) => activeContexts.value.has(c))
|
||||||
return projectsOk && contextsOk
|
const prioritiesOk =
|
||||||
|
activePriorities.value.size === 0 ||
|
||||||
|
activePriorities.value.has(line.priority ?? "none")
|
||||||
|
return projectsOk && contextsOk && prioritiesOk
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -229,10 +271,33 @@ const createTodoFile = async () => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="allProjects.length || allContexts.length"
|
v-if="
|
||||||
|
allProjects.length || allContexts.length || allPriorities.length
|
||||||
|
"
|
||||||
class="todo-filters"
|
class="todo-filters"
|
||||||
>
|
>
|
||||||
<div class="todo-filter-columns">
|
<div class="todo-filter-columns">
|
||||||
|
<section
|
||||||
|
v-if="allPriorities.length"
|
||||||
|
class="todo-filter-column"
|
||||||
|
>
|
||||||
|
<h4 class="todo-filter-label">Priorities</h4>
|
||||||
|
<div class="todo-filter-chips">
|
||||||
|
<button
|
||||||
|
v-for="p in allPriorities"
|
||||||
|
:key="`pri:${p}`"
|
||||||
|
type="button"
|
||||||
|
class="todo-filter-chip badge"
|
||||||
|
:class="[
|
||||||
|
priorityBadgeFilterClass(p),
|
||||||
|
{ 'badge-soft': !activePriorities.has(p) }
|
||||||
|
]"
|
||||||
|
@click="togglePriority(p)"
|
||||||
|
>
|
||||||
|
{{ p === "none" ? "—" : `(${p})` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section
|
<section
|
||||||
v-if="allProjects.length"
|
v-if="allProjects.length"
|
||||||
class="todo-filter-column"
|
class="todo-filter-column"
|
||||||
@@ -370,41 +435,40 @@ const createTodoFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.todo-filter-columns {
|
.todo-filter-columns {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-filter-column {
|
.todo-filter-column {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-filter-label {
|
.todo-filter-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin: 0 0 0.35rem;
|
margin: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-align: center;
|
flex: 0 0 auto;
|
||||||
|
min-width: 5.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-filter-chips {
|
.todo-filter-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
justify-content: center;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-filter-chip {
|
.todo-filter-chip {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.todo-filter-columns {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user