feat(*): one task editor, vim navigation and undo
This commit is contained in:
1 parent
0873cbb366
commit
83ca32d98e
37 files changed
+4043
-505
No files matched your search
+50
-34
@@ -1,21 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Archive from "@lucide/svelte/icons/archive";
|
||||
import CalendarDays from "@lucide/svelte/icons/calendar-days";
|
||||
import Repeat from "@lucide/svelte/icons/repeat";
|
||||
import { dueLabel, dueTone } from "../lib/due";
|
||||
import { anchorDate, isOpen } from "../model/types";
|
||||
import type { Task } from "../model/types";
|
||||
import { draggable, dropZone } from "../lib/dnd.svelte";
|
||||
import { labelHue } from "../lib/hue";
|
||||
import { cn } from "../lib/utils";
|
||||
import { STATUS_META } from "../lib/vocab";
|
||||
import {
|
||||
moveTask,
|
||||
setDate,
|
||||
setRecurrence,
|
||||
setStatus,
|
||||
setText,
|
||||
} from "../vault/mutate";
|
||||
import { setStatus, setText } from "../vault/mutate";
|
||||
import ContextArea from "./context-area.svelte";
|
||||
import DuePicker from "./due-picker.svelte";
|
||||
import LanePicker from "./lane-picker.svelte";
|
||||
import PriorityBars from "./priority-bars.svelte";
|
||||
import RowMenu from "./row-menu.svelte";
|
||||
import { dropTarget, handleDrop } from "./drop";
|
||||
@@ -41,7 +36,11 @@
|
||||
const zone = $derived(dropZone());
|
||||
const isDropTarget = $derived(zone?.target === target);
|
||||
const date = $derived(anchorDate(task));
|
||||
const field = $derived(!task.due && task.scheduled ? "scheduled" : "due");
|
||||
const tone = $derived(dueTone(date));
|
||||
/** The editor hangs off the chip that opened it, not the whole row. */
|
||||
const chip = (field: string): string => `${field}:${task.id}`;
|
||||
const on = $derived(view.store.cursorTask === task.id);
|
||||
const here = $derived(on && view.store.keyboard);
|
||||
|
||||
function startEdit() {
|
||||
draft = task.text;
|
||||
@@ -70,16 +69,24 @@
|
||||
</script>
|
||||
|
||||
<ContextArea class="contents" items={taskMenu(view, task)}>
|
||||
<!-- scroll-mt clears the sticky group header when the cursor scrolls a row
|
||||
into view, which would otherwise land it half underneath. -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={cn(
|
||||
"group/task grid select-none items-center gap-2 border-border/60 border-b px-3 transition-colors hover:bg-secondary/50",
|
||||
"group/task grid scroll-mt-8 select-none items-center gap-2 border-border/60 border-b px-3 transition-colors hover:bg-secondary/50",
|
||||
!isOpen(task.status) && "opacity-60 hover:opacity-100",
|
||||
here && "bg-secondary/60 opacity-100 inset-ring-1 inset-ring-ring",
|
||||
isDropTarget &&
|
||||
(zone?.after
|
||||
? "shadow-[inset_0_-2px_0_0_var(--signal)]"
|
||||
: "shadow-[inset_0_2px_0_0_var(--signal)]"),
|
||||
)}
|
||||
data-cursor={on}
|
||||
data-drop={target}
|
||||
onpointerdown={() => {
|
||||
view.store.cursorTask = task.id;
|
||||
}}
|
||||
style="grid-template-columns: 1.25rem minmax(0, 1fr) auto;"
|
||||
use:draggable={{
|
||||
payload: () => ({ id: task.id, kind: "task" }),
|
||||
@@ -115,22 +122,18 @@
|
||||
{task.project}
|
||||
</span>
|
||||
{:else if showProject}
|
||||
<LanePicker
|
||||
class="hidden shrink-0 @min-[24rem]:inline-flex"
|
||||
onpick={(path, project) =>
|
||||
void moveTask(view.app, task, { path, project })}
|
||||
path={task.path}
|
||||
project={task.project}
|
||||
<button
|
||||
aria-label="Project"
|
||||
class="hue-chip hidden max-w-32 shrink-0 truncate rounded-md border px-1.5 py-0.5 text-[11px] outline-none transition-[filter] hover:brightness-110 focus-visible:ring-2 focus-visible:ring-ring @min-[24rem]:inline-block"
|
||||
data-edit={chip("project")}
|
||||
data-nodrag
|
||||
onclick={() =>
|
||||
view.store.edit(task.id, "project", `[data-edit="${chip("project")}"]`)}
|
||||
style="--hue: {labelHue(task.project)}"
|
||||
type="button"
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<span
|
||||
class="hue-chip max-w-32 truncate rounded-md border px-1.5 py-0.5 text-[11px]"
|
||||
style="--hue: {labelHue(task.project)}"
|
||||
>
|
||||
{task.project}
|
||||
</span>
|
||||
{/snippet}
|
||||
</LanePicker>
|
||||
{task.project}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if editing}
|
||||
@@ -172,18 +175,31 @@
|
||||
<PriorityBars class="shrink-0" priority={task.priority} />
|
||||
{/if}
|
||||
<div class="flex justify-end @min-[22rem]:w-24">
|
||||
<DuePicker
|
||||
<button
|
||||
aria-label={task.recurrence ? "Date and repeat" : "Date"}
|
||||
class={cn(
|
||||
"inline-flex h-6 items-center gap-1 rounded-md px-1 text-xs outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring",
|
||||
tone === "overdue" && "text-destructive",
|
||||
tone === "soon" && "text-status-snooze",
|
||||
tone === "none" && "text-muted-foreground",
|
||||
!date &&
|
||||
"opacity-0 transition-opacity group-hover/task:opacity-100 focus-visible:opacity-100 touch-shown",
|
||||
)}
|
||||
{field}
|
||||
onclear={() => void setDate(view.app, task, field, null)}
|
||||
onpick={(key) => void setDate(view.app, task, field, key)}
|
||||
onrepeat={(rule) => void setRecurrence(view.app, task, rule)}
|
||||
recurrence={task.recurrence}
|
||||
value={date}
|
||||
/>
|
||||
data-edit={chip("date")}
|
||||
data-nodrag
|
||||
onclick={() =>
|
||||
view.store.edit(task.id, "date", `[data-edit="${chip("date")}"]`)}
|
||||
type="button"
|
||||
>
|
||||
{#if task.recurrence}
|
||||
<Repeat aria-hidden="true" class="size-3" />
|
||||
{:else}
|
||||
<CalendarDays aria-hidden="true" class="size-3" />
|
||||
{/if}
|
||||
{#if dueLabel(date)}
|
||||
<span class="tabular">{dueLabel(date)}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<RowMenu
|
||||
class="opacity-0 transition-opacity focus-visible:opacity-100 group-hover/task:opacity-100 touch-shown"
|
||||
|
||||
Reference in new issue
Block a user