feat: init
This commit is contained in:
commit
8a609da778
59 files changed
+10053
No files matched your search
@@ -0,0 +1,194 @@
|
||||
<script lang="ts">
|
||||
import Archive from "@lucide/svelte/icons/archive";
|
||||
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 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";
|
||||
import { followLink, markdown } from "./markdown";
|
||||
import { taskMenu } from "./task-menu";
|
||||
import { useView } from "./context";
|
||||
|
||||
interface Props {
|
||||
showProject?: boolean;
|
||||
showSphere?: boolean;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
let { task, showSphere = true, showProject = true }: Props = $props();
|
||||
|
||||
const view = useView();
|
||||
|
||||
let editing = $state(false);
|
||||
let draft = $state("");
|
||||
|
||||
const meta = $derived(STATUS_META[task.status]);
|
||||
const target = $derived(dropTarget.task(task.id));
|
||||
const zone = $derived(dropZone());
|
||||
const isDropTarget = $derived(zone?.target === target);
|
||||
const date = $derived(anchorDate(task));
|
||||
const field = $derived(!task.due && task.scheduled ? "scheduled" : "due");
|
||||
|
||||
function startEdit() {
|
||||
draft = task.text;
|
||||
editing = true;
|
||||
}
|
||||
|
||||
async function commitEdit() {
|
||||
if (!editing) return;
|
||||
editing = false;
|
||||
await setText(view.app, task, draft);
|
||||
}
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void commitEdit();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
editing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
await setStatus(view.app, task, isOpen(task.status) ? "x" : " ");
|
||||
}
|
||||
</script>
|
||||
|
||||
<ContextArea class="contents" items={taskMenu(view, task)}>
|
||||
<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",
|
||||
!isOpen(task.status) && "opacity-60 hover:opacity-100",
|
||||
isDropTarget &&
|
||||
(zone?.after
|
||||
? "shadow-[inset_0_-2px_0_0_var(--signal)]"
|
||||
: "shadow-[inset_0_2px_0_0_var(--signal)]"),
|
||||
)}
|
||||
data-drop={target}
|
||||
style="grid-template-columns: 1.25rem minmax(0, 1fr) auto;"
|
||||
use:draggable={{
|
||||
payload: () => ({ id: task.id, kind: "task" }),
|
||||
ondrop: (to, after) => void handleDrop(view, task, to, after),
|
||||
enabled: () => !editing,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label={`Status: ${meta.label}`}
|
||||
class="grid size-5 place-items-center rounded-md outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-nodrag
|
||||
onclick={toggle}
|
||||
title={meta.label}
|
||||
type="button"
|
||||
>
|
||||
<meta.icon aria-hidden="true" class="size-4" style="color: {meta.color}" />
|
||||
</button>
|
||||
|
||||
<div class="flex min-w-0 items-center gap-1.5 py-2.5">
|
||||
{#if showSphere}
|
||||
<span
|
||||
class="hidden shrink-0 rounded-md border border-border bg-secondary/40 px-1.5 py-0.5 font-medium text-[11px] text-muted-foreground @min-[30rem]:inline-block"
|
||||
>
|
||||
{task.sphere}
|
||||
</span>
|
||||
{/if}
|
||||
{#if task.inArchive}
|
||||
<span
|
||||
class="hidden shrink-0 items-center gap-1 rounded-md border border-border border-dashed px-1.5 py-0.5 text-[11px] text-muted-foreground @min-[24rem]:inline-flex"
|
||||
title={task.project}
|
||||
>
|
||||
<Archive aria-hidden="true" class="size-3" />
|
||||
{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}
|
||||
>
|
||||
{#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>
|
||||
{/if}
|
||||
|
||||
{#if editing}
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="min-w-0 flex-1 rounded-md border border-ring bg-background px-1.5 py-0.5 text-sm outline-none"
|
||||
onblur={commitEdit}
|
||||
{onkeydown}
|
||||
bind:value={draft}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
"bcal-md min-w-0 flex-1 cursor-text truncate font-medium text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
!isOpen(task.status) && "line-through decoration-muted-foreground/50",
|
||||
)}
|
||||
onclick={(event) => {
|
||||
if (!followLink(view.app, event, task.path)) startEdit();
|
||||
}}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === "Enter") startEdit();
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title={task.text}
|
||||
use:markdown={{
|
||||
app: view.app,
|
||||
component: view.component,
|
||||
sourcePath: task.path,
|
||||
text: task.text,
|
||||
}}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
{#if task.priority !== 2}
|
||||
<PriorityBars class="shrink-0" priority={task.priority} />
|
||||
{/if}
|
||||
<div class="flex justify-end @min-[22rem]:w-24">
|
||||
<DuePicker
|
||||
class={cn(
|
||||
!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}
|
||||
/>
|
||||
</div>
|
||||
<RowMenu
|
||||
class="opacity-0 transition-opacity focus-visible:opacity-100 group-hover/task:opacity-100 touch-shown"
|
||||
items={taskMenu(view, task)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContextArea>
|
||||
Reference in new issue
Block a user